Commit 18455d74 authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge branch 'master' into balsalmiq-support

parents cc248f3e 4d4aefc5
...@@ -11,6 +11,7 @@ variables: ...@@ -11,6 +11,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
......
...@@ -2,6 +2,21 @@ ...@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.1.1 (2017-04-26)
- Add a transaction around move_issues_to_ghost_user. !10465
- Properly expire cache for all MRs of a pipeline. !10770
- Add sub-nav for Project Integration Services edit page. !10813
- Fix missing duration for blocked pipelines. !10856
- Fix lastest commit status text on main project page. !10863
- Add index on ci_builds.updated_at. !10870 (blackst0ne)
- Fix 500 error due to trying to show issues from pending deleting projects. !10906
- Ensures that OAuth/LDAP/SAML users don't need to be confirmed.
- Ensure replying to an individual note by email creates a note with its own discussion ID.
- Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled.
- Fix usage ping docs link from empty cohorts page.
- Eliminate N+1 queries in loading namespaces for every issuable in milestones.
## 9.1.0 (2017-04-22) ## 9.1.0 (2017-04-22)
- Added merge requests empty state. !7342 - Added merge requests empty state. !7342
......
...@@ -17,6 +17,8 @@ gem 'pg', '~> 0.18.2', group: :postgres ...@@ -17,6 +17,8 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1' gem 'rugged', '~> 0.25.1.1'
gem 'faraday', '~> 0.11.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
...@@ -142,7 +144,7 @@ gem 'after_commit_queue', '~> 1.3.0' ...@@ -142,7 +144,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'
...@@ -186,7 +188,7 @@ gem 'gemnasium-gitlab-service', '~> 0.2' ...@@ -186,7 +188,7 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
gem 'slack-notifier', '~> 1.5.1' gem 'slack-notifier', '~> 1.5.1'
# Asana integration # Asana integration
gem 'asana', '~> 0.4.0' gem 'asana', '~> 0.6.0'
# FogBugz integration # FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1' gem 'ruby-fogbugz', '~> 0.2.1'
...@@ -291,6 +293,7 @@ group :development, :test do ...@@ -291,6 +293,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'
...@@ -345,7 +348,7 @@ gem 'html2text' ...@@ -345,7 +348,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2' gem 'ruby-prof', '~> 0.16.2'
# OAuth # OAuth
gem 'oauth2', '~> 1.2.0' gem 'oauth2', '~> 1.3.0'
# Soft deletion # Soft deletion
gem 'paranoia', '~> 2.2' gem 'paranoia', '~> 2.2'
......
...@@ -47,7 +47,7 @@ GEM ...@@ -47,7 +47,7 @@ GEM
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.5) allocations (1.0.5)
arel (6.0.4) arel (6.0.4)
asana (0.4.0) asana (0.6.0)
faraday (~> 0.9) faraday (~> 0.9)
faraday_middleware (~> 0.9) faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0) faraday_middleware-multi_json (~> 0.0)
...@@ -193,10 +193,10 @@ GEM ...@@ -193,10 +193,10 @@ GEM
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0) factory_girl (~> 4.7.0)
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.9.2) faraday (0.11.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (0.10.0) faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 0.10) faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6) faraday_middleware-multi_json (0.0.6)
faraday_middleware faraday_middleware
multi_json multi_json
...@@ -429,7 +429,7 @@ GEM ...@@ -429,7 +429,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)
...@@ -454,15 +454,15 @@ GEM ...@@ -454,15 +454,15 @@ GEM
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
numerizer (0.1.1) numerizer (0.1.1)
oauth (0.5.1) oauth (0.5.1)
oauth2 (1.2.0) oauth2 (1.3.1)
faraday (>= 0.8, < 0.10) faraday (>= 0.8, < 0.12)
jwt (~> 1.0) jwt (~> 1.0)
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.6.2) octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4) oj (2.17.5)
omniauth (1.4.2) omniauth (1.4.2)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
...@@ -603,7 +603,7 @@ GEM ...@@ -603,7 +603,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)
...@@ -659,6 +659,7 @@ GEM ...@@ -659,6 +659,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
...@@ -716,11 +717,11 @@ GEM ...@@ -716,11 +717,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)
...@@ -853,7 +854,7 @@ DEPENDENCIES ...@@ -853,7 +854,7 @@ DEPENDENCIES
after_commit_queue (~> 1.3.0) after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
allocations (~> 1.0) allocations (~> 1.0)
asana (~> 0.4.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7) asciidoctor-plantuml (= 0.0.7)
attr_encrypted (~> 3.0.0) attr_encrypted (~> 3.0.0)
...@@ -891,6 +892,7 @@ DEPENDENCIES ...@@ -891,6 +892,7 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
faraday (~> 0.11.0)
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.8.0) flay (~> 2.8.0)
fog-aws (~> 0.9) fog-aws (~> 0.9)
...@@ -943,7 +945,7 @@ DEPENDENCIES ...@@ -943,7 +945,7 @@ DEPENDENCIES
mysql2 (~> 0.3.16) mysql2 (~> 0.3.16)
net-ssh (~> 3.0.1) net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2) nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0) oauth2 (~> 1.3.0)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.4.2) omniauth (~> 1.4.2)
...@@ -988,6 +990,7 @@ DEPENDENCIES ...@@ -988,6 +990,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)
...@@ -1004,7 +1007,7 @@ DEPENDENCIES ...@@ -1004,7 +1007,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)
......
...@@ -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,
...@@ -140,76 +141,90 @@ export default Vue.component('environment-component', { ...@@ -140,76 +141,90 @@ 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"> </a>
Stopped </li>
<span class="badge js-stopped-environments-count"> <li :class="{ active : scope === 'stopped' }">
{{state.stoppedCounter}} <a :href="projectStoppedEnvironmentsPath">
</span> Stopped
</a> <span class="badge js-stopped-environments-count">
</li> {{state.stoppedCounter}}
</ul> </span>
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-create">
New environment
</a> </a>
</div> </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="environments-list-loading text-center"
v-if="isLoading">
<div class="content-list environments-container"> <i
<div class="environments-list-loading text-center" v-if="isLoading"> class="fa fa-spinner fa-spin"
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i> aria-hidden="true" />
</div> </div>
<div class="blank-state blank-state-no-icon" <div
v-if="!isLoading && state.environments.length === 0"> class="blank-state blank-state-no-icon"
<h2 class="blank-state-title js-blank-state-title"> v-if="!isLoading && state.environments.length === 0">
You don't have any environments right now. <h2 class="blank-state-title js-blank-state-title">
</h2> You don't have any environments right now.
<p class="blank-state-text"> </h2>
Environments are places where code gets deployed, such as staging or production. <p class="blank-state-text">
<br /> Environments are places where code gets deployed, such as staging or production.
<a :href="helpPagePath"> <br />
Read more about environments <a :href="helpPagePath">
</a> Read more about environments
</p>
<a v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
</a> </a>
</div> </p>
<div class="table-holder" <a
v-if="!isLoading && state.environments.length > 0"> v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
<environment-table class="btn btn-create js-new-environment-button">
:environments="state.environments" New Environment
:can-create-deployment="canCreateDeploymentParsed" </a>
:can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</div> </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>
<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,
...@@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', { ...@@ -116,54 +116,66 @@ 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">
<environment-table <environment-table
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:service="service"/> :service="service"/>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" <table-pagination
:change="changePage" v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:pageInfo="state.paginationInformation"/> :change="changePage"
</div> :pageInfo="state.paginationInformation"/>
</div> </div>
</div> </div>
`, </div>
}); </template>
...@@ -77,13 +77,14 @@ class FilteredSearchManager { ...@@ -77,13 +77,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);
...@@ -96,12 +97,13 @@ class FilteredSearchManager { ...@@ -96,12 +97,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);
} }
...@@ -117,12 +119,13 @@ class FilteredSearchManager { ...@@ -117,12 +119,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);
} }
...@@ -195,14 +198,28 @@ class FilteredSearchManager { ...@@ -195,14 +198,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);
...@@ -248,16 +265,21 @@ class FilteredSearchManager { ...@@ -248,16 +265,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;
} }
} }
......
...@@ -614,6 +614,7 @@ pre.light-well { ...@@ -614,6 +614,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
...@@ -91,21 +93,13 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -91,21 +93,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
...@@ -115,7 +109,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -115,7 +109,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]
...@@ -216,20 +217,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -216,20 +217,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)
...@@ -252,6 +239,10 @@ class ProjectsController < Projects::ApplicationController ...@@ -252,6 +239,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
......
...@@ -196,38 +196,6 @@ module ApplicationHelper ...@@ -196,38 +196,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,12 +160,17 @@ module ProjectsHelper ...@@ -160,12 +160,17 @@ 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
end end
def load_pipeline_status(projects)
Gitlab::Cache::Ci::ProjectPipelineStatus.
load_in_batch_for_projects(projects)
end
private private
def repo_children_classes(field) def repo_children_classes(field)
......
...@@ -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?
......
...@@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -28,6 +28,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 }
...@@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -159,6 +161,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
...@@ -344,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -344,6 +347,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
......
...@@ -120,7 +120,9 @@ module CacheMarkdownField ...@@ -120,7 +120,9 @@ module CacheMarkdownField
attrs attrs
end end
before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache? # Using before_update here conflicts with elasticsearch-model somehow
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end end
class_methods do class_methods do
......
...@@ -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
......
...@@ -74,6 +74,7 @@ class Project < ActiveRecord::Base ...@@ -74,6 +74,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace attr_accessor :old_path_with_namespace
attr_writer :pipeline_status
alias_attribute :title, :name alias_attribute :title, :name
...@@ -1181,6 +1182,7 @@ class Project < ActiveRecord::Base ...@@ -1181,6 +1182,7 @@ class Project < ActiveRecord::Base
end end
end end
# Lazy loading of the `pipeline_status` attribute
def pipeline_status def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self) @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end end
......
...@@ -17,9 +17,9 @@ class Repository ...@@ -17,9 +17,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
...@@ -28,7 +28,7 @@ class Repository ...@@ -28,7 +28,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,
...@@ -527,7 +527,11 @@ class Repository ...@@ -527,7 +527,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)
......
...@@ -2,20 +2,13 @@ class ProjectPolicy < BasePolicy ...@@ -2,20 +2,13 @@ class ProjectPolicy < BasePolicy
def rules def rules
team_access!(user) team_access!(user)
owner = project.owner == user || owner_access! if user.admin? || owner?
(project.group && project.group.has_owner?(user)) team_member_owner_access! if owner?
owner_access! if user.admin? || owner
team_member_owner_access! if owner
if project.public? || (project.internal? && !user.external?) if project.public? || (project.internal? && !user.external?)
guest_access! guest_access!
public_access! public_access!
can! :request_access if access_requestable?
if project.request_access_enabled &&
!(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
end end
archived_access! if project.archived? archived_access! if project.archived?
...@@ -27,6 +20,13 @@ class ProjectPolicy < BasePolicy ...@@ -27,6 +20,13 @@ class ProjectPolicy < BasePolicy
@subject @subject
end end
def owner?
return @owner if defined?(@owner)
@owner = project.owner == user ||
(project.group && project.group.has_owner?(user))
end
def guest_access! def guest_access!
can! :read_project can! :read_project
can! :read_board can! :read_board
...@@ -226,14 +226,6 @@ class ProjectPolicy < BasePolicy ...@@ -226,14 +226,6 @@ class ProjectPolicy < BasePolicy
disabled_features! disabled_features!
end end
def project_group_member?(user)
project.group &&
(
project.group.members_with_parents.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end
def block_issues_abilities def block_issues_abilities
unless project.feature_available?(:issues, user) unless project.feature_available?(:issues, user)
cannot! :read_issue if project.default_issues_tracker? cannot! :read_issue if project.default_issues_tracker?
...@@ -254,6 +246,22 @@ class ProjectPolicy < BasePolicy ...@@ -254,6 +246,22 @@ class ProjectPolicy < BasePolicy
private private
def project_group_member?(user)
project.group &&
(
project.group.members_with_parents.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end
def access_requestable?
project.request_access_enabled &&
!owner? &&
!user.admin? &&
!project.team.member?(user) &&
!project_group_member?(user)
end
# A base set of abilities for read-only users, which # A base set of abilities for read-only users, which
# is then augmented as necessary for anonymous and other # is then augmented as necessary for anonymous and other
# read-only users. # read-only users.
......
...@@ -97,7 +97,8 @@ module Projects ...@@ -97,7 +97,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
@project.group&.refresh_members_authorized_projects @project.group&.refresh_members_authorized_projects
......
...@@ -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>
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true - remote = false unless local_assigns[:remote] == true
- load_pipeline_status(projects)
.js-projects-list-holder .js-projects-list-holder
- if projects.any? - if projects.any?
......
...@@ -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 issues/:iid/closed_by api endpoint
merge_request:
author: mhasbini
---
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: Fetch pipeline status in batch from redis
merge_request: 10785
author:
---
title: Allow admins to sudo to blocked users via the API
merge_request: 10842
author:
---
title: Add a transaction around move_issues_to_ghost_user
merge_request: 10465
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: Add sub-nav for Project Integration Services edit page
merge_request: 10813
author:
---
title: Fix 500 error due to trying to show issues from pending deleting projects
merge_request: 10906
author:
---
title: Add username to activity atom feed
merge_request: 10802
author: winniehell
---
title: Add index on ci_builds.updated_at
merge_request: 10870
author: blackst0ne
---
title: Ensure replying to an individual note by email creates a note with its own
discussion ID
merge_request:
author:
---
title: Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled
merge_request:
author:
--- ---
title: Fix usage ping docs link from empty cohorts page title: Bump Sidekiq to 5.0.0
merge_request: merge_request:
author: author:
---
title: Fix missing duration for blocked pipelines
merge_request: 10856
author:
---
title: Fix lastest commit status text on main project page
merge_request: 10863
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: Eliminate N+1 queries in loading namespaces for every issuable in milestones
merge_request:
author:
---
title: Ensure namespace owner is Master of project upon creation
merge_request: 10910
author:
---
title: Properly expire cache for all MRs of a pipeline
merge_request: 10770
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 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.
......
...@@ -823,6 +823,67 @@ Example response: ...@@ -823,6 +823,67 @@ Example response:
} }
``` ```
## List merge requests that will close issue on merge
Get all the merge requests that will close issue when merged.
```
GET /projects/:id/issues/:issue_iid/closed_by
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project issue |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/11/closed_by
```
Example response:
```json
[
{
"id": 6471,
"iid": 6432,
"project_id": 1,
"title": "add a test for cgi lexer options",
"description": "closes #11",
"state": "opened",
"created_at": "2017-04-06T18:33:34.168Z",
"updated_at": "2017-04-09T20:10:24.983Z",
"target_branch": "master",
"source_branch": "feature.custom-highlighting",
"upvotes": 0,
"downvotes": 0,
"author": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root"
},
"assignee": null,
"source_project_id": 1,
"target_project_id": 1,
"labels": [],
"work_in_progress": false,
"milestone": null,
"merge_when_pipeline_succeeds": false,
"merge_status": "unchecked",
"sha": "5a62481d563af92b8e32d735f2fa63b94e806835",
"merge_commit_sha": null,
"user_notes_count": 1,
"should_remove_source_branch": null,
"force_remove_source_branch": false,
"web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/6432"
}
]
```
## Comments on issues ## Comments on issues
Comments are done via the [notes](notes.md) resource. Comments are done via the [notes](notes.md) resource.
...@@ -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
......
## Migrate GitLab CI to GitLab CE or EE # Migrate GitLab CI to GitLab CE or EE
Beginning with version 8.0 of GitLab Community Edition (CE) and Enterprise Beginning with version 8.0 of GitLab Community Edition (CE) and Enterprise
Edition (EE), GitLab CI is no longer its own application, but is instead built Edition (EE), GitLab CI is no longer its own application, but is instead built
...@@ -12,7 +12,7 @@ is not possible.** ...@@ -12,7 +12,7 @@ is not possible.**
We recommend that you read through the entire migration process in this We recommend that you read through the entire migration process in this
document before beginning. document before beginning.
### Overview ## Overview
In this document we assume you have a GitLab server and a GitLab CI server. It In this document we assume you have a GitLab server and a GitLab CI server. It
does not matter if these are the same machine. does not matter if these are the same machine.
...@@ -26,7 +26,7 @@ can be online for most of the procedure; the only GitLab downtime (if any) is ...@@ -26,7 +26,7 @@ can be online for most of the procedure; the only GitLab downtime (if any) is
during the upgrade to 8.0. Your CI service will be offline from the moment you during the upgrade to 8.0. Your CI service will be offline from the moment you
upgrade to 8.0 until you finish the migration procedure. upgrade to 8.0 until you finish the migration procedure.
### Before upgrading ## Before upgrading
If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**: If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**:
...@@ -38,12 +38,12 @@ run `sudo gitlab-ctl reconfigure` and you can reach CI at `gitlab.example.com/ci ...@@ -38,12 +38,12 @@ run `sudo gitlab-ctl reconfigure` and you can reach CI at `gitlab.example.com/ci
If you want to migrate your existing data, continue reading. If you want to migrate your existing data, continue reading.
#### 0. Updating Omnibus from versions prior to 7.13 ### 0. Updating Omnibus from versions prior to 7.13
If you are updating from older versions you should first update to 7.14 and then to 8.0. If you are updating from older versions you should first update to 7.14 and then to 8.0.
Otherwise it's pretty likely that you will encounter problems described in the [Troubleshooting](#troubleshooting). Otherwise it's pretty likely that you will encounter problems described in the [Troubleshooting](#troubleshooting).
#### 1. Verify that backups work ### 1. Verify that backups work
Make sure that the backup script on both servers can connect to the database. Make sure that the backup script on both servers can connect to the database.
...@@ -73,7 +73,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=r ...@@ -73,7 +73,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=r
If this fails you need to fix it before upgrading to 8.0. Also see If this fails you need to fix it before upgrading to 8.0. Also see
https://about.gitlab.com/getting-help/ https://about.gitlab.com/getting-help/
#### 2. Check source and target database types ### 2. Check source and target database types
Check what databases you use on your GitLab server and your CI server. Check what databases you use on your GitLab server and your CI server.
Look for the 'adapter:' line. If your CI server and your GitLab server use Look for the 'adapter:' line. If your CI server and your GitLab server use
...@@ -102,7 +102,7 @@ cd /home/git/gitlab ...@@ -102,7 +102,7 @@ cd /home/git/gitlab
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
``` ```
#### 3. Storage planning ### 3. Storage planning
Decide where to store CI build traces on GitLab server. GitLab CI uses Decide where to store CI build traces on GitLab server. GitLab CI uses
files on disk to store CI build traces. The default path for these build files on disk to store CI build traces. The default path for these build
...@@ -111,34 +111,34 @@ traces is `/var/opt/gitlab/gitlab-ci/builds` (Omnibus) or ...@@ -111,34 +111,34 @@ traces is `/var/opt/gitlab/gitlab-ci/builds` (Omnibus) or
a special location, or if you are using NFS, you should make sure that you a special location, or if you are using NFS, you should make sure that you
store build traces on the same storage as your Git repositories. store build traces on the same storage as your Git repositories.
### I. Upgrading ## I. Upgrading
From this point on, GitLab CI will be unavailable for your end users. From this point on, GitLab CI will be unavailable for your end users.
#### 1. Upgrade GitLab to 8.0 ### 1. Upgrade GitLab to 8.0
First upgrade your GitLab server to version 8.0: First upgrade your GitLab server to version 8.0:
https://about.gitlab.com/update/ https://about.gitlab.com/update/
#### 2. Disable CI on the GitLab server during the migration ### 2. Disable CI on the GitLab server during the migration
After you update, go to the admin panel and temporarily disable CI. As After you update, go to the admin panel and temporarily disable CI. As
an administrator, go to **Admin Area** -> **Settings**, and under an administrator, go to **Admin Area** -> **Settings**, and under
**Continuous Integration** uncheck **Disable to prevent CI usage until rake **Continuous Integration** uncheck **Disable to prevent CI usage until rake
ci:migrate is run (8.0 only)**. ci:migrate is run (8.0 only)**.
#### 3. CI settings are now in GitLab ### 3. CI settings are now in GitLab
If you want to use custom CI settings (e.g. change where builds are If you want to use custom CI settings (e.g. change where builds are
stored), please update `/etc/gitlab/gitlab.rb` (Omnibus) or stored), please update `/etc/gitlab/gitlab.rb` (Omnibus) or
`/home/git/gitlab/config/gitlab.yml` (Source). `/home/git/gitlab/config/gitlab.yml` (Source).
#### 4. Upgrade GitLab CI to 8.0 ### 4. Upgrade GitLab CI to 8.0
Now upgrade GitLab CI to version 8.0. If you are using Omnibus packages, Now upgrade GitLab CI to version 8.0. If you are using Omnibus packages,
this may have already happened when you upgraded GitLab to 8.0. this may have already happened when you upgraded GitLab to 8.0.
#### 5. Disable GitLab CI on the CI server ### 5. Disable GitLab CI on the CI server
Disable GitLab CI after upgrading to 8.0. Disable GitLab CI after upgrading to 8.0.
...@@ -154,9 +154,9 @@ cd /home/gitlab_ci/gitlab-ci ...@@ -154,9 +154,9 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production
``` ```
### II. Moving data ## II. Moving data
#### 1. Database encryption key ### 1. Database encryption key
Move the database encryption key from your CI server to your GitLab Move the database encryption key from your CI server to your GitLab
server. The command below will show you what you need to copy-paste to your server. The command below will show you what you need to copy-paste to your
...@@ -174,7 +174,7 @@ cd /home/gitlab_ci/gitlab-ci ...@@ -174,7 +174,7 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rake backup:show_secrets RAILS_ENV=production sudo -u gitlab_ci -H bundle exec rake backup:show_secrets RAILS_ENV=production
``` ```
#### 2. SQL data and build traces ### 2. SQL data and build traces
Create your final CI data export. If you are converting from MySQL to Create your final CI data export. If you are converting from MySQL to
PostgreSQL, add ` MYSQL_TO_POSTGRESQL=1` to the end of the rake command. When PostgreSQL, add ` MYSQL_TO_POSTGRESQL=1` to the end of the rake command. When
...@@ -192,7 +192,7 @@ cd /home/gitlab_ci/gitlab-ci ...@@ -192,7 +192,7 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production
``` ```
#### 3. Copy data to the GitLab server ### 3. Copy data to the GitLab server
If you were running GitLab and GitLab CI on the same server you can skip this If you were running GitLab and GitLab CI on the same server you can skip this
step. step.
...@@ -209,7 +209,7 @@ ssh -A ci_admin@ci_server.example ...@@ -209,7 +209,7 @@ ssh -A ci_admin@ci_server.example
scp /path/to/12345_gitlab_ci_backup.tar gitlab_admin@gitlab_server.example:~ scp /path/to/12345_gitlab_ci_backup.tar gitlab_admin@gitlab_server.example:~
``` ```
#### 4. Move data to the GitLab backups folder ### 4. Move data to the GitLab backups folder
Make the CI data archive discoverable for GitLab. We assume below that you Make the CI data archive discoverable for GitLab. We assume below that you
store backups in the default path, adjust the command if necessary. store backups in the default path, adjust the command if necessary.
...@@ -223,7 +223,7 @@ sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/ ...@@ -223,7 +223,7 @@ sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/
sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/ sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/
``` ```
#### 5. Import the CI data into GitLab. ### 5. Import the CI data into GitLab.
This step will delete any existing CI data on your GitLab server. There should This step will delete any existing CI data on your GitLab server. There should
be no CI data yet because you turned CI on the GitLab server off earlier. be no CI data yet because you turned CI on the GitLab server off earlier.
...@@ -239,7 +239,7 @@ cd /home/git/gitlab ...@@ -239,7 +239,7 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake ci:migrate RAILS_ENV=production sudo -u git -H bundle exec rake ci:migrate RAILS_ENV=production
``` ```
#### 6. Restart GitLab ### 6. Restart GitLab
``` ```
# On your GitLab server: # On your GitLab server:
...@@ -251,7 +251,7 @@ sudo gitlab-ctl restart sidekiq ...@@ -251,7 +251,7 @@ sudo gitlab-ctl restart sidekiq
sudo service gitlab reload sudo service gitlab reload
``` ```
### III. Redirecting traffic ## III. Redirecting traffic
If you were running GitLab CI with Omnibus packages and you were using the If you were running GitLab CI with Omnibus packages and you were using the
internal NGINX configuration your CI service should now be available both at internal NGINX configuration your CI service should now be available both at
...@@ -261,7 +261,7 @@ If you installed GitLab CI from source we now need to configure a redirect in ...@@ -261,7 +261,7 @@ If you installed GitLab CI from source we now need to configure a redirect in
NGINX so that existing CI runners can keep using the old CI server address, and NGINX so that existing CI runners can keep using the old CI server address, and
so that existing links to your CI server keep working. so that existing links to your CI server keep working.
#### 1. Update Nginx configuration ### 1. Update Nginx configuration
To ensure that your existing CI runners are able to communicate with the To ensure that your existing CI runners are able to communicate with the
migrated installation, and that existing build triggers still work, you'll need migrated installation, and that existing build triggers still work, you'll need
...@@ -317,22 +317,22 @@ You should also make sure that you can: ...@@ -317,22 +317,22 @@ You should also make sure that you can:
1. `curl https://YOUR_GITLAB_SERVER_FQDN/` from your previous GitLab CI server. 1. `curl https://YOUR_GITLAB_SERVER_FQDN/` from your previous GitLab CI server.
1. `curl https://YOUR_CI_SERVER_FQDN/` from your GitLab CE (or EE) server. 1. `curl https://YOUR_CI_SERVER_FQDN/` from your GitLab CE (or EE) server.
#### 2. Check Nginx configuration ### 2. Check Nginx configuration
sudo nginx -t sudo nginx -t
#### 3. Restart Nginx ### 3. Restart Nginx
sudo /etc/init.d/nginx restart sudo /etc/init.d/nginx restart
#### Restore from backup ### Restore from backup
If something went wrong and you need to restore a backup, consult the [Backup If something went wrong and you need to restore a backup, consult the [Backup
restoration](../raketasks/backup_restore.md) guide. restoration](../raketasks/backup_restore.md) guide.
### Troubleshooting ## Troubleshooting
#### show:secrets problem (Omnibus-only) ### show:secrets problem (Omnibus-only)
If you see errors like this: If you see errors like this:
``` ```
Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml` Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml`
...@@ -343,7 +343,7 @@ Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml ...@@ -343,7 +343,7 @@ Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml
This can happen if you are updating from versions prior to 7.13 straight to 8.0. This can happen if you are updating from versions prior to 7.13 straight to 8.0.
The fix for this is to update to Omnibus 7.14 first and then update it to 8.0. The fix for this is to update to Omnibus 7.14 first and then update it to 8.0.
#### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds ### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds
To fix that issue you have to change builds/ folder permission before doing final backup: To fix that issue you have to change builds/ folder permission before doing final backup:
``` ```
sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
...@@ -354,7 +354,7 @@ Then before executing `ci:migrate` you need to fix builds folder permission: ...@@ -354,7 +354,7 @@ Then before executing `ci:migrate` you need to fix builds folder permission:
sudo chown git:git /var/opt/gitlab/gitlab-ci/builds sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
``` ```
#### Problems when importing CI database to GitLab ### Problems when importing CI database to GitLab
If you were migrating CI database from MySQL to PostgreSQL manually you can see errors during import about missing sequences: If you were migrating CI database from MySQL to PostgreSQL manually you can see errors during import about missing sequences:
``` ```
ALTER SEQUENCE ALTER SEQUENCE
......
This diff is collapsed.
...@@ -17,6 +17,10 @@ We've gathered some resources to help you to get the best from Git with GitLab. ...@@ -17,6 +17,10 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [Start using Git on the command line](../../gitlab-basics/start-using-git.md) - [Start using Git on the command line](../../gitlab-basics/start-using-git.md)
- [Command Line basic commands](../../gitlab-basics/command-line-commands.md) - [Command Line basic commands](../../gitlab-basics/command-line-commands.md)
- [GitLab Git Cheat Sheet (download)](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) - [GitLab Git Cheat Sheet (download)](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf)
- Commits
- [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit)
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
- **Articles:** - **Articles:**
- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/) - [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/) - [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
......
...@@ -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.
module SharedActiveTab module SharedActiveTab
include Spinach::DSL include Spinach::DSL
include WaitForAjax
after do
wait_for_ajax if javascript_test?
end
def ensure_active_main_tab(content) def ensure_active_main_tab(content)
expect(find('.layout-nav li.active')).to have_content(content) expect(find('.layout-nav li.active')).to have_content(content)
......
...@@ -102,7 +102,7 @@ module API ...@@ -102,7 +102,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!
......
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.
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.
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