Commit e2e1cac2 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'ce/master' into nt/ce-to-ee-thursday

parents 0748960b 00e4ec55
...@@ -12,6 +12,7 @@ variables: ...@@ -12,6 +12,7 @@ variables:
NODE_ENV: "test" NODE_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
GIT_DEPTH: "20" GIT_DEPTH: "20"
GIT_SUBMODULE_STRATEGY: "none"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
......
...@@ -26,6 +26,7 @@ logs, and code as it's very hard to read otherwise.) ...@@ -26,6 +26,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info #### Results of GitLab environment info
<details> <details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`) `sudo gitlab-rake gitlab:env:info`)
...@@ -33,11 +34,13 @@ logs, and code as it's very hard to read otherwise.) ...@@ -33,11 +34,13 @@ logs, and code as it's very hard to read otherwise.)
(For installations from source run and paste the output of: (For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
</pre>
</details> </details>
#### Results of GitLab application Check #### Results of GitLab application Check
<details> <details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`) `sudo gitlab-rake gitlab:check SANITIZE=true`)
...@@ -47,6 +50,7 @@ logs, and code as it's very hard to read otherwise.) ...@@ -47,6 +50,7 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing) (we will only investigate if the tests are passing)
</pre>
</details> </details>
### Possible fixes ### Possible fixes
......
...@@ -154,7 +154,7 @@ gem 'after_commit_queue', '~> 1.3.0' ...@@ -154,7 +154,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2.7' gem 'sidekiq', '~> 5.0'
gem 'sidekiq-cron', '~> 0.4.4' gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
...@@ -304,6 +304,7 @@ group :development, :test do ...@@ -304,6 +304,7 @@ group :development, :test do
gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -465,7 +465,7 @@ GEM ...@@ -465,7 +465,7 @@ GEM
multi_json (~> 1.10) multi_json (~> 1.10)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.4) mail (2.6.5)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.1) mail_room (0.9.1)
memoist (0.15.0) memoist (0.15.0)
...@@ -639,7 +639,7 @@ GEM ...@@ -639,7 +639,7 @@ GEM
json json
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.2.2) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
...@@ -695,6 +695,7 @@ GEM ...@@ -695,6 +695,7 @@ GEM
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-set (0.1.3)
rspec-support (3.5.0) rspec-support (3.5.0)
rspec_profiling (0.0.5) rspec_profiling (0.0.5)
activerecord activerecord
...@@ -752,11 +753,11 @@ GEM ...@@ -752,11 +753,11 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (4.2.10) sidekiq (5.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.3, >= 3.3.3)
sidekiq-cron (0.4.4) sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2) redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24) rufus-scheduler (>= 2.0.24)
...@@ -1034,6 +1035,7 @@ DEPENDENCIES ...@@ -1034,6 +1035,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.47.1) rubocop (~> 0.47.1)
rubocop-rspec (~> 1.15.0) rubocop-rspec (~> 1.15.0)
...@@ -1050,7 +1052,7 @@ DEPENDENCIES ...@@ -1050,7 +1052,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7) sidekiq (~> 5.0)
sidekiq-cron (~> 0.4.4) sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simplecov (~> 0.14.0) simplecov (~> 0.14.0)
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ee/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ee/pipelines) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ee/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ee/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Test coverage ## Test coverage
......
...@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', { ...@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
<script>
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table.vue'; import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
...@@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati ...@@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default Vue.component('environment-component', { export default {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
...@@ -162,58 +163,65 @@ export default Vue.component('environment-component', { ...@@ -162,58 +163,65 @@ export default Vue.component('environment-component', {
}); });
}, },
}, },
};
template: ` </script>
<div :class="cssContainerClass"> <template>
<div class="top-area"> <div :class="cssContainerClass">
<ul v-if="!isLoading" class="nav-links"> <div class="top-area">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }"> <ul
<a :href="projectEnvironmentsPath"> v-if="!isLoading"
Available class="nav-links">
<span class="badge js-available-environments-count"> <li :class="{ active: scope === null || scope === 'available' }">
{{state.availableCounter}} <a :href="projectEnvironmentsPath">
</span> Available
</a> <span class="badge js-available-environments-count">
</li> {{state.availableCounter}}
<li v-bind:class="{ 'active' : scope === 'stopped' }"> </span>
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-create">
New environment
</a> </a>
</div> </li>
<li :class="{ active : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
<div
v-if="canCreateEnvironmentParsed && !isLoading"
class="nav-controls">
<a
:href="newEnvironmentPath"
class="btn btn-create">
New environment
</a>
</div> </div>
</div>
<div class="content-list environments-container"> <div class="content-list environments-container">
<div class="environments-list-loading text-center" v-if="isLoading"> <div
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i> class="environments-list-loading text-center"
</div> v-if="isLoading">
<div class="blank-state blank-state-no-icon" <i
v-if="!isLoading && state.environments.length === 0"> class="fa fa-spinner fa-spin"
<h2 class="blank-state-title js-blank-state-title"> aria-hidden="true" />
You don't have any environments right now. </div>
</h2>
<p class="blank-state-text"> <div
Environments are places where code gets deployed, such as staging or production. class="blank-state blank-state-no-icon"
<br /> v-if="!isLoading && state.environments.length === 0">
<a :href="helpPagePath"> <h2 class="blank-state-title js-blank-state-title">
Read more about environments You don't have any environments right now.
</a> </h2>
</p> <p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<a v-if="canCreateEnvironmentParsed" <br />
:href="newEnvironmentPath" <a :href="helpPagePath">
class="btn btn-create js-new-environment-button"> Read more about environments
New Environment
</a> </a>
<<<<<<< HEAD:app/assets/javascripts/environments/components/environment.js
</div> </div>
<div class="table-holder" <div class="table-holder"
...@@ -233,7 +241,34 @@ export default Vue.component('environment-component', { ...@@ -233,7 +241,34 @@ export default Vue.component('environment-component', {
:change="changePage" :change="changePage"
:pageInfo="state.paginationInformation"> :pageInfo="state.paginationInformation">
</table-pagination> </table-pagination>
=======
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
</a>
>>>>>>> ce/master:app/assets/javascripts/environments/components/environment.vue
</div>
<div
class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div> </div>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation" />
</div> </div>
`, </div>
}); </template>
import EnvironmentsComponent from './components/environment'; import Vue from 'vue';
import EnvironmentsComponent from './components/environment.vue';
$(() => { document.addEventListener('DOMContentLoaded', () => new Vue({
window.gl = window.gl || {}; el: '#environments-list-view',
components: {
if (gl.EnvironmentsListApp) { 'environments-table-app': EnvironmentsComponent,
gl.EnvironmentsListApp.$destroy(true); },
} render: createElement => createElement('environments-table-app'),
}));
gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
});
});
import EnvironmentsFolderComponent from './environments_folder_view'; import Vue from 'vue';
import EnvironmentsFolderComponent from './environments_folder_view.vue';
$(() => { document.addEventListener('DOMContentLoaded', () => new Vue({
window.gl = window.gl || {}; el: '#environments-folder-list-view',
components: {
if (gl.EnvironmentsListFolderApp) { 'environments-folder-app': EnvironmentsFolderComponent,
gl.EnvironmentsListFolderApp.$destroy(true); },
} render: createElement => createElement('environments-folder-app'),
}));
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
<script>
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table.vue'; import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
...@@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati ...@@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('environment-folder-view', { export default {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
'table-pagination': TablePaginationComponent, 'table-pagination': TablePaginationComponent,
...@@ -128,43 +128,56 @@ export default Vue.component('environment-folder-view', { ...@@ -128,43 +128,56 @@ export default Vue.component('environment-folder-view', {
return param; return param;
}, },
}, },
};
</script>
<template>
<div :class="cssContainerClass">
<div
class="top-area"
v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li :class="{ active: scope === null || scope === 'available' }">
<a
:href="availablePath"
class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :class="{ active : scope === 'stopped' }">
<a
:href="stoppedPath"
class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
template: ` <div class="environments-container">
<div :class="cssContainerClass"> <div
<div class="top-area" v-if="!isLoading"> class="environments-list-loading text-center"
v-if="isLoading">
<h4 class="js-folder-name environments-folder-name"> <i
Environments / <b>{{folderName}}</b> class="fa fa-spinner fa-spin"
</h4> aria-hidden="true"/>
<ul class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="availablePath" class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div> </div>
<div class="environments-container"> <div
<div class="environments-list-loading text-center" v-if="isLoading"> class="table-holder"
<i class="fa fa-spinner fa-spin"></i> v-if="!isLoading && state.environments.length > 0">
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<<<<<<< HEAD:app/assets/javascripts/environments/folder/environments_folder_view.js
<environment-table <environment-table
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
...@@ -172,12 +185,19 @@ export default Vue.component('environment-folder-view', { ...@@ -172,12 +185,19 @@ export default Vue.component('environment-folder-view', {
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
:store="store" :store="store"
:service="service"/> :service="service"/>
=======
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" <environment-table
:change="changePage" :environments="state.environments"
:pageInfo="state.paginationInformation"/> :can-create-deployment="canCreateDeploymentParsed"
</div> :can-read-environment="canReadEnvironmentParsed"
:service="service"/>
>>>>>>> ce/master:app/assets/javascripts/environments/folder/environments_folder_view.vue
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation"/>
</div> </div>
</div> </div>
`, </div>
}); </template>
...@@ -81,13 +81,14 @@ class FilteredSearchManager { ...@@ -81,13 +81,14 @@ class FilteredSearchManager {
this.checkForEnterWrapper = this.checkForEnter.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this); this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
this.removeTokenWrapper = this.removeToken.bind(this);
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
...@@ -100,12 +101,13 @@ class FilteredSearchManager { ...@@ -100,12 +101,13 @@ class FilteredSearchManager {
this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
...@@ -121,12 +123,13 @@ class FilteredSearchManager { ...@@ -121,12 +123,13 @@ class FilteredSearchManager {
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
...@@ -201,14 +204,28 @@ class FilteredSearchManager { ...@@ -201,14 +204,28 @@ class FilteredSearchManager {
static selectToken(e) { static selectToken(e) {
const button = e.target.closest('.selectable'); const button = e.target.closest('.selectable');
const removeButtonSelected = e.target.closest('.remove-token');
if (button) { if (!removeButtonSelected && button) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button); gl.FilteredSearchVisualTokens.selectToken(button);
} }
} }
removeToken(e) {
const removeButtonSelected = e.target.closest('.remove-token');
if (removeButtonSelected) {
e.preventDefault();
e.stopPropagation();
const button = e.target.closest('.selectable');
gl.FilteredSearchVisualTokens.selectToken(button, true);
this.removeSelectedToken();
}
}
unselectEditTokens(e) { unselectEditTokens(e) {
const inputContainer = this.container.querySelector('.filtered-search-box'); const inputContainer = this.container.querySelector('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
...@@ -256,16 +273,21 @@ class FilteredSearchManager { ...@@ -256,16 +273,21 @@ class FilteredSearchManager {
} }
} }
removeSelectedToken(e) { removeSelectedTokenKeydown(e) {
// 8 = Backspace Key // 8 = Backspace Key
// 46 = Delete Key // 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken(); this.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
} }
} }
removeSelectedToken() {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
this.dropdownManager.updateCurrentDropdownOffset();
}
onClearSearch(e) { onClearSearch(e) {
e.preventDefault(); e.preventDefault();
this.clearSearch(); this.clearSearch();
......
...@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens { ...@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
} }
static selectToken(tokenButton) { static selectToken(tokenButton, forceSelection = false) {
const selected = tokenButton.classList.contains('selected'); const selected = tokenButton.classList.contains('selected');
FilteredSearchVisualTokens.unselectTokens(); FilteredSearchVisualTokens.unselectTokens();
if (!selected) { if (!selected || forceSelection) {
tokenButton.classList.add('selected'); tokenButton.classList.add('selected');
} }
} }
...@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens { ...@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
return ` return `
<div class="selectable" role="button"> <div class="selectable" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value"></div> <div class="value-container">
<div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div>
</div> </div>
`; `;
} }
...@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens { ...@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
if (value) { if (value) {
const button = lastVisualToken.querySelector('.selectable'); const button = lastVisualToken.querySelector('.selectable');
button.removeChild(value); const valueContainer = lastVisualToken.querySelector('.value-container');
button.removeChild(valueContainer);
lastVisualToken.innerHTML = button.innerHTML; lastVisualToken.innerHTML = button.innerHTML;
} else { } else {
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import emojiMap from 'emojis/digests.json'; import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji'; import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
// Creates the variables for setting up GFM auto-completion // Creates the variables for setting up GFM auto-completion
window.gl = window.gl || {}; window.gl = window.gl || {};
...@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = { ...@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = {
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert, beforeInsert: this.DefaultOptions.beforeInsert,
filter: this.DefaultOptions.filter filter: this.DefaultOptions.filter,
matcher: (flag, subtext) => {
const relevantText = subtext.trim().split(/\s/).pop();
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
const match = regexp.exec(relevantText);
return match && match.length ? match[1] : null;
}
} }
}); });
// Team Members // Team Members
......
/**
* Regexp utility for the convenience of working with regular expressions.
*
*/
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
// Unicode 6.1
const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
export default { unicodeLetters };
...@@ -28,7 +28,9 @@ export default class MiniPipelineGraph { ...@@ -28,7 +28,9 @@ export default class MiniPipelineGraph {
* All dropdown events are fired at the .dropdown-menu's parent element. * All dropdown events are fired at the .dropdown-menu's parent element.
*/ */
bindEvents() { bindEvents() {
$(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); $(document)
.off('shown.bs.dropdown', this.container)
.on('shown.bs.dropdown', this.container, this.getBuildsList);
} }
/** /**
...@@ -91,6 +93,9 @@ export default class MiniPipelineGraph { ...@@ -91,6 +93,9 @@ export default class MiniPipelineGraph {
}, },
error: () => { error: () => {
this.toggleLoading(button); this.toggleLoading(button);
if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle');
}
new Flash('An error occurred while fetching the builds.', 'alert'); new Flash('An error occurred while fetching the builds.', 'alert');
}, },
}); });
......
...@@ -2,13 +2,6 @@ ...@@ -2,13 +2,6 @@
import StatusIconEntityMap from '../../ci_status_icons'; import StatusIconEntityMap from '../../ci_status_icons';
export default { export default {
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: { props: {
stage: { stage: {
type: Object, type: Object,
...@@ -16,6 +9,13 @@ export default { ...@@ -16,6 +9,13 @@ export default {
}, },
}, },
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
updated() { updated() {
if (this.builds) { if (this.builds) {
this.stopDropdownClickPropagation(); this.stopDropdownClickPropagation();
...@@ -31,7 +31,13 @@ export default { ...@@ -31,7 +31,13 @@ export default {
return this.$http.get(this.stage.dropdown_path) return this.$http.get(this.stage.dropdown_path)
.then((response) => { .then((response) => {
this.builds = JSON.parse(response.body).html; this.builds = JSON.parse(response.body).html;
}, () => { })
.catch(() => {
// If dropdown is opened we'll close it.
if (this.$el.classList.contains('open')) {
$(this.$refs.dropdown).dropdown('toggle');
}
const flash = new Flash('Something went wrong on our end.'); const flash = new Flash('Something went wrong on our end.');
return flash; return flash;
}); });
...@@ -46,9 +52,10 @@ export default { ...@@ -46,9 +52,10 @@ export default {
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
e.stopPropagation(); .on('click', (e) => {
}); e.stopPropagation();
});
}, },
}, },
computed: { computed: {
...@@ -81,12 +88,22 @@ export default { ...@@ -81,12 +88,22 @@ export default {
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
type="button" type="button"
:aria-label="stage.title"> :aria-label="stage.title"
<span v-html="svgHTML" aria-hidden="true"></span> ref="dropdown">
<i class="fa fa-caret-down" aria-hidden="true"></i> <span
v-html="svgHTML"
aria-hidden="true">
</span>
<i
class="fa fa-caret-down"
aria-hidden="true" />
</button> </button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul
<div class="arrow-up" aria-hidden="true"></div> ref="dropdown-content"
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div
class="arrow-up"
aria-hidden="true"></div>
<div <div
:class="dropdownClass" :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
......
...@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg'; ...@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility'; import '../../lib/utils/datetime_utility';
export default { export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
data() { data() {
return { return {
currentTime: new Date(),
iconTimerSvg, iconTimerSvg,
}; };
}, },
props: ['pipeline'],
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
computed: { computed: {
timeAgo() { hasDuration() {
return gl.utils.getTimeago(); return this.duration > 0;
}, },
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at); hasFinishedTime() {
return this.finishedTime !== '';
}, },
timeStopped() {
const changeTime = this.currentTime; localTimeFinished() {
const options = { return gl.utils.formatDate(this.finishedTime);
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
}, },
duration() {
const { duration } = this.pipeline.details; durationFormated() {
const date = new Date(duration * 1000); const date = new Date(this.duration * 1000);
let hh = date.getUTCHours(); let hh = date.getUTCHours();
let mm = date.getUTCMinutes(); let mm = date.getUTCMinutes();
let ss = date.getSeconds(); let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`; // left pad
if (mm < 10) mm = `0${mm}`; if (hh < 10) {
if (ss < 10) ss = `0${ss}`; hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
if (duration !== null) return `${hh}:${mm}:${ss}`; return `${hh}:${mm}:${ss}`;
return false;
}, },
},
methods: { finishedTimeFormated() {
changeTime() { const timeAgo = gl.utils.getTimeago();
this.currentTime = new Date();
return timeAgo.format(this.finishedTime);
}, },
}, },
template: ` template: `
<td class="pipelines-time-ago"> <td class="pipelines-time-ago">
<p class="duration" v-if='duration'> <p
<span v-html="iconTimerSvg"></span> class="duration"
{{duration}} v-if="hasDuration">
<span
v-html="iconTimerSvg">
</span>
{{durationFormated}}
</p> </p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i> <p
class="finished-at"
v-if="hasFinishedTime">
<i
class="fa fa-calendar"
aria-hidden="true" />
<time <time
ref="tooltip"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
data-container="body" data-container="body"
:data-original-title='localTimeFinished'> :title="localTimeFinished">
{{timeStopped.words}} {{finishedTimeFormated}}
</time> </time>
</p> </p>
</td> </td>
......
import Vue from 'vue';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service'; import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
...@@ -161,15 +160,6 @@ export default { ...@@ -161,15 +160,6 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
/* eslint-disable no-underscore-dangle*/
import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore { export default class PipelinesStore {
constructor() { constructor() {
this.state = {}; this.state = {};
...@@ -30,32 +27,4 @@ export default class PipelinesStore { ...@@ -30,32 +27,4 @@ export default class PipelinesStore {
this.state.pageInfo = paginationInfo; this.state.pageInfo = paginationInfo;
} }
/**
* FIXME: Move this inside the component.
*
* Once the data is received we will start the time ago loops.
*
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed.
*
*/
startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
VueRealtimeListener(removeIntervals, startIntervals);
}
} }
export default (removeIntervals, startIntervals) => {
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
window.removeEventListener('onbeforeload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
window.addEventListener('onbeforeload', removeIntervals);
};
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
...@@ -166,6 +165,32 @@ export default { ...@@ -166,6 +165,32 @@ export default {
} }
return undefined; return undefined;
}, },
/**
* Timeago components expects a number
*
* @return {type} description
*/
pipelineDuration() {
if (this.pipeline.details && this.pipeline.details.duration) {
return this.pipeline.details.duration;
}
return 0;
},
/**
* Timeago component expects a String.
*
* @return {String}
*/
pipelineFinishedAt() {
if (this.pipeline.details && this.pipeline.details.finished_at) {
return this.pipeline.details.finished_at;
}
return '';
},
}, },
template: ` template: `
...@@ -192,7 +217,9 @@ export default { ...@@ -192,7 +217,9 @@ export default {
</div> </div>
</td> </td>
<time-ago :pipeline="pipeline"/> <time-ago
:duration="pipelineDuration"
:finished-time="pipelineFinishedAt" />
<td class="pipeline-actions"> <td class="pipeline-actions">
<div class="pull-right btn-group"> <div class="pull-right btn-group">
......
...@@ -195,7 +195,6 @@ ...@@ -195,7 +195,6 @@
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
overflow: hidden;
@include set-invisible; @include set-invisible;
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
......
...@@ -104,6 +104,24 @@ ...@@ -104,6 +104,24 @@
padding: 2px 7px; padding: 2px 7px;
} }
.value {
padding-right: 0;
}
.remove-token {
display: inline-block;
padding-left: 4px;
padding-right: 8px;
.fa-close {
color: $gl-text-color-disabled;
}
&:hover .fa-close {
color: $gl-text-color-secondary;
}
}
.name { .name {
background-color: $filter-name-resting-color; background-color: $filter-name-resting-color;
color: $filter-name-text-color; color: $filter-name-text-color;
...@@ -112,7 +130,7 @@ ...@@ -112,7 +130,7 @@
text-transform: capitalize; text-transform: capitalize;
} }
.value { .value-container {
background-color: $white-normal; background-color: $white-normal;
color: $filter-value-text-color; color: $filter-value-text-color;
border-radius: 0 2px 2px 0; border-radius: 0 2px 2px 0;
...@@ -124,7 +142,7 @@ ...@@ -124,7 +142,7 @@
background-color: $filter-name-selected-color; background-color: $filter-name-selected-color;
} }
.value { .value-container {
background-color: $filter-value-selected-color; background-color: $filter-value-selected-color;
} }
} }
......
...@@ -622,6 +622,7 @@ pre.light-well { ...@@ -622,6 +622,7 @@ pre.light-well {
.controls { .controls {
margin-left: auto; margin-left: auto;
text-align: right;
} }
.ci-status-link { .ci-status-link {
......
...@@ -160,7 +160,6 @@ ...@@ -160,7 +160,6 @@
.tree-controls { .tree-controls {
float: right; float: right;
margin-top: 11px;
position: relative; position: relative;
z-index: 2; z-index: 2;
......
module MarkdownPreview
private
def render_markdown_preview(text, markdown_context = {})
render json: {
body: view_context.markdown(text, markdown_context),
references: {
users: preview_referenced_users(text)
}
}
end
def preview_referenced_users(text)
extractor = Gitlab::ReferenceExtractor.new(@project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
end
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include MarkdownPreview
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy before_action :authorize_admin_wiki!, only: :destroy
...@@ -97,21 +99,13 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -97,21 +99,13 @@ class Projects::WikisController < Projects::ApplicationController
) )
end end
def preview_markdown def git_access
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: ext.users.map(&:username)
}
}
end end
def git_access def preview_markdown
context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
render_markdown_preview(params[:text], context)
end end
private private
...@@ -121,7 +115,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -121,7 +115,6 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized # Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki @project_wiki.wiki
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -217,20 +218,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -217,20 +218,6 @@ class ProjectsController < Projects::ApplicationController
} }
end end
def preview_markdown
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text),
references: {
users: ext.users.map(&:username)
}
}
end
def refs def refs
branches = BranchesFinder.new(@repository, params).execute.map(&:name) branches = BranchesFinder.new(@repository, params).execute.map(&:name)
...@@ -253,6 +240,10 @@ class ProjectsController < Projects::ApplicationController ...@@ -253,6 +240,10 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json render json: options.to_json
end end
def preview_markdown
render_markdown_preview(params[:text])
end
private private
# Render project landing depending of which features are available # Render project landing depending of which features are available
......
...@@ -2,6 +2,7 @@ class SnippetsController < ApplicationController ...@@ -2,6 +2,7 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
...@@ -77,6 +78,10 @@ class SnippetsController < ApplicationController ...@@ -77,6 +78,10 @@ class SnippetsController < ApplicationController
) )
end end
def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true)
end
protected protected
def snippet def snippet
......
...@@ -199,38 +199,6 @@ module ApplicationHelper ...@@ -199,38 +199,6 @@ module ApplicationHelper
end end
end end
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Hamlit::RailsHelpers.preserve(markdown(file_content))
elsif asciidoc?(file_name)
asciidoc(file_content)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
file_content
end
else
other_markup(file_name, file_content)
end
rescue RuntimeError
simple_format(file_content)
end
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
def markup?(filename)
Gitlab::MarkupHelper.markup?(filename)
end
def gitlab_markdown?(filename)
Gitlab::MarkupHelper.gitlab_markdown?(filename)
end
def asciidoc?(filename)
Gitlab::MarkupHelper.asciidoc?(filename)
end
def promo_host def promo_host
'about.gitlab.com' 'about.gitlab.com'
end end
......
require 'nokogiri' require 'nokogiri'
module GitlabMarkdownHelper module MarkupHelper
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
def markup?(filename)
Gitlab::MarkupHelper.markup?(filename)
end
def gitlab_markdown?(filename)
Gitlab::MarkupHelper.gitlab_markdown?(filename)
end
def asciidoc?(filename)
Gitlab::MarkupHelper.asciidoc?(filename)
end
# Use this in places where you would normally use link_to(gfm(...), ...). # Use this in places where you would normally use link_to(gfm(...), ...).
# #
# It solves a problem occurring with nested links (i.e. # It solves a problem occurring with nested links (i.e.
...@@ -11,7 +27,7 @@ module GitlabMarkdownHelper ...@@ -11,7 +27,7 @@ module GitlabMarkdownHelper
# explicitly produce the correct linking behavior (i.e. # explicitly produce the correct linking behavior (i.e.
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>"). # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
def link_to_gfm(body, url, html_options = {}) def link_to_gfm(body, url, html_options = {})
return "" if body.blank? return '' if body.blank?
context = { context = {
project: @project, project: @project,
...@@ -43,71 +59,73 @@ module GitlabMarkdownHelper ...@@ -43,71 +59,73 @@ module GitlabMarkdownHelper
fragment.to_html.html_safe fragment.to_html.html_safe
end end
# Return the first line of +text+, up to +max_chars+, after parsing the line
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {})
md = markdown(text, options).strip
truncate_visible(md, max_chars || md.length) if md.present?
end
def markdown(text, context = {}) def markdown(text, context = {})
return "" unless text.present? return '' unless text.present?
context[:project] ||= @project context[:project] ||= @project
html = markdown_unsafe(text, context)
html = Banzai.render(text, context)
banzai_postprocess(html, context) banzai_postprocess(html, context)
end end
def markdown_field(object, field) def markdown_field(object, field)
object = object.for_display if object.respond_to?(:for_display) object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present? return '' unless object.present?
html = Banzai.render_field(object, field) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field)) banzai_postprocess(html, object.banzai_render_context(field))
end end
def asciidoc(text) def markup(file_name, text, context = {})
Gitlab::Asciidoc.render( context[:project] ||= @project
text, html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
project: @project, banzai_postprocess(html, context)
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
end end
def other_markup(file_name, text) def render_wiki_content(wiki_page)
Gitlab::OtherMarkup.render( text = wiki_page.content
file_name, return '' unless text.present?
text,
project: @project, context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
current_user: (current_user if defined?(current_user)),
html =
case wiki_page.format
when :markdown
markdown_unsafe(text, context)
when :asciidoc
asciidoc_unsafe(text)
else
wiki_page.formatted_content.html_safe
end
# RelativeLinkFilter banzai_postprocess(html, context)
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
end end
# Return the first line of +text+, up to +max_chars+, after parsing the line def markup_unsafe(file_name, text, context = {})
# as Markdown. HTML tags in the parsed output are not counted toward the return '' unless text.present?
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {})
md = markdown(text, options).strip
truncate_visible(md, max_chars || md.length) if md.present? if gitlab_markdown?(file_name)
end Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context))
elsif asciidoc?(file_name)
def render_wiki_content(wiki_page) asciidoc_unsafe(text)
case wiki_page.format elsif plain?(file_name)
when :markdown content_tag :pre, class: 'plain-readme' do
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) text
when :asciidoc end
asciidoc(wiki_page.content)
else else
wiki_page.formatted_content.html_safe other_markup_unsafe(file_name, text)
end end
rescue RuntimeError
simple_format(text)
end end
# Returns the text necessary to reference `entity` across projects # Returns the text necessary to reference `entity` across projects
...@@ -183,10 +201,10 @@ module GitlabMarkdownHelper ...@@ -183,10 +201,10 @@ module GitlabMarkdownHelper
end end
def markdown_toolbar_button(options = {}) def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: "body" }) data = options[:data].merge({ container: 'body' })
content_tag :button, content_tag :button,
type: "button", type: 'button',
class: "toolbar-btn js-md has-tooltip hidden-xs", class: 'toolbar-btn js-md has-tooltip hidden-xs',
tabindex: -1, tabindex: -1,
data: data, data: data,
title: options[:title], title: options[:title],
...@@ -195,17 +213,34 @@ module GitlabMarkdownHelper ...@@ -195,17 +213,34 @@ module GitlabMarkdownHelper
end end
end end
def markdown_unsafe(text, context = {})
Banzai.render(text, context)
end
def asciidoc_unsafe(text)
Gitlab::Asciidoc.render(text)
end
def other_markup_unsafe(file_name, text)
Gitlab::OtherMarkup.render(file_name, text)
end
# Calls Banzai.post_process with some common context options # Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context) def banzai_postprocess(html, context = {})
return '' unless html.present?
context.merge!( context.merge!(
current_user: (current_user if defined?(current_user)), current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter # RelativeLinkFilter
requested_path: @path, commit: @commit,
project_wiki: @project_wiki, project_wiki: @project_wiki,
ref: @ref ref: @ref,
requested_path: @path
) )
Banzai.post_process(html, context) Banzai.post_process(html, context)
end end
extend self
end end
...@@ -160,7 +160,7 @@ module ProjectsHelper ...@@ -160,7 +160,7 @@ module ProjectsHelper
end end
def project_list_cache_key(project) def project_list_cache_key(project)
key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3'] key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.4']
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
key key
......
...@@ -13,8 +13,8 @@ module ServicesHelper ...@@ -13,8 +13,8 @@ module ServicesHelper
"Event will be triggered when a confidential issue is created/updated/closed" "Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events" when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged" "Event will be triggered when a merge request is created/updated/merged"
when "build", "build_events" when "pipeline", "pipeline_events"
"Event will be triggered when a build status changes" "Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events" when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events" when "commit", "commit_events"
......
...@@ -12,10 +12,6 @@ module TreeHelper ...@@ -12,10 +12,6 @@ module TreeHelper
tree.html_safe tree.html_safe
end end
def render_readme(readme)
render_markup(readme.name, readme.data)
end
# Return an image icon depending on the file type and mode # Return an image icon depending on the file type and mode
# #
# type - String type of the tree item; either 'folder' or 'file' # type - String type of the tree item; either 'folder' or 'file'
......
class BaseMailer < ActionMailer::Base class BaseMailer < ActionMailer::Base
helper ApplicationHelper helper ApplicationHelper
helper GitlabMarkdownHelper helper MarkupHelper
attr_accessor :current_user attr_accessor :current_user
helper_method :current_user, :can? helper_method :current_user, :can?
......
...@@ -29,6 +29,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -29,6 +29,8 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :uuid, presence: true
validates :session_expire_delay, validates :session_expire_delay,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
...@@ -176,6 +178,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -176,6 +178,7 @@ class ApplicationSetting < ActiveRecord::Base
end end
end end
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token before_save :ensure_health_check_access_token
...@@ -402,6 +405,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -402,6 +405,12 @@ class ApplicationSetting < ActiveRecord::Base
private private
def ensure_uuid!
return if uuid?
self.uuid = SecureRandom.uuid
end
def check_repository_storages def check_repository_storages
invalid = repository_storages - Gitlab.config.repositories.storages.keys invalid = repository_storages - Gitlab.config.repositories.storages.keys
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
......
...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true
......
...@@ -107,7 +107,8 @@ module Network ...@@ -107,7 +107,8 @@ module Network
def find_commits(skip = 0) def find_commits(skip = 0)
opts = { opts = {
max_count: self.class.max_count, max_count: self.class.max_count,
skip: skip skip: skip,
order: :date
} }
opts[:ref] = @commit.id if @filter_ref opts[:ref] = @commit.id if @filter_ref
......
...@@ -24,9 +24,9 @@ class Repository ...@@ -24,9 +24,9 @@ class Repository
# same name. The cache key used by those methods must also match method's # same name. The cache key used by those methods must also match method's
# name. # name.
# #
# For example, for entry `:readme` there's a method called `readme` which # For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `readme` cache key. # stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count readme contribution_guide CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze tag_count avatar exists? empty? root_ref).freeze
...@@ -35,7 +35,7 @@ class Repository ...@@ -35,7 +35,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches. # the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = { METHOD_CACHES_FOR_FILE_TYPES = {
readme: :readme, readme: :rendered_readme,
changelog: :changelog, changelog: :changelog,
license: %i(license_blob license_key), license: %i(license_blob license_key),
contributing: :contribution_guide, contributing: :contribution_guide,
...@@ -534,7 +534,11 @@ class Repository ...@@ -534,7 +534,11 @@ class Repository
head.readme head.readme
end end
end end
cache_method :readme
def rendered_readme
MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
end
cache_method :rendered_readme
def contribution_guide def contribution_guide
file_on_head(:contributing) file_on_head(:contributing)
......
...@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base ...@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: proc { |service| service.template? } validates :project_id, presence: true, unless: proc { |service| service.template? }
validates :type, presence: true
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
......
...@@ -5,7 +5,10 @@ class ProjectPolicy < BasePolicy ...@@ -5,7 +5,10 @@ class ProjectPolicy < BasePolicy
team_access!(user) team_access!(user)
owner_access! if user.admin? || owner? owner_access! if user.admin? || owner?
<<<<<<< HEAD
auditor_access! if user.auditor? auditor_access! if user.auditor?
=======
>>>>>>> ce/master
team_member_owner_access! if owner? team_member_owner_access! if owner?
if project.public? || (project.internal? && !user.external?) if project.public? || (project.internal? && !user.external?)
......
...@@ -100,7 +100,8 @@ module Projects ...@@ -100,7 +100,8 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create) system_hook_service.execute_hooks_for(@project, :create)
unless @project.group || @project.gitlab_project_import? unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user] owners = [current_user, @project.namespace.owner].compact.uniq
@project.add_master(owners, current_user: current_user)
end end
predefined_push_rule = PushRule.find_by(is_sample: true) predefined_push_rule = PushRule.find_by(is_sample: true)
......
...@@ -8,6 +8,7 @@ xml.entry do ...@@ -8,6 +8,7 @@ xml.entry do
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
xml.author do xml.author do
xml.username event.author_username
xml.name event.author_name xml.name event.author_name
xml.email event.author_public_email xml.email event.author_public_email
end end
......
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
= link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme' = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
.file-content.wiki .file-content.wiki
= cache(readme_cache_key) do = markup(readme.name, readme.data, rendered: @repository.rendered_readme)
= render_readme(readme)
- else - else
.row-content-block.second-block.center .row-content-block.second-block.center
%h3.page-title %h3.page-title
......
- blob.load_all_data!(@repository) - blob.load_all_data!(@repository)
.file-content.wiki .file-content.wiki
= render_markup(blob.name, blob.data) = markup(blob.name, blob.data)
.diff-file .diff-file
.diff-content .diff-content
- if gitlab_markdown?(@blob.name) - if markup?(@blob.name)
.file-content.wiki .file-content.wiki
= preserve do = markup(@blob.name, @content)
= markdown(@content)
- elsif markup?(@blob.name)
.file-content.wiki
= raw render_markup(@blob.name, @content)
- else - else
.file-content.code.js-syntax-highlight .file-content.code.js-syntax-highlight
- unless @diff_lines.empty? - unless @diff_lines.empty?
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
Milestone Milestone
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable
= dropdown_title("Assignee milestone") = dropdown_title("Assign milestone")
= dropdown_filter("Search milestones") = dropdown_filter("Search milestones")
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
%strong %strong
= readme.name = readme.name
.file-content.wiki .file-content.wiki
= render_readme(readme) = markup(readme.name, readme.data)
.tree-controls
= render 'projects/find_file_link'
= render 'projects/buttons/download', project: @project, ref: @ref
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path
......
...@@ -7,12 +7,4 @@ ...@@ -7,12 +7,4 @@
= render 'projects/last_push' = render 'projects/last_push'
%div{ class: container_class } %div{ class: container_class }
.tree-controls = render 'projects/files'
= render 'projects/find_file_link'
= render 'projects/buttons/download', project: @project, ref: @ref
#tree-holder.tree-holder.clearfix
.nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.file-content.wiki .file-content.wiki
- snippet_chunks.each do |chunk| - snippet_chunks.each do |chunk|
- unless chunk[:data].empty? - unless chunk[:data].empty?
= render_markup(snippet.file_name, chunk[:data]) = markup(snippet.file_name, chunk[:data])
- else - else
.file-content.code .file-content.code
.nothing-here-block Empty file .nothing-here-block Empty file
......
<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
.sidebar-collapsed-icon .sidebar-collapsed-icon
%strong %strong
= icon('exclamation', 'aria-hidden': 'true') = icon('exclamation', 'aria-hidden': 'true')
%span= milestone.issues_visible_to_user(current_user).count %span= milestone.merge_requests.count
.title.hide-collapsed .title.hide-collapsed
Merge requests Merge requests
%span.badge= milestone.merge_requests.count %span.badge= milestone.merge_requests.count
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project) - cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.updated_at)
%li.project-row{ class: css_class } %li.project-row{ class: css_class }
= cache(cache_key) do = cache(cache_key) do
...@@ -37,18 +38,21 @@ ...@@ -37,18 +38,21 @@
= markdown_field(project, :description) = markdown_field(project, :description)
.controls .controls
- if project.archived .prepend-top-0
%span.prepend-left-10.label.label-warning archived - if project.archived
- if project.pipeline_status.has_status? %span.prepend-left-10.label.label-warning archived
%span.prepend-left-10 - if project.pipeline_status.has_status?
= render_project_pipeline_status(project.pipeline_status) %span.prepend-left-10
- if forks = render_project_pipeline_status(project.pipeline_status)
%span.prepend-left-10 - if forks
= icon('code-fork') %span.prepend-left-10
= number_with_delimiter(project.forks_count) = icon('code-fork')
- if stars = number_with_delimiter(project.forks_count)
%span.prepend-left-10 - if stars
= icon('star') %span.prepend-left-10
= number_with_delimiter(project.star_count) = icon('star')
%span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } = number_with_delimiter(project.star_count)
= visibility_level_icon(project.visibility_level, fw: true) %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
= visibility_level_icon(project.visibility_level, fw: true)
.prepend-top-5
updated #{updated_tooltip}
...@@ -24,6 +24,6 @@ ...@@ -24,6 +24,6 @@
- if gitlab_markdown?(@snippet.file_name) - if gitlab_markdown?(@snippet.file_name)
= preserve(markdown_field(@snippet, :content)) = preserve(markdown_field(@snippet, :content))
- else - else
= render_markup(@snippet.file_name, @snippet.content) = markup(@snippet.file_name, @snippet.content)
- else - else
= render 'shared/file_highlight', blob: @snippet = render 'shared/file_highlight', blob: @snippet
---
title: Support Markdown previews for personal snippets
merge_request: 10810
author:
---
title: Lazily sets UUID in ApplicationSetting for new installations
merge_request:
author:
---
title: Fix UI inconsistency different files view (find file button missing)
merge_request: 9847
author: TM Lee
---
title: Add update time to project lists.
merge_request: 8514
author: Jeff Stubler
---
title: 'Remove view fragment caching for project READMEs'
merge_request: 8838
author:
---
title: Allow admins to sudo to blocked users via the API
merge_request: 10842
author:
---
title: Add button to delete filters from filtered search bar
merge_request:
author:
---
title: Fix pipeline events description for Slack and Mattermost integration
merge_request: 10908
author:
---
title: Fix ordering of commits in the network graph
merge_request: 10936
author:
---
title: Fixed milestone sidebar showing incorrect number of MRs when collapsed
merge_request: 10933
author:
---
title: Add username to activity atom feed
merge_request: 10802
author: winniehell
---
title: Bump Sidekiq to 5.0.0
merge_request:
author:
---
title: Fixed wrong method call on notify_post_receive
merge_request:
author: Luigi Leoni
---
title: Fix rendering emoji inside a string
merge_request: 10647
author: blackst0ne
---
title: Refactor backup/restore docs
merge_request:
author:
---
title: Replace header merge request icon
merge_request: 10932
author: blackst0ne
---
title: Ensure namespace owner is Master of project upon creation
merge_request: 10910
author:
---
title: Dockerfiles templates are imported from gitlab.com/gitlab-org/Dockerfile
merge_request: 10663
author:
...@@ -3,6 +3,7 @@ resources :snippets, concerns: :awardable do ...@@ -3,6 +3,7 @@ resources :snippets, concerns: :awardable do
get 'raw' get 'raw'
get 'download' get 'download'
post :mark_as_spam post :mark_as_spam
post :preview_markdown
end end
end end
......
class RemoveNilTypeServices < ActiveRecord::Migration
DOWNTIME = false
def up
execute <<-SQL
DELETE FROM services WHERE type IS NULL OR type = '';
SQL
end
def down
end
end
class FillMissingUuidOnApplicationSettings < ActiveRecord::Migration
DOWNTIME = false
def up
execute("UPDATE application_settings SET uuid = #{quote(SecureRandom.uuid)} WHERE uuid is NULL")
end
def down
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170424142900) do ActiveRecord::Schema.define(version: 20170426175636) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
This diff is collapsed.
...@@ -18,7 +18,8 @@ you need to use with GitLab. ...@@ -18,7 +18,8 @@ you need to use with GitLab.
## GitLab Pages Ports ## GitLab Pages Ports
If you're using GitLab Pages you will need some additional port configurations. If you're using GitLab Pages with custom domain support you will need some
additional port configurations.
GitLab Pages requires a separate virtual IP address. Configure DNS to point the GitLab Pages requires a separate virtual IP address. Configure DNS to point the
`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the `pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
[GitLab Pages documentation][gitlab-pages] for more information. [GitLab Pages documentation][gitlab-pages] for more information.
......
...@@ -7,21 +7,20 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as ...@@ -7,21 +7,20 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as
Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not
specifically test NFSv3. specifically test NFSv3.
**no_root_squash**: NFS normally changes the `root` user to `nobody`. This is
a good security measure when NFS shares will be accessed by many different
users. However, in this case only GitLab will use the NFS share so it
is safe. GitLab requires the `no_root_squash` setting because we need to
manage file permissions automatically. Without the setting you will receive
errors when the Omnibus package tries to alter permissions. Note that GitLab
and other bundled components do **not** run as `root` but as non-privileged
users. The requirement for `no_root_squash` is to allow the Omnibus package to
set ownership and permissions on files, as needed.
### Recommended options ### Recommended options
When you define your NFS exports, we recommend you also add the following When you define your NFS exports, we recommend you also add the following
options: options:
- `no_root_squash` - NFS normally changes the `root` user to `nobody`. This is
a good security measure when NFS shares will be accessed by many different
users. However, in this case only GitLab will use the NFS share so it
is safe. GitLab recommends the `no_root_squash` setting because we need to
manage file permissions automatically. Without the setting you may receive
errors when the Omnibus package tries to alter permissions. Note that GitLab
and other bundled components do **not** run as `root` but as non-privileged
users. The recommendation for `no_root_squash` is to allow the Omnibus package
to set ownership and permissions on files, as needed.
- `sync` - Force synchronous behavior. Default is asynchronous and under certain - `sync` - Force synchronous behavior. Default is asynchronous and under certain
circumstances it could lead to data loss if a failure occurs before data has circumstances it could lead to data loss if a failure occurs before data has
synced. synced.
......
...@@ -202,6 +202,7 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md). ...@@ -202,6 +202,7 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases. to separate phases.
- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` - Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test [four-phase-test]: https://robots.thoughtbot.com/four-phase-test
...@@ -225,6 +226,20 @@ so we need to set some guidelines for their use going forward: ...@@ -225,6 +226,20 @@ so we need to set some guidelines for their use going forward:
[lets-not]: https://robots.thoughtbot.com/lets-not [lets-not]: https://robots.thoughtbot.com/lets-not
#### `set` variables
In some cases there is no need to recreate the same object for tests again for
each example. For example, a project is needed to test issues on the same
project, one project will do for the entire file. This can be achieved by using
`set` in the same way you would use `let`.
`rspec-set` only works on ActiveRecord objects, and before new examples it
reloads or recreates the model, _only_ if needed. That is, when you changed
properties or destroyed the object.
There is one gotcha; you can't reference a model defined in a `let` block in a
`set` block.
### Time-sensitive tests ### Time-sensitive tests
[Timecop](https://github.com/travisjeffery/timecop) is available in our [Timecop](https://github.com/travisjeffery/timecop) is available in our
......
This diff is collapsed.
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
GitLab Inc. will periodically collect information about your instance in order GitLab Inc. will periodically collect information about your instance in order
to perform various actions. to perform various actions.
All statistics are opt-in and you can always disable them from the admin panel. All statistics are opt-out, you can disable them from the admin panel.
## Version check ## Version check
......
...@@ -51,9 +51,9 @@ service in GitLab. ...@@ -51,9 +51,9 @@ service in GitLab.
## Troubleshooting ## Troubleshooting
If builds are not triggered, these are a couple of things to keep in mind. If builds are not triggered, ensure you entered the right GitLab IP address in
Bamboo under 'Trigger IP addresses'.
>**Note:**
- Starting with GitLab 8.14.0, builds are triggered on push events.
1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
IP addresses'.
1. Remember that GitLab only triggers builds on push events. A commit via the
web interface will not trigger CI currently.
...@@ -104,7 +104,7 @@ module API ...@@ -104,7 +104,7 @@ module API
end end
def authenticate! def authenticate!
unauthorized! unless current_user && can?(current_user, :access_api) unauthorized! unless current_user && can?(initial_current_user, :access_api)
end end
def authenticate_non_get! def authenticate_non_get!
......
...@@ -152,7 +152,7 @@ module API ...@@ -152,7 +152,7 @@ module API
begin begin
Gitlab::GitalyClient::Notifications.new(project.repository).post_receive Gitlab::GitalyClient::Notifications.new(project.repository).post_receive
rescue GRPC::Unavailable => e rescue GRPC::Unavailable => e
render_api_error(e, 500) render_api_error!(e, 500)
end end
end end
end end
......
...@@ -15,7 +15,7 @@ module Backup ...@@ -15,7 +15,7 @@ module Backup
s[:gitlab_version] = Gitlab::VERSION s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version s[:tar_version] = tar_version
s[:skipped] = ENV["SKIP"] s[:skipped] = ENV["SKIP"]
tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}" tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{s[:gitlab_version]}#{FILE_NAME_SUFFIX}"
Dir.chdir(backup_path) do Dir.chdir(backup_path) do
File.open("#{backup_path}/backup_information.yml", "w+") do |file| File.open("#{backup_path}/backup_information.yml", "w+") do |file|
......
...@@ -53,7 +53,10 @@ module Banzai ...@@ -53,7 +53,10 @@ module Banzai
# Build a regexp that matches all valid :emoji: names. # Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ @emoji_pattern ||=
/(?<=[^[:alnum:]:]|\n|^)
:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):
(?=[^[:alnum:]:]|$)/x
end end
# Build a regexp that matches all valid unicode emojis names. # Build a regexp that matches all valid unicode emojis names.
......
...@@ -14,28 +14,16 @@ module Gitlab ...@@ -14,28 +14,16 @@ module Gitlab
# Public: Converts the provided Asciidoc markup into HTML. # Public: Converts the provided Asciidoc markup into HTML.
# #
# input - the source text in Asciidoc format # input - the source text in Asciidoc format
# context - a Hash with the template context:
# :commit
# :project
# :project_wiki
# :requested_path
# :ref
# asciidoc_opts - a Hash of options to pass to the Asciidoctor converter
# #
def self.render(input, context, asciidoc_opts = {}) def self.render(input)
asciidoc_opts.reverse_merge!( asciidoc_opts = { safe: :secure,
safe: :secure, backend: :gitlab_html5,
backend: :gitlab_html5, attributes: DEFAULT_ADOC_ATTRS }
attributes: []
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
plantuml_setup plantuml_setup
html = ::Asciidoctor.convert(input, asciidoc_opts) html = ::Asciidoctor.convert(input, asciidoc_opts)
html = Banzai.post_process(html, context)
filter = Banzai::Filter::SanitizationFilter.new(html) filter = Banzai::Filter::SanitizationFilter.new(html)
html = filter.call.to_s html = filter.call.to_s
......
...@@ -494,7 +494,9 @@ module Gitlab ...@@ -494,7 +494,9 @@ module Gitlab
# :contains is the commit contained by the refs from which to begin (SHA1 or name) # :contains is the commit contained by the refs from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch # :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip # :skip is the number of commits to skip
# :order is the commits order and allowed value is :date(default) or :topo # :order is the commits order and allowed value is :none (default), :date, or :topo
# commit ordering types are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
# #
def find_commits(options = {}) def find_commits(options = {})
actual_options = options.dup actual_options = options.dup
...@@ -522,11 +524,8 @@ module Gitlab ...@@ -522,11 +524,8 @@ module Gitlab
end end
end end
if actual_options[:order] == :topo sort_type = rugged_sort_type(actual_options[:order])
walker.sorting(Rugged::SORT_TOPO) walker.sorting(sort_type)
else
walker.sorting(Rugged::SORT_NONE)
end
commits = [] commits = []
offset = actual_options[:skip] offset = actual_options[:skip]
...@@ -1273,6 +1272,18 @@ module Gitlab ...@@ -1273,6 +1272,18 @@ module Gitlab
def gitaly_ref_client def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self) @gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self)
end end
# Returns the `Rugged` sorting type constant for a given
# sort type key. Valid keys are `:none`, `:topo`, and `:date`
def rugged_sort_type(key)
@rugged_sort_types ||= {
none: Rugged::SORT_NONE,
topo: Rugged::SORT_TOPO,
date: Rugged::SORT_DATE
}
@rugged_sort_types.fetch(key, Rugged::SORT_NONE)
end
end end
end end
end end
...@@ -4,19 +4,11 @@ module Gitlab ...@@ -4,19 +4,11 @@ module Gitlab
# Public: Converts the provided markup into HTML. # Public: Converts the provided markup into HTML.
# #
# input - the source text in a markup format # input - the source text in a markup format
# context - a Hash with the template context:
# :commit
# :project
# :project_wiki
# :requested_path
# :ref
# #
def self.render(file_name, input, context) def self.render(file_name, input)
html = GitHub::Markup.render(file_name, input). html = GitHub::Markup.render(file_name, input).
force_encoding(input.encoding) force_encoding(input.encoding)
html = Banzai.post_process(html, context)
filter = Banzai::Filter::SanitizationFilter.new(html) filter = Banzai::Filter::SanitizationFilter.new(html)
html = filter.call.to_s html = filter.call.to_s
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
class << self class << self
def extension def extension
'Dockerfile' '.Dockerfile'
end end
def categories def categories
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
end end
def base_dir def base_dir
Rails.root.join('vendor/dockerfile') Rails.root.join('vendor/Dockerfile')
end end
def finder(project = nil) def finder(project = nil)
......
...@@ -5,7 +5,7 @@ namespace :gitlab do ...@@ -5,7 +5,7 @@ namespace :gitlab do
end end
def update(template) def update(template)
sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1] sub_dir = template.repo_url.match(/([A-Za-z-]+)\.git\z/)[1]
dir = File.join(vendor_directory, sub_dir) dir = File.join(vendor_directory, sub_dir)
unless clone_repository(template.repo_url, dir) unless clone_repository(template.repo_url, dir)
...@@ -45,7 +45,11 @@ namespace :gitlab do ...@@ -45,7 +45,11 @@ namespace :gitlab do
Template.new( Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git", "https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
/(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/ /(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/
) ),
Template.new(
"https://gitlab.com/gitlab-org/Dockerfile.git",
/(\.{1,2}|LICENSE|CONTRIBUTING.md|\.Dockerfile)\z/
),
].freeze ].freeze
def vendor_directory def vendor_directory
......
require 'spec_helper'
describe Projects::WikisController do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
describe 'POST #preview_markdown' do
it 'renders json in a correct format' do
sign_in(user)
post :preview_markdown, namespace_id: project.namespace, project_id: project, id: 'page/path', text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
end
end
...@@ -420,6 +420,7 @@ describe ProjectsController do ...@@ -420,6 +420,7 @@ describe ProjectsController do
end end
end end
<<<<<<< HEAD
describe 'GET edit' do describe 'GET edit' do
it 'does not allow an auditor user to access the page' do it 'does not allow an auditor user to access the page' do
sign_in(create(:user, :auditor)) sign_in(create(:user, :auditor))
...@@ -439,6 +440,15 @@ describe ProjectsController do ...@@ -439,6 +440,15 @@ describe ProjectsController do
id: project.path id: project.path
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
=======
describe 'POST #preview_markdown' do
it 'renders json in a correct format' do
sign_in(user)
post :preview_markdown, namespace_id: public_project.namespace, id: public_project, text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
>>>>>>> ce/master
end end
end end
end end
...@@ -521,4 +521,16 @@ describe SnippetsController do ...@@ -521,4 +521,16 @@ describe SnippetsController do
end end
end end
end end
describe 'POST #preview_markdown' do
let(:snippet) { create(:personal_snippet, :public) }
it 'renders json in a correct format' do
sign_in(user)
post :preview_markdown, id: snippet, text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
end
end end
FactoryGirl.define do FactoryGirl.define do
factory :service do factory :service do
project factory: :empty_project project factory: :empty_project
type 'Service'
end
factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
project factory: :empty_project
type 'CustomIssueTrackerService'
category 'issue_tracker'
active true
properties(
project_url: 'https://project.url.com',
issues_url: 'https://issues.url.com',
new_issue_url: 'https://newissue.url.com'
)
end end
factory :kubernetes_service do factory :kubernetes_service do
......
require 'rails_helper'
feature 'Admin cohorts page', feature: true do
before do
login_as :admin
end
scenario 'See users count per month' do
2.times { create(:user) }
visit admin_cohorts_path
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
end
end
require 'spec_helper' require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do describe 'Copy as GFM', feature: true, js: true do
include GitlabMarkdownHelper include MarkupHelper
include RepoHelpers include RepoHelpers
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
......
...@@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) } let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment