summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml12
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock52
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/dispatcher.js4
-rw-r--r--app/assets/javascripts/dropzone_input.js2
-rw-r--r--app/assets/javascripts/extensions/array.js11
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/gl_dropdown.js12
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js10
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js23
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/header.scss4
-rw-r--r--app/assets/stylesheets/new_nav.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss68
-rw-r--r--app/assets/stylesheets/pages/issuable.scss8
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss1
-rw-r--r--app/assets/stylesheets/pages/settings.scss20
-rw-r--r--app/helpers/diff_helper.rb18
-rw-r--r--app/models/notification_recipient.rb125
-rw-r--r--app/models/project_services/jira_service.rb8
-rw-r--r--app/models/repository.rb34
-rw-r--r--app/services/notification_recipient_service.rb471
-rw-r--r--app/services/notification_service.rb69
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/header/_new.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml2
-rw-r--r--app/views/layouts/nav/_new_admin_sidebar.html.haml2
-rw-r--r--app/views/layouts/nav/_new_group_sidebar.html.haml4
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml10
-rw-r--r--app/views/projects/_merge_request_settings.html.haml7
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml66
-rw-r--r--app/views/projects/edit.html.haml431
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml2
-rw-r--r--app/views/shared/icons/_container_registry.svg1
-rw-r--r--app/views/shared/icons/_spam_logs.svg1
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--changelogs/unreleased/35659-rename-pipeline.yml4
-rw-r--r--changelogs/unreleased/diff-changed-files-dropdown.yml4
-rw-r--r--changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml4
-rw-r--r--config/application.rb4
-rw-r--r--doc/articles/index.md2
-rw-r--r--doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.pngbin0 -> 37386 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/add-to-project.pngbin0 -> 21672 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/create-project-ui.pngbin0 -> 22290 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/gitlab-logs.pngbin0 -> 70858 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/gitlab-overview.pngbin0 -> 106432 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/gitlab-running.pngbin0 -> 107993 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/gitlab-scale.pngbin0 -> 36628 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/gitlab-settings.pngbin0 -> 111366 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/no-resources.pngbin0 -> 34669 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/openshift-infra-project.pngbin0 -> 95725 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/pods-overview.pngbin0 -> 106861 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/rc-name.pngbin0 -> 51390 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/running-pods.pngbin0 -> 29818 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/storage-volumes.pngbin0 -> 49584 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/img/web-console.pngbin0 -> 34774 bytes
-rw-r--r--doc/articles/openshift_and_gitlab/index.md510
-rw-r--r--doc/development/fe_guide/style_guide_js.md54
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/update/8.17-to-9.0.md5
-rw-r--r--doc/update/9.0-to-9.1.md5
-rw-r--r--doc/update/9.1-to-9.2.md5
-rw-r--r--doc/update/9.2-to-9.3.md5
-rw-r--r--doc/update/9.3-to-9.4.md5
-rw-r--r--features/steps/project/project.rb16
-rw-r--r--features/steps/shared/project.rb2
-rw-r--r--lib/banzai/renderer.rb2
-rw-r--r--lib/declarative_policy.rb14
-rw-r--r--lib/gitlab/git/blame.rb24
-rw-r--r--lib/gitlab/git/repository.rb21
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb25
-rw-r--r--spec/features/projects/features_visibility_spec.rb32
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb19
-rw-r--r--spec/features/projects/project_settings_spec.rb8
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb13
-rw-r--r--spec/javascripts/abuse_reports_spec.js6
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js1
-rw-r--r--spec/javascripts/extensions/array_spec.js22
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js1
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js1
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js1
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js1
-rw-r--r--spec/javascripts/lib/utils/sticky_spec.js52
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb102
-rw-r--r--spec/models/project_services/jira_service_spec.rb9
-rw-r--r--spec/models/repository_spec.rb40
-rw-r--r--spec/requests/api/merge_requests_spec.rb20
-rw-r--r--spec/services/notification_recipient_service_spec.rb34
-rw-r--r--spec/services/notification_service_spec.rb5
-rw-r--r--spec/support/api/schema_matcher.rb18
-rw-r--r--spec/support/login_helpers.rb2
-rw-r--r--spec/support/test_env.rb1
105 files changed, 1816 insertions, 833 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 27fdf6ca0b5..f55bdc19eec 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -66,16 +66,18 @@ stages:
- mysql:latest
- redis:alpine
-.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
+.only-if-want-mysql: &only-if-want-mysql
only:
- /mysql/
- /-stable/
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
- tags@gitlab-org/gitlab-ce
+ - tags@gitlab-org/gitlab-ee
- tags@gitlab/gitlabhq
- - //@gitlab-org/gitlab-ee
- - //@gitlab/gitlab-ee
+ - tags@gitlab/gitlab-ee
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
@@ -114,7 +116,7 @@ stages:
.rspec-knapsack-mysql: &rspec-knapsack-mysql
<<: *rspec-knapsack
<<: *use-mysql
- <<: *only-master-and-ee-or-mysql
+ <<: *only-if-want-mysql
<<: *except-docs
.spinach-knapsack: &spinach-knapsack
@@ -146,7 +148,7 @@ stages:
.spinach-knapsack-mysql: &spinach-knapsack-mysql
<<: *spinach-knapsack
<<: *use-mysql
- <<: *only-master-and-ee-or-mysql
+ <<: *only-if-want-mysql
<<: *except-docs
.only-canonical-masters: &only-canonical-masters
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 4e8f395fa5e..1b58cc10180 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.26.0
+0.27.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 8a30e8f94a3..42cdd0b540f 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.4.0
+5.7.0
diff --git a/Gemfile b/Gemfile
index 403b104a9d6..09934bf5654 100644
--- a/Gemfile
+++ b/Gemfile
@@ -314,11 +314,11 @@ group :development, :test do
gem 'pry-rails', '~> 0.3.4'
gem 'awesome_print', '~> 1.2.0', require: false
- gem 'fuubar', '~> 2.0.0'
+ gem 'fuubar', '~> 2.2.0'
gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.7.0'
- gem 'rspec-rails', '~> 3.5.0'
+ gem 'rspec-rails', '~> 3.6.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
@@ -391,7 +391,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
-gem 'gitaly', '~> 0.23.0'
+gem 'gitaly', '~> 0.24.0'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 9f90965a567..57cfe0a9de0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -156,7 +156,7 @@ GEM
devise (~> 4.0)
railties
rotp (~> 2.0)
- diff-lcs (1.2.5)
+ diff-lcs (1.3)
diffy (3.1.0)
docile (1.1.5)
domain_name (0.5.20161021)
@@ -250,8 +250,8 @@ GEM
foreman (0.78.0)
thor (~> 0.19.1)
formatador (0.2.5)
- fuubar (2.0.0)
- rspec (~> 3.0)
+ fuubar (2.2.0)
+ rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
@@ -269,7 +269,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.23.0)
+ gitaly (0.24.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -393,7 +393,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.2)
- i18n (0.8.1)
+ i18n (0.8.6)
ice_nine (0.11.2)
influxdb (0.2.3)
cause
@@ -610,7 +610,7 @@ GEM
pry-rails (0.3.5)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
- rack (1.6.5)
+ rack (1.6.8)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -658,7 +658,7 @@ GEM
rainbow (2.2.2)
rake
raindrops (0.18.0)
- rake (10.5.0)
+ rake (12.0.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2)
@@ -702,30 +702,26 @@ GEM
chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
- rspec (3.5.0)
- rspec-core (~> 3.5.0)
- rspec-expectations (~> 3.5.0)
- rspec-mocks (~> 3.5.0)
- rspec-core (3.5.0)
- rspec-support (~> 3.5.0)
- rspec-expectations (3.5.0)
+ rspec-core (3.6.0)
+ rspec-support (~> 3.6.0)
+ rspec-expectations (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.5.0)
- rspec-mocks (3.5.0)
+ rspec-support (~> 3.6.0)
+ rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.5.0)
- rspec-rails (3.5.0)
+ rspec-support (~> 3.6.0)
+ rspec-rails (3.6.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec-core (~> 3.5.0)
- rspec-expectations (~> 3.5.0)
- rspec-mocks (~> 3.5.0)
- rspec-support (~> 3.5.0)
+ rspec-core (~> 3.6.0)
+ rspec-expectations (~> 3.6.0)
+ rspec-mocks (~> 3.6.0)
+ rspec-support (~> 3.6.0)
rspec-retry (0.4.5)
rspec-core
rspec-set (0.1.3)
- rspec-support (3.5.0)
+ rspec-support (3.6.0)
rspec_profiling (0.0.5)
activerecord
pg
@@ -860,7 +856,7 @@ GEM
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
- tzinfo (1.2.2)
+ tzinfo (1.2.3)
thread_safe (~> 0.1)
u2f (0.2.1)
uglifier (2.7.2)
@@ -972,13 +968,13 @@ DEPENDENCIES
fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7)
foreman (~> 0.78.0)
- fuubar (~> 2.0.0)
+ fuubar (~> 2.2.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly (~> 0.23.0)
+ gitaly (~> 0.24.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
@@ -1076,7 +1072,7 @@ DEPENDENCIES
responders (~> 2.0)
rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
- rspec-rails (~> 3.5.0)
+ rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
@@ -1130,4 +1126,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.15.1
+ 1.15.3
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
index 38a8317dbd7..8f5e2e545ec 100644
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -10,7 +10,7 @@ class AjaxLoadingSpinner {
e.target.setAttribute('disabled', '');
const iconElement = e.target.querySelector('i');
// get first fa- icon
- const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first();
+ const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0];
iconElement.dataset.icon = originalIcon;
AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
$(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 1d36519c75c..96af69e7a36 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -1,8 +1,8 @@
/* global ListIssue */
import Vue from 'vue';
-import queryData from '../../utils/query_data';
-import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import queryData from '~/boards/utils/query_data';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header';
import './list';
import './footer';
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 44791a93936..6583e471a48 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -92,7 +92,7 @@ $(() => {
});
},
selectDefaultStage() {
- const stage = this.state.stages.first();
+ const stage = this.state.stages[0];
this.selectStage(stage);
},
selectStage(stage) {
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 070d0a01c0f..5630940f5bb 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -348,6 +348,8 @@ import GpgBadges from './gpg_badges';
break;
case 'projects:edit':
setupProjectEdit();
+ // Initialize expandable settings panels
+ initSettingsPanels();
break;
case 'projects:imports:show':
new ProjectImport();
@@ -504,7 +506,7 @@ import GpgBadges from './gpg_badges';
new gl.DueDateSelectors();
break;
}
- switch (path.first()) {
+ switch (path[0]) {
case 'sessions':
case 'omniauth_callbacks':
if (!gon.u2f) break;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index bd1864b8321..6d19a6d9b3a 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -217,7 +217,7 @@ window.DropzoneInput = (function() {
value = e.clipboardData.getData('text/plain');
}
value = value.split("\r");
- return value.first();
+ return value[0];
};
const showSpinner = function(e) {
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
deleted file mode 100644
index 027222f804d..00000000000
--- a/app/assets/javascripts/extensions/array.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// TODO: remove this
-
-// eslint-disable-next-line no-extend-native
-Array.prototype.first = function first() {
- return this[0];
-};
-
-// eslint-disable-next-line no-extend-native
-Array.prototype.last = function last() {
- return this[this.length - 1];
-};
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index db5560fa753..8d711e3213c 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -123,11 +123,11 @@ class DropdownUtils {
if (!allowMultiple && itemInExistingTokens) {
updatedItem.droplab_hidden = true;
- } else if (!lastKey || searchInput.split('').last() === ' ') {
+ } else if (!lastKey || _.last(searchInput.split('')) === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastKey) {
const split = lastKey.split(':');
- const tokenName = split[0].split(' ').last();
+ const tokenName = _.last(split[0].split(' '));
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
updatedItem.droplab_hidden = tokenName ? match : false;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index 47cecd5b5f7..dd1c067df87 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -167,7 +167,7 @@ class FilteredSearchDropdownManager {
// Eg. token = 'label:'
const split = lastToken.split(':');
- const dropdownName = split[0].split(' ').last();
+ const dropdownName = _.last(split[0].split(' '));
this.loadDropdown(split.length > 1 ? dropdownName : '');
} else if (lastToken) {
// Token has been initialized into an object because it has a value
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 3ce8b8607ad..a31be2b0bc7 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -367,7 +367,7 @@ class FilteredSearchManager {
const fragments = searchToken.split(':');
if (fragments.length > 1) {
const inputValues = fragments[0].split(' ');
- const tokenKey = inputValues.last();
+ const tokenKey = _.last(inputValues);
if (inputValues.length > 1) {
inputValues.pop();
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a17680794cc..7d11cd0b6b2 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -115,7 +115,7 @@ GitLabDropdownFilter = (function() {
} else {
elements = this.options.elements();
if (search_text) {
- return elements.each(function() {
+ elements.each(function() {
var $el, matches;
$el = $(this);
matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
@@ -128,8 +128,10 @@ GitLabDropdownFilter = (function() {
}
});
} else {
- return elements.show().removeClass('option-hidden');
+ elements.show().removeClass('option-hidden');
}
+
+ elements.parent().find('.dropdown-menu-empty-link').toggleClass('hidden', elements.is(':visible'));
}
};
@@ -732,9 +734,15 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
if (this.options.filterable) {
this.dropdown.one('transitionend', () => {
+ const initialScrollTop = $(window).scrollTop();
+
if (this.dropdown.is('.open')) {
this.filterInput.focus();
}
+
+ if ($(window).scrollTop() < initialScrollTop) {
+ $(window).scrollTop(initialScrollTop);
+ }
});
if (triggerFocus) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 122ec138c59..e916724b666 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -86,8 +86,9 @@
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
- var fixedTabs = document.querySelector('.js-tabs-affix');
- var fixedNav = document.querySelector('.navbar-gitlab');
+ const fixedTabs = document.querySelector('.js-tabs-affix');
+ const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
+ const fixedNav = document.querySelector('.navbar-gitlab');
var adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
@@ -104,6 +105,11 @@
if (fixedTabs) {
adjustment -= fixedTabs.offsetHeight;
}
+
+ if (fixedDiffStats) {
+ adjustment -= fixedDiffStats.offsetHeight;
+ }
+
window.scrollBy(0, adjustment);
}
};
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
new file mode 100644
index 00000000000..43a808b6ab3
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -0,0 +1,23 @@
+export const isSticky = (el, scrollY, stickyTop) => {
+ const top = el.offsetTop - scrollY;
+
+ if (top === stickyTop) {
+ el.classList.add('is-stuck');
+ } else {
+ el.classList.remove('is-stuck');
+ }
+};
+
+export default (el) => {
+ if (!el) return;
+
+ const computedStyle = window.getComputedStyle(el);
+
+ if (!/sticky/.test(computedStyle.position)) return;
+
+ const stickyTop = parseInt(computedStyle.top, 10);
+
+ document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), {
+ passive: true,
+ });
+};
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index a807ce326a6..42092a34c2f 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -16,9 +16,6 @@ import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
import 'vendor/fuzzaldrin-plus';
-// extensions
-import './extensions/array';
-
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
window.$ = jQuery;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 7840f05a8ae..4ffd71d9de5 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -7,6 +7,7 @@ import Cookies from 'js-cookie';
import './breakpoints';
import './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
+import stickyMonitor from './lib/utils/sticky';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -266,6 +267,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
const $container = $('#diffs');
$container.html(data.html);
+ this.initChangesDropdown();
+
+ stickyMonitor(document.querySelector('.js-diff-files-changed'));
+
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
@@ -314,6 +319,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
});
}
+ initChangesDropdown() {
+ $('.js-diff-stats-dropdown').glDropdown({
+ filterable: true,
+ remoteFilter: false,
+ });
+ }
+
// Show or hide the loading spinner
//
// status - Boolean, true to show, false to hide
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 77cbaeb43ef..66bc1d1979c 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,7 +1,7 @@
<script>
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import '~/flash';
import stageColumnComponent from './stage_column_component.vue';
- import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
- import '../../../flash';
export default {
props: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index fe5e1bbb55c..546a3f625c7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -1,7 +1,7 @@
/**
* This file is the centerpiece of an attempt to reduce potential conflicts
* between the CE and EE versions of the MR widget. EE additions to the MR widget should
- * be contained in the ./vue_merge_request_widget/ee directory, and should **extend**
+ * be contained in the ee/vue_merge_request_widget directory, and should **extend**
* rather than mutate CE MR Widget code.
*
* This file should be the only source of conflicts between EE and CE. EE-only components should
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 89dd99831e4..02e0ba74158 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -574,6 +574,7 @@
.dropdown-input-field,
.default-dropdown-input {
+ display: block;
width: 100%;
min-height: 30px;
padding: 0 7px;
@@ -727,6 +728,10 @@
@mixin new-style-dropdown {
.dropdown-menu,
.dropdown-menu-nav {
+ .divider {
+ margin: 6px 0;
+ }
+
li {
padding: 0 1px;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 555e444a062..4a69c14fa7e 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -325,9 +325,9 @@ header {
li {
.badge {
position: inherit;
- top: -3px;
+ top: -8px;
font-weight: normal;
- margin-left: -12px;
+ margin-left: -11px;
font-size: 11px;
color: $white-light;
padding: 1px 5px 2px;
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 1c4a84de7ec..795ee91af8b 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -312,6 +312,10 @@ header.navbar-gitlab-new {
// TODO: fallback to global style
.dropdown-menu {
+ .divider {
+ margin: 6px 0;
+ }
+
li {
padding: 0 1px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 398fd4444ea..da77346d8b2 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -395,12 +395,11 @@
background-color: transparent;
border: 0;
color: $gl-link-color;
- transition: color 0.1s linear;
+ font-weight: 600;
&:hover,
&:focus {
outline: none;
- text-decoration: underline;
color: $gl-link-hover-color;
}
}
@@ -559,3 +558,68 @@
outline: 0;
}
}
+
+.diff-files-changed {
+ .commit-stat-summary {
+ @include new-style-dropdown;
+ z-index: -1;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: -$gl-padding;
+ padding-left: $gl-padding;
+ background-color: $white-light;
+ }
+ }
+
+ @media (min-width: $screen-sm-min) {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 84px;
+ background-color: $white-light;
+ z-index: 190;
+
+ + .files,
+ + .alert {
+ margin-top: 1px;
+ }
+
+ &:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
+ display: none;
+ }
+
+ &.is-stuck {
+ padding-top: 0;
+ padding-bottom: 0;
+ border-bottom: 1px solid $white-dark;
+ transform: translateY(16px);
+
+ .diff-stats-additions-deletions-expanded,
+ .inline-parallel-buttons {
+ display: none;
+ }
+
+ + .files,
+ + .alert {
+ margin-top: 30px;
+ }
+ }
+ }
+}
+
+.diff-file-changes {
+ width: 450px;
+ z-index: 150;
+
+ @media (min-width: $screen-sm-min) {
+ left: $gl-padding;
+ }
+
+ a {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ }
+}
+
+.diff-file-changes-path {
+ @include str-truncated(78%);
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6da14320914..88343bd0113 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -328,9 +328,17 @@
margin-bottom: 10px;
color: $issuable-sidebar-color;
+ svg {
+ fill: $issuable-sidebar-color;
+ }
+
&:hover,
&:hover .todo-undone {
color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
}
span {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 4693b2434c7..a4e19094508 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -691,8 +691,10 @@
}
.mr-version-controls {
+ position: relative;
background: $gray-light;
color: $gl-text-color;
+ z-index: 199;
.mr-version-menus-container {
display: -webkit-flex;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index d29421aa1b3..73603f20ef6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -36,7 +36,6 @@
}
select {
- background: transparent;
transition: background 2s ease-out;
&.highlight-changes {
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index d69a8e0995c..15df51e9c69 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -54,8 +54,7 @@
.settings-content {
max-height: 1px;
overflow-y: scroll;
- margin-right: -20px;
- padding-right: 130px;
+ padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
&.expanded {
@@ -87,6 +86,23 @@
overflow: hidden;
margin-top: 20px;
}
+
+ .sub-section {
+ margin-bottom: 32px;
+ padding: 16px;
+ border: 1px solid $border-color;
+ background-color: $gray-light;
+ }
+
+ .bs-callout,
+ .checkbox:first-child,
+ .help-block {
+ margin-top: 0;
+ }
+
+ .label-light {
+ margin-bottom: 0;
+ }
}
.settings-list-icon {
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 91ddd73fac1..087f7f88fb5 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -148,6 +148,24 @@ module DiffHelper
options
end
+ def diff_file_changed_icon(diff_file)
+ if diff_file.deleted_file? || diff_file.renamed_file?
+ "minus"
+ elsif diff_file.new_file?
+ "plus"
+ else
+ "adjust"
+ end
+ end
+
+ def diff_file_changed_icon_color(diff_file)
+ if diff_file.deleted_file?
+ "cred"
+ elsif diff_file.new_file?
+ "cgreen"
+ end
+ end
+
private
def diff_btn(title, name, selected)
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
new file mode 100644
index 00000000000..418b42d8f1d
--- /dev/null
+++ b/app/models/notification_recipient.rb
@@ -0,0 +1,125 @@
+class NotificationRecipient
+ attr_reader :user, :type
+ def initialize(
+ user, type,
+ custom_action: nil,
+ target: nil,
+ acting_user: nil,
+ project: nil
+ )
+ @custom_action = custom_action
+ @acting_user = acting_user
+ @target = target
+ @project = project || @target&.project
+ @user = user
+ @type = type
+ end
+
+ def notification_setting
+ @notification_setting ||= find_notification_setting
+ end
+
+ def raw_notification_level
+ notification_setting&.level&.to_sym
+ end
+
+ def notification_level
+ # custom is treated the same as watch if it's enabled - otherwise it's
+ # set to :custom, meaning to send exactly when our type is :participating
+ # or :mention.
+ @notification_level ||=
+ case raw_notification_level
+ when :custom
+ if @custom_action && notification_setting&.event_enabled?(@custom_action)
+ :watch
+ else
+ :custom
+ end
+ else
+ raw_notification_level
+ end
+ end
+
+ def notifiable?
+ return false unless has_access?
+ return false if own_activity?
+
+ return true if @type == :subscription
+
+ return false if notification_level.nil? || notification_level == :disabled
+
+ return %i[participating mention].include?(@type) if notification_level == :custom
+
+ return false if %i[watch participating].include?(notification_level) && excluded_watcher_action?
+
+ return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[@type]
+
+ return false if unsubscribed?
+
+ true
+ end
+
+ def unsubscribed?
+ return false unless @target
+ return false unless @target.respond_to?(:subscriptions)
+
+ subscription = @target.subscriptions.find_by_user_id(@user.id)
+ subscription && !subscription.subscribed
+ end
+
+ def own_activity?
+ return false unless @acting_user
+ return false if @acting_user.notified_of_own_activity?
+
+ user == @acting_user
+ end
+
+ def has_access?
+ DeclarativePolicy.subject_scope do
+ return false unless user.can?(:receive_notifications)
+ return false if @project && !user.can?(:read_project, @project)
+
+ return true unless read_ability
+ return true unless DeclarativePolicy.has_policy?(@target)
+
+ user.can?(read_ability, @target)
+ end
+ end
+
+ def excluded_watcher_action?
+ return false unless @custom_action
+ return false if raw_notification_level == :custom
+
+ NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
+ end
+
+ private
+
+ def read_ability
+ return @read_ability if instance_variable_defined?(:@read_ability)
+
+ @read_ability =
+ case @target
+ when Issuable
+ :"read_#{@target.to_ability_name}"
+ when Ci::Pipeline
+ :read_build # We have build trace in pipeline emails
+ when ActiveRecord::Base
+ :"read_#{@target.class.model_name.name.underscore}"
+ else
+ nil
+ end
+ end
+
+ def find_notification_setting
+ project_setting = @project && user.notification_settings_for(@project)
+
+ return project_setting unless project_setting.nil? || project_setting.global?
+
+ group_setting = @project&.group && user.notification_settings_for(@project.group)
+
+ return group_setting unless group_setting.nil? || group_setting.global?
+
+ user.global_notification_setting
+ end
+end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index c2414885368..9ee3a533c1e 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -104,7 +104,7 @@ class JiraService < IssueTrackerService
def close_issue(entity, external_issue)
issue = jira_request { client.Issue.find(external_issue.iid) }
- return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present?
+ return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present?
commit_id = if entity.is_a?(Commit)
entity.id
@@ -118,7 +118,7 @@ class JiraService < IssueTrackerService
# may or may not be allowed. Refresh the issue after transition and check
# if it is closed, so we don't have one comment for every commit.
issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
- add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution
+ add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
end
def create_cross_reference_note(mentioned, noteable, author)
@@ -216,6 +216,10 @@ class JiraService < IssueTrackerService
end
end
+ def has_resolution?(issue)
+ issue.respond_to?(:resolution) && issue.resolution.present?
+ end
+
def comment_exists?(issue, message)
comments = jira_request { issue.comments }
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 4e9fe759fdc..2dd48290e58 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -613,17 +613,26 @@ class Repository
end
def last_commit_for_path(sha, path)
- sha = last_commit_id_for_path(sha, path)
- commit(sha)
+ raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled|
+ if is_enabled
+ last_commit_for_path_by_gitaly(sha, path)
+ else
+ last_commit_for_path_by_rugged(sha, path)
+ end
+ end
end
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/383
def last_commit_id_for_path(sha, path)
key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
cache.fetch(key) do
- args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
- Gitlab::Popen.popen(args, path_to_repo).first.strip
+ raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled|
+ if is_enabled
+ last_commit_for_path_by_gitaly(sha, path).id
+ else
+ last_commit_id_for_path_by_shelling_out(sha, path)
+ end
+ end
end
end
@@ -1138,6 +1147,21 @@ class Repository
Rugged::Commit.create(rugged, params)
end
+ def last_commit_for_path_by_gitaly(sha, path)
+ c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
+ commit(c)
+ end
+
+ def last_commit_for_path_by_rugged(sha, path)
+ sha = last_commit_id_for_path_by_shelling_out(sha, path)
+ commit(sha)
+ end
+
+ def last_commit_id_for_path_by_shelling_out(sha, path)
+ args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
+ Gitlab::Popen.popen(args, path_to_repo).first.strip
+ end
+
def repository_storage_path
@project.repository_storage_path
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 9ac561e4bd2..21c9c314a2a 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -1,331 +1,288 @@
#
# Used by NotificationService to determine who should receive notification
#
-class NotificationRecipientService
- attr_reader :project
-
- def initialize(project)
- @project = project
+module NotificationRecipientService
+ def self.notifiable_users(users, *args)
+ users.compact.map { |u| NotificationRecipient.new(u, *args) }.select(&:notifiable?).map(&:user)
end
- def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
- custom_action = build_custom_key(action, target)
-
- recipients = participants(target, current_user)
- recipients = add_project_watchers(recipients)
- recipients = add_custom_notifications(recipients, custom_action)
- recipients = reject_mention_users(recipients)
-
- # Re-assign is considered as a mention of the new assignee so we add the
- # new assignee to the list of recipients after we rejected users with
- # the "on mention" notification level
- case custom_action
- when :reassign_merge_request
- recipients << previous_assignee if previous_assignee
- recipients << target.assignee
- when :reassign_issue
- previous_assignees = Array(previous_assignee)
- recipients.concat(previous_assignees)
- recipients.concat(target.assignees)
- end
-
- recipients = reject_muted_users(recipients)
- recipients = add_subscribed_users(recipients, target)
-
- if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, target)
- end
-
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
+ def self.notifiable?(user, *args)
+ NotificationRecipient.new(user, *args).notifiable?
+ end
- recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
+ def self.build_recipients(*a)
+ Builder::Default.new(*a).recipient_users
+ end
- recipients.uniq
+ def self.build_new_note_recipients(*a)
+ Builder::NewNote.new(*a).recipient_users
end
- def build_pipeline_recipients(target, current_user, action:)
- return [] unless current_user
+ module Builder
+ class Base
+ def initialize(*)
+ raise 'abstract'
+ end
- custom_action =
- case action.to_s
- when 'failed'
- :failed_pipeline
- when 'success'
- :success_pipeline
+ def build!
+ raise 'abstract'
end
- notification_setting = notification_setting_for_user_project(current_user, target.project)
+ def filter!
+ recipients.select!(&:notifiable?)
+ end
- return [] if notification_setting.mention? || notification_setting.disabled?
+ def acting_user
+ current_user
+ end
- return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action)
+ def target
+ raise 'abstract'
+ end
- return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ # rubocop:disable Rails/Delegate
+ def project
+ target.project
+ end
- reject_users_without_access([current_user], target)
- end
+ def recipients
+ @recipients ||= []
+ end
- def build_relabeled_recipients(target, current_user, labels:)
- recipients = add_labels_subscribers([], target, labels: labels)
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user) unless current_user.notified_of_own_activity?
- recipients.uniq
- end
+ def <<(pair)
+ users, type = pair
- def build_new_note_recipients(note)
- target = note.noteable
+ if users.is_a?(ActiveRecord::Relation)
+ users = users.includes(:notification_settings)
+ end
- ability, subject = if note.for_personal_snippet?
- [:read_personal_snippet, note.noteable]
- else
- [:read_project, note.project]
- end
+ users = Array(users)
+ users.compact!
+ recipients.concat(users.map { |u| make_recipient(u, type) })
+ end
- mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
+ def user_scope
+ User.includes(:notification_settings)
+ end
- # Add all users participating in the thread (author, assignee, comment authors)
- recipients = participants(target, note.author) || mentioned_users
+ def make_recipient(user, type)
+ NotificationRecipient.new(
+ user, type,
+ project: project,
+ custom_action: custom_action,
+ target: target,
+ acting_user: acting_user
+ )
+ end
- unless note.for_personal_snippet?
- # Merge project watchers
- recipients = add_project_watchers(recipients)
+ def recipient_users
+ @recipient_users ||=
+ begin
+ build!
+ filter!
+ users = recipients.map(&:user)
+ users.uniq!
+ users.freeze
+ end
+ end
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, :new_note)
- end
+ def custom_action
+ nil
+ end
- # Reject users with Mention notification level, except those mentioned in _this_ note.
- recipients = reject_mention_users(recipients - mentioned_users)
- recipients = recipients + mentioned_users
+ protected
- recipients = reject_muted_users(recipients)
+ def add_participants(user)
+ return unless target.respond_to?(:participants)
- recipients = add_subscribed_users(recipients, note.noteable)
- recipients = reject_unsubscribed_users(recipients, note.noteable)
- recipients = reject_users_without_access(recipients, note.noteable)
+ self << [target.participants(user), :watch]
+ end
- recipients.delete(note.author) unless note.author.notified_of_own_activity?
- recipients.uniq
- end
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications
+ user_ids = []
- # Remove users with disabled notifications from array
- # Also remove duplications and nil recipients
- def reject_muted_users(users)
- reject_users(users, :disabled)
- end
+ # Users with a notification setting on group or project
+ user_ids += user_ids_notifiable_on(project, :custom)
+ user_ids += user_ids_notifiable_on(project.group, :custom)
- protected
+ # Users with global level custom
+ user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
- # Ensure that if we modify this array, we aren't modifying the memoised
- # participants on the target.
- def participants(target, user)
- return unless target.respond_to?(:participants)
+ global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
+ user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action)
- target.participants(user).dup
- end
+ self << [user_scope.where(id: user_ids), :watch]
+ end
- # Get project/group users with CUSTOM notification level
- def add_custom_notifications(recipients, action)
- user_ids = []
+ def add_project_watchers
+ self << [project_watchers, :watch]
+ end
- # Users with a notification setting on group or project
- user_ids += user_ids_notifiable_on(project, :custom, action)
- user_ids += user_ids_notifiable_on(project.group, :custom, action)
+ # Get project users with WATCH notification level
+ def project_watchers
+ project_members_ids = user_ids_notifiable_on(project)
- # Users with global level custom
- user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
- user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+ user_ids_with_project_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_global = user_ids_notifiable_on(project.group, :global)
- global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
- user_ids += user_ids_with_global_level_custom(global_users_ids, action)
+ user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
- recipients.concat(User.find(user_ids))
- end
+ user_ids_with_project_setting = select_project_members_ids(user_ids_with_project_global, user_ids)
+ user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
- def add_project_watchers(recipients)
- recipients.concat(project_watchers).compact
- end
+ user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq)
+ end
- # Get project users with WATCH notification level
- def project_watchers
- project_members_ids = user_ids_notifiable_on(project)
+ def add_subscribed_users
+ return unless target.respond_to? :subscribers
- user_ids_with_project_global = user_ids_notifiable_on(project, :global)
- user_ids_with_group_global = user_ids_notifiable_on(project.group, :global)
+ self << [target.subscribers(project), :subscription]
+ end
- user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
+ def user_ids_notifiable_on(resource, notification_level = nil)
+ return [] unless resource
- user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
- user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
+ scope = resource.notification_settings
- User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
- end
+ if notification_level
+ scope = scope.where(level: NotificationSetting.levels[notification_level])
+ end
- # Remove users with notification level 'Mentioned'
- def reject_mention_users(users)
- reject_users(users, :mention)
- end
+ scope.pluck(:user_id)
+ end
- def add_subscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscribers
+ # Build a list of user_ids based on project notification settings
+ def select_project_members_ids(global_setting, user_ids_global_level_watch)
+ user_ids = user_ids_notifiable_on(project, :watch)
- recipients + target.subscribers(project)
- end
+ # If project setting is global, add to watch list if global setting is watch
+ user_ids + (global_setting & user_ids_global_level_watch)
+ end
- def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
- return [] unless resource
+ # Build a list of user_ids based on group notification settings
+ def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
+ uids = user_ids_notifiable_on(group, :watch)
- if notification_level
- settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
- settings = settings.select { |setting| setting.event_enabled?(action) } if action.present?
- settings.map(&:user_id)
- else
- resource.notification_settings.pluck(:user_id)
- end
- end
+ # Group setting is global, add to user_ids list if global setting is watch
+ uids + (global_setting & user_ids_global_level_watch) - project_members
+ end
- # Build a list of user_ids based on project notification settings
- def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
- user_ids = user_ids_notifiable_on(project, :watch)
+ def user_ids_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
+ end
- # If project setting is global, add to watch list if global setting is watch
- global_setting.each do |user_id|
- if user_ids_global_level_watch.include?(user_id)
- user_ids << user_id
+ def user_ids_with_global_level_custom(ids, action)
+ settings_with_global_level_of(:custom, ids).pluck(:user_id)
end
- end
- user_ids
- end
+ def settings_with_global_level_of(level, ids)
+ NotificationSetting.where(
+ user_id: ids,
+ source_type: nil,
+ level: NotificationSetting.levels[level]
+ )
+ end
- # Build a list of user_ids based on group notification settings
- def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
- uids = user_ids_notifiable_on(group, :watch)
+ def add_labels_subscribers(labels: nil)
+ return unless target.respond_to? :labels
- # Group setting is watch, add to user_ids list if user is not project member
- user_ids = []
- uids.each do |user_id|
- if project_members.exclude?(user_id)
- user_ids << user_id
+ (labels || target.labels).each do |label|
+ self << [label.subscribers(project), :subscription]
+ end
end
end
- # Group setting is global, add to user_ids list if global setting is watch
- global_setting.each do |user_id|
- if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
- user_ids << user_id
+ class Default < Base
+ attr_reader :target
+ attr_reader :current_user
+ attr_reader :action
+ attr_reader :previous_assignee
+ attr_reader :skip_current_user
+ def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
+ @target = target
+ @current_user = current_user
+ @action = action
+ @previous_assignee = previous_assignee
+ @skip_current_user = skip_current_user
end
- end
-
- user_ids
- end
-
- def user_ids_with_global_level_watch(ids)
- settings_with_global_level_of(:watch, ids).pluck(:user_id)
- end
-
- def user_ids_with_global_level_custom(ids, action)
- settings = settings_with_global_level_of(:custom, ids)
- settings = settings.select { |setting| setting.event_enabled?(action) }
- settings.map(&:user_id)
- end
- def settings_with_global_level_of(level, ids)
- NotificationSetting.where(
- user_id: ids,
- source_type: nil,
- level: NotificationSetting.levels[level]
- )
- end
+ def build!
+ add_participants(current_user)
+ add_project_watchers
+ add_custom_notifications
+
+ # Re-assign is considered as a mention of the new assignee
+ case custom_action
+ when :reassign_merge_request
+ self << [previous_assignee, :mention]
+ self << [target.assignee, :mention]
+ when :reassign_issue
+ previous_assignees = Array(previous_assignee)
+ self << [previous_assignees, :mention]
+ self << [target.assignees, :mention]
+ end
+
+ add_subscribed_users
+
+ if [:new_issue, :new_merge_request].include?(custom_action)
+ add_labels_subscribers
+ end
+ end
- # Reject users which has certain notification level
- #
- # Example:
- # reject_users(users, :watch, project)
- #
- def reject_users(users, level)
- level = level.to_s
+ def acting_user
+ current_user if skip_current_user
+ end
- unless NotificationSetting.levels.keys.include?(level)
- raise 'Invalid notification level'
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def custom_action
+ @custom_action ||= "#{action}_#{target.class.model_name.name.underscore}".to_sym
+ end
end
- users = users.to_a.compact.uniq
- users = users.select { |u| u.can?(:receive_notifications) }
-
- users.reject do |user|
- global_notification_setting = user.global_notification_setting
-
- next global_notification_setting.level == level unless project
-
- setting = user.notification_settings_for(project)
-
- if project.group && (setting.nil? || setting.global?)
- setting = user.notification_settings_for(project.group)
+ class NewNote < Base
+ attr_reader :note
+ def initialize(note)
+ @note = note
end
- # reject users who globally set mention notification and has no setting per project/group
- next global_notification_setting.level == level unless setting
-
- # reject users who set mention notification in project
- next true if setting.level == level
-
- # reject users who have mention level in project and disabled in global settings
- setting.global? && global_notification_setting.level == level
- end
- end
+ def target
+ note.noteable
+ end
- def reject_unsubscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
+ # NOTE: may be nil, in the case of a PersonalSnippet
+ #
+ # (this is okay because NotificationRecipient is written
+ # to handle nil projects)
+ def project
+ note.project
+ end
- recipients.reject do |user|
- subscription = target.subscriptions.find_by_user_id(user.id)
- subscription && !subscription.subscribed
- end
- end
+ def build!
+ # Add all users participating in the thread (author, assignee, comment authors)
+ add_participants(note.author)
+ self << [note.mentioned_users, :mention]
- def reject_users_without_access(recipients, target)
- ability = case target
- when Issuable
- :"read_#{target.to_ability_name}"
- when Ci::Pipeline
- :read_build # We have build trace in pipeline emails
- end
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ add_project_watchers
- return recipients unless ability
+ # Merge project with custom notification
+ add_custom_notifications
+ end
- recipients.select do |user|
- user.can?(ability, target)
- end
- end
+ add_subscribed_users
+ end
- def add_labels_subscribers(recipients, target, labels: nil)
- return recipients unless target.respond_to? :labels
+ def custom_action
+ :new_note
+ end
- (labels || target.labels).each do |label|
- recipients += label.subscribers(project)
+ def acting_user
+ note.author
+ end
end
-
- recipients
- end
-
- # Build event key to search on custom notification level
- # Check NotificationSetting::EMAIL_EVENTS
- def build_custom_key(action, object)
- "#{action}_#{object.class.model_name.name.underscore}".to_sym
- end
-
- def notification_setting_for_user_project(user, project)
- project_setting = user.notification_settings_for(project)
-
- return project_setting unless project_setting.global?
-
- group_setting = user.notification_settings_for(project.group)
-
- return group_setting unless group_setting.global?
-
- user.global_notification_setting
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index b94921d2a08..df04b1a4fe3 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -42,7 +42,7 @@ class NotificationService
# * users with custom level checked with "new issue"
#
def new_issue(issue, current_user)
- new_resource_email(issue, issue.project, :new_issue_email)
+ new_resource_email(issue, :new_issue_email)
end
# When issue text is updated, we should send an email to:
@@ -52,7 +52,6 @@ class NotificationService
def new_mentions_in_issue(issue, new_mentioned_users, current_user)
new_mentions_in_resource_email(
issue,
- issue.project,
new_mentioned_users,
current_user,
:new_mention_in_issue_email
@@ -67,7 +66,7 @@ class NotificationService
# * users with custom level checked with "close issue"
#
def close_issue(issue, current_user)
- close_resource_email(issue, issue.project, current_user, :closed_issue_email)
+ close_resource_email(issue, current_user, :closed_issue_email)
end
# When we reassign an issue we should send an email to:
@@ -77,7 +76,7 @@ class NotificationService
# * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user, previous_assignees = [])
- recipients = NotificationRecipientService.new(issue.project).build_recipients(
+ recipients = NotificationRecipientService.build_recipients(
issue,
current_user,
action: "reassign",
@@ -102,7 +101,7 @@ class NotificationService
# * watchers of the issue's labels
#
def relabeled_issue(issue, added_labels, current_user)
- relabeled_resource_email(issue, issue.project, added_labels, current_user, :relabeled_issue_email)
+ relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
end
# When create a merge request we should send an email to:
@@ -113,7 +112,7 @@ class NotificationService
# * users with custom level checked with "new merge request"
#
def new_merge_request(merge_request, current_user)
- new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
+ new_resource_email(merge_request, :new_merge_request_email)
end
# When merge request text is updated, we should send an email to:
@@ -123,7 +122,6 @@ class NotificationService
def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user)
new_mentions_in_resource_email(
merge_request,
- merge_request.target_project,
new_mentioned_users,
current_user,
:new_mention_in_merge_request_email
@@ -137,7 +135,7 @@ class NotificationService
# * users with custom level checked with "reassign merge request"
#
def reassigned_merge_request(merge_request, current_user)
- reassign_resource_email(merge_request, merge_request.target_project, current_user, :reassigned_merge_request_email)
+ reassign_resource_email(merge_request, current_user, :reassigned_merge_request_email)
end
# When we add labels to a merge request we should send an email to:
@@ -145,21 +143,20 @@ class NotificationService
# * watchers of the mr's labels
#
def relabeled_merge_request(merge_request, added_labels, current_user)
- relabeled_resource_email(merge_request, merge_request.target_project, added_labels, current_user, :relabeled_merge_request_email)
+ relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
end
def close_mr(merge_request, current_user)
- close_resource_email(merge_request, merge_request.target_project, current_user, :closed_merge_request_email)
+ close_resource_email(merge_request, current_user, :closed_merge_request_email)
end
def reopen_issue(issue, current_user)
- reopen_resource_email(issue, issue.project, current_user, :issue_status_changed_email, 'reopened')
+ reopen_resource_email(issue, current_user, :issue_status_changed_email, 'reopened')
end
def merge_mr(merge_request, current_user)
close_resource_email(
merge_request,
- merge_request.target_project,
current_user,
:merged_merge_request_email,
skip_current_user: !merge_request.merge_when_pipeline_succeeds?
@@ -169,7 +166,6 @@ class NotificationService
def reopen_mr(merge_request, current_user)
reopen_resource_email(
merge_request,
- merge_request.target_project,
current_user,
:merge_request_status_email,
'reopened'
@@ -177,7 +173,7 @@ class NotificationService
end
def resolve_all_discussions(merge_request, current_user)
- recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients(
+ recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
action: "resolve_all_discussions")
@@ -202,7 +198,7 @@ class NotificationService
notify_method = "note_#{note.to_ability_name}_email".to_sym
- recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
+ recipients = NotificationRecipientService.build_new_note_recipients(note)
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
@@ -270,8 +266,7 @@ class NotificationService
end
def project_was_moved(project, old_path_with_namespace)
- recipients = project.team.members
- recipients = NotificationRecipientService.new(project).reject_muted_users(recipients)
+ recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project)
recipients.each do |recipient|
mailer.project_was_moved_email(
@@ -283,7 +278,7 @@ class NotificationService
end
def issue_moved(issue, new_issue, current_user)
- recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user, action: 'moved')
+ recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved')
recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
@@ -305,10 +300,10 @@ class NotificationService
return unless mailer.respond_to?(email_template)
- recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
- pipeline,
- pipeline.user,
- action: pipeline.status
+ recipients ||= NotificationRecipientService.notifiable_users(
+ [pipeline.user], :watch,
+ custom_action: :"#{pipeline.status}_pipeline",
+ target: pipeline
).map(&:notification_email)
if recipients.any?
@@ -318,16 +313,16 @@ class NotificationService
protected
- def new_resource_email(target, project, method)
- recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new")
+ def new_resource_email(target, method)
+ recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
end
end
- def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
- recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new")
+ def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method)
+ recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new")
recipients = recipients & new_mentioned_users
recipients.each do |recipient|
@@ -335,10 +330,10 @@ class NotificationService
end
end
- def close_resource_email(target, project, current_user, method, skip_current_user: true)
+ def close_resource_email(target, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close"
- recipients = NotificationRecipientService.new(project).build_recipients(
+ recipients = NotificationRecipientService.build_recipients(
target,
current_user,
action: action,
@@ -350,11 +345,11 @@ class NotificationService
end
end
- def reassign_resource_email(target, project, current_user, method)
+ def reassign_resource_email(target, current_user, method)
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = NotificationRecipientService.new(project).build_recipients(
+ recipients = NotificationRecipientService.build_recipients(
target,
current_user,
action: "reassign",
@@ -372,8 +367,14 @@ class NotificationService
end
end
- def relabeled_resource_email(target, project, labels, current_user, method)
- recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels)
+ def relabeled_resource_email(target, labels, current_user, method)
+ recipients = labels.flat_map { |l| l.subscribers(target.project) }
+ recipients = NotificationRecipientService.notifiable_users(
+ recipients, :subscription,
+ target: target,
+ acting_user: current_user
+ )
+
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -381,8 +382,8 @@ class NotificationService
end
end
- def reopen_resource_email(target, project, current_user, method, status)
- recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen")
+ def reopen_resource_email(target, current_user, method, status)
+ recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index bc3293fd100..b32cfe158bb 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -44,7 +44,7 @@
= icon('tachometer fw')
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('hashtag fw')
+ = custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
index 60940dba475..2c1c23d6ea9 100644
--- a/app/views/layouts/header/_new.html.haml
+++ b/app/views/layouts/header/_new.html.haml
@@ -38,7 +38,7 @@
= icon('tachometer fw')
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('hashtag fw')
+ = custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 8605380848d..261445ecd2b 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -25,7 +25,7 @@
%span
Members
- if current_user && can?(current_user, :admin_group, @group)
- = nav_link(path: %w[groups#projects groups#edit]) do
+ = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
= link_to edit_group_path(@group), title: 'Settings' do
%span
Settings
diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml
index e2f6e9c1aa2..54ea39a2d36 100644
--- a/app/views/layouts/nav/_new_admin_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml
@@ -110,7 +110,7 @@
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
.nav-icon-container
- = custom_icon('mr_bold')
+ = custom_icon('spam_logs')
%span.nav-item-name
Spam Logs
diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml
index fdfd7e60732..33a83866cbf 100644
--- a/app/views/layouts/nav/_new_group_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_group_sidebar.html.haml
@@ -85,6 +85,6 @@
Projects
= nav_link(controller: :ci_cd) do
- = link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do
+ = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do
%span
- Pipelines
+ CI / CD
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index 9f1cb248c4e..673febbc798 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -76,7 +76,7 @@
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
.nav-icon-container
- = custom_icon('mr_bold')
+ = custom_icon('container_registry')
%span.nav-item-name
Registry
@@ -122,11 +122,11 @@
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do
.nav-icon-container
= custom_icon('pipeline')
%span.nav-item-name
- Pipelines
+ CI / CD
%ul.sidebar-sub-level-items
- if project_nav_tab? :pipelines
@@ -205,9 +205,9 @@
Repository
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
- = link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do
+ = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do
%span
- Pipelines
+ CI / CD
- if Gitlab.config.pages.enabled
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 818010bc7d3..cc5afa943cf 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,8 +1,3 @@
- form = local_assigns.fetch(:form)
-%fieldset.features.merge-requests-feature.append-bottom-default
- %hr
- %h5.prepend-top-0
- Merge Requests
-
- = render 'projects/merge_request_merge_settings', form: form
+= render 'projects/merge_request_merge_settings', form: form
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index f9385459a66..8c8aa4c78f5 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -3,7 +3,7 @@
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
-.content-block.oneline-block.files-changed
+.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.inline-parallel-buttons
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
= link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index e69c7f20d49..efc0ea31917 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -1,36 +1,34 @@
-.js-toggle-container
- .commit-stat-summary
- Showing
- %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" }
- %strong= pluralize(diff_files.size, "changed file")
+- sum_added_lines = diff_files.sum(&:added_lines)
+- sum_removed_lines = diff_files.sum(&:removed_lines)
+.commit-stat-summary.dropdown
+ Showing
+ %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }<
+ = pluralize(diff_files.size, "changed file")
+ = icon("caret-down", class: "prepend-left-5")
+ %span.diff-stats-additions-deletions-expanded#diff-stats
with
- %strong.cgreen #{diff_files.sum(&:added_lines)} additions
+ %strong.cgreen #{sum_added_lines} additions
and
- %strong.cred #{diff_files.sum(&:removed_lines)} deletions
- .file-stats.js-toggle-content.hide
- %ul
- - diff_files.each do |diff_file|
- - file_hash = hexdigest(diff_file.file_path)
- %li
- - if diff_file.deleted_file?
- %span.deleted-file
- %a{ href: "##{file_hash}" }
- %i.fa.fa-minus
- = diff_file.old_path
- - elsif diff_file.renamed_file?
- %span.renamed-file
- %a{ href: "##{file_hash}" }
- %i.fa.fa-minus
- = diff_file.old_path
- &rarr;
- = diff_file.new_path
- - elsif diff_file.new_file?
- %span.new-file
- %a{ href: "##{file_hash}" }
- %i.fa.fa-plus
- = diff_file.new_path
- - else
- %span.edit-file
- %a{ href: "##{file_hash}" }
- %i.fa.fa-adjust
- = diff_file.new_path
+ %strong.cred #{sum_removed_lines} deletions
+ .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
+ %strong.cgreen<
+ +#{sum_added_lines}
+ %strong.cred<
+ \-#{sum_removed_lines}
+ .dropdown-menu.diff-file-changes
+ = dropdown_filter("Search files")
+ .dropdown-content
+ %ul
+ - diff_files.each do |diff_file|
+ %li
+ %a{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
+ = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5")
+ %span.diff-file-changes-path= diff_file.new_path
+ .pull-right
+ %span.cgreen<
+ +#{diff_file.added_lines}
+ %span.cred<
+ \-#{diff_file.removed_lines}
+ %li.dropdown-menu-empty-link.hidden
+ %a{ href: "#" }
+ No files found.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 087cb804449..20fceda26dc 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,12 +1,19 @@
+- page_title "General"
- @content_class = "limit-container-width" unless fluid_layout
+- expanded = Rails.env.test?
= render "projects/settings/head"
+
.project-edit-container
- .row.prepend-top-default
- .col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0
- Project settings
- .col-lg-8
+ %section.settings.general-settings
+ .settings-header
+ %h4
+ General project settings
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Update your project name, description, avatar, and other general settings.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
.project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset
@@ -35,89 +42,7 @@
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
- %hr
- %fieldset
- %h5.prepend-top-0
- Sharing &amp; Permissions
- .form_group.prepend-top-20.sharing-and-permissions
- .row.js-visibility-select
- .col-md-8
- .label-light
- = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
- = link_to icon('question-circle'), help_page_path("public_access/public_access")
- %span.help-block
- .col-md-4.visibility-select-container
- = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
- = f.fields_for :project_feature do |feature_fields|
- %fieldset.features
- .row
- .col-md-8.project-feature
- = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
- %span.help-block View and edit files in this project
- .col-md-4.js-repo-access-level
- = project_feature_access_select(:repository_access_level)
-
- .row
- .col-md-8.project-feature.nested
- = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
- %span.help-block Submit changes to be merged upstream
- .col-md-4
- = project_feature_access_select(:merge_requests_access_level)
-
- .row
- .col-md-8.project-feature.nested
- = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
- %span.help-block Build, test, and deploy your changes
- .col-md-4
- = project_feature_access_select(:builds_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
- %span.help-block Share code pastes with others out of Git repository
- .col-md-4
- = project_feature_access_select(:snippets_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
- %span.help-block Lightweight issue tracking system for this project
- .col-md-4
- = project_feature_access_select(:issues_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
- %span.help-block Pages for project documentation
- .col-md-4
- = project_feature_access_select(:wiki_access_level)
- .form-group
- = render 'shared/allow_request_access', form: f
- - if Gitlab.config.lfs.enabled && current_user.admin?
- .row.js-lfs-enabled
- .col-md-8
- = f.label :lfs_enabled, 'LFS', class: 'label-light'
- %span.help-block
- Git Large File Storage
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- .col-md-4
- .select-wrapper
- = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
- = icon('chevron-down')
- - if Gitlab.config.registry.enabled
- .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
- .checkbox
- = f.label :container_registry_enabled do
- = f.check_box :container_registry_enabled
- %strong Container Registry
- %br
- %span.descr Enable Container Registry for this project
- = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
-
- = render 'merge_request_settings', form: f
-
- %hr
- %fieldset.features.append-bottom-default
+ %fieldset.features
%h5.prepend-top-0
Project avatar
.form-group
@@ -137,41 +62,114 @@
= link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
- .row.prepend-top-default
- %hr
- .row.prepend-top-default
- .col-lg-4
- %h4.prepend-top-0
- Housekeeping
- %p.append-bottom-0
- %p
- Runs a number of housekeeping tasks within the current repository,
- such as compressing file revisions and removing unreachable objects.
- .col-lg-8
- = link_to 'Housekeeping', housekeeping_project_path(@project),
- method: :post, class: "btn btn-default"
- %hr
- .row.prepend-top-default
- .col-lg-4
- %h4.prepend-top-0
- Export project
- %p.append-bottom-0
- %p
- Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
- %p
- Once the exported file is ready, you will receive a notification email with a download link.
+ %section.settings.sharing-permissions
+ .settings-header
+ %h4
+ Sharing and permissions
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Enable or disable certain project features and choose access levels.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
+ .form_group.sharing-and-permissions
+ .row.js-visibility-select
+ .col-md-8
+ .label-light
+ = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
+ %span.help-block
+ .col-md-4.visibility-select-container
+ = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
+ = f.fields_for :project_feature do |feature_fields|
+ %fieldset.features
+ .row
+ .col-md-8.project-feature
+ = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
+ %span.help-block View and edit files in this project
+ .col-md-4.js-repo-access-level
+ = project_feature_access_select(:repository_access_level)
- .col-lg-8
+ .row
+ .col-md-8.project-feature.nested
+ = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
+ %span.help-block Submit changes to be merged upstream
+ .col-md-4
+ = project_feature_access_select(:merge_requests_access_level)
- - if @project.export_project_path
- = link_to 'Download export', download_export_project_path(@project),
- rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
- = link_to 'Generate new export', generate_new_export_project_path(@project),
- method: :post, class: "btn btn-default"
- - else
- = link_to 'Export project', export_project_path(@project),
- method: :post, class: "btn btn-default"
+ .row
+ .col-md-8.project-feature.nested
+ = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
+ %span.help-block Build, test, and deploy your changes
+ .col-md-4
+ = project_feature_access_select(:builds_access_level)
+
+ .row
+ .col-md-8.project-feature
+ = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
+ %span.help-block Share code pastes with others out of Git repository
+ .col-md-4
+ = project_feature_access_select(:snippets_access_level)
+
+ .row
+ .col-md-8.project-feature
+ = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
+ %span.help-block Lightweight issue tracking system for this project
+ .col-md-4
+ = project_feature_access_select(:issues_access_level)
+
+ .row
+ .col-md-8.project-feature
+ = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
+ %span.help-block Pages for project documentation
+ .col-md-4
+ = project_feature_access_select(:wiki_access_level)
+ .form-group
+ = render 'shared/allow_request_access', form: f
+ - if Gitlab.config.lfs.enabled && current_user.admin?
+ .row.js-lfs-enabled.form-group.sharing-and-permissions
+ .col-md-8
+ = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light'
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ %span.help-block Manages large files such as audio, video and graphics files.
+ .col-md-4
+ .select-wrapper
+ = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
+ = icon('chevron-down')
+ - if Gitlab.config.registry.enabled
+ .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
+ .checkbox
+ = f.label :container_registry_enabled do
+ = f.check_box :container_registry_enabled
+ %strong Container Registry
+ %br
+ %span.descr Enable Container Registry for this project
+ = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
+ = f.submit 'Save changes', class: "btn btn-save"
+
+
+ %section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) }
+ .settings-header
+ %h4
+ Merge request settings
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Customize your merge request restrictions.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
+ = render 'merge_request_settings', form: f
+ = f.submit 'Save changes', class: "btn btn-save"
+ %section.settings
+ .settings-header
+ %h4
+ Export project
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
.bs-callout.bs-callout-info
%p.append-bottom-0
%p
@@ -189,110 +187,117 @@
%li Container registry images
%li CI variables
%li Any encrypted tokens
- - if can? current_user, :archive_project, @project
- %hr
- .row.prepend-top-default
- .col-lg-4
- %h4.warning-title.prepend-top-0
- - if @project.archived?
- Unarchive project
- - else
- Archive project
- %p.append-bottom-0
+ %p
+ Once the exported file is ready, you will receive a notification email with a download link.
+ - if @project.export_project_path
+ = link_to 'Download export', download_export_project_path(@project),
+ rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
+ = link_to 'Generate new export', generate_new_export_project_path(@project),
+ method: :post, class: "btn btn-default"
+ - else
+ = link_to 'Export project', export_project_path(@project),
+ method: :post, class: "btn btn-default"
+
+ %section.settings.advanced-settings
+ .settings-header
+ %h4
+ Advanced settings
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Perform advanced options such as housekeeping, exporting, archiveing, renameing, transfering, or removeing your project.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .sub-section
+ %h4 Housekeeping
+ %p
+ Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.
+ = link_to 'Run housekeeping', housekeeping_project_path(@project),
+ method: :post, class: "btn btn-default"
+ - if can? current_user, :archive_project, @project
+ .sub-section
+ %h4.warning-title
+ - if @project.archived?
+ Unarchive project
+ - else
+ Archive project
- if @project.archived?
- Unarchiving the project will mark its repository as active. The project can be committed to.
+ %p
+ Unarchiving the project will mark its repository as active. The project can be committed to.
+ %strong Once active this project shows up in the search and on the dashboard.
+ = link_to 'Unarchive project', unarchive_project_path(@project),
+ data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
+ method: :post, class: "btn btn-success"
- else
- Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
- .col-lg-8
- - if @project.archived?
- %p
- %strong Once active this project shows up in the search and on the dashboard.
- = link_to 'Unarchive project', unarchive_project_path(@project),
- data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
- method: :post, class: "btn btn-success"
- - else
- %p
- %strong Archived projects cannot be committed to!
- = link_to 'Archive project', archive_project_path(@project),
- data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
- method: :post, class: "btn btn-warning"
- %hr
- .row.prepend-top-default
- .col-lg-4
- %h4.prepend-top-0.warning-title
- Rename repository
- .col-lg-8
- = render 'projects/errors'
- = form_for([@project.namespace.becomes(Namespace), @project]) do |f|
- .form-group.project_name_holder
- = f.label :name, class: 'label-light' do
- Project name
- .form-group
- = f.text_field :name, class: "form-control"
- .form-group
- = f.label :path, class: 'label-light' do
- %span Path
- .form-group
- .input-group
- .input-group-addon
- #{URI.join(root_url, @project.namespace.full_path)}/
- = f.text_field :path, class: 'form-control'
- %ul
- %li Be careful. Renaming a project's repository can have unintended side effects.
- %li You will need to update your local repositories to point to the new location.
- - if @project.deployment_services.any?
- %li Your deployment services will be broken, you will need to manually fix the services after renaming.
- = f.submit 'Rename project', class: "btn btn-warning"
- - if can?(current_user, :change_namespace, @project)
- %hr
- .row.prepend-top-default
- .col-lg-4
- %h4.prepend-top-0.danger-title
- Transfer project to new group
- %p.append-bottom-0
- Please select the group you want to transfer this project to in the dropdown to the right.
- .col-lg-8
- = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f|
+ %p
+ Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
+ %strong Archived projects cannot be committed to!
+ = link_to 'Archive project', archive_project_path(@project),
+ data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
+ method: :post, class: "btn btn-warning"
+ .sub-section.rename-respository
+ %h4.warning-title
+ Rename repository
+ %p
+ Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
+ = render 'projects/errors'
+ = form_for([@project.namespace.becomes(Namespace), @project]) do |f|
+ .form-group.project_name_holder
+ = f.label :name, class: 'label-light' do
+ Project name
+ .form-group
+ = f.text_field :name, class: "form-control"
.form-group
- = label_tag :new_namespace_id, nil, class: 'label-light' do
- %span Select a new namespace
+ = f.label :path, class: 'label-light' do
+ %span Path
.form-group
- = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2'
+ .input-group
+ .input-group-addon
+ #{URI.join(root_url, @project.namespace.full_path)}/
+ = f.text_field :path, class: 'form-control'
%ul
- %li Be careful. Changing the project's namespace can have unintended side effects.
- %li You can only transfer the project to namespaces you manage.
+ %li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
- %li Project visibility level will be changed to match namespace rules when transfering to a group.
- = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- - if @project.forked? && can?(current_user, :remove_fork_project, @project)
- %hr
- .row.prepend-top-default.append-bottom-default
- .col-lg-4
- %h4.prepend-top-0.danger-title
- Remove fork relationship
- %p.append-bottom-0
+ - if @project.deployment_services.any?
+ %li Your deployment services will be broken, you will need to manually fix the services after renaming.
+ = f.submit 'Rename project', class: "btn btn-warning"
+ - if can?(current_user, :change_namespace, @project)
+ .sub-section
+ %h4.danger-title
+ Transfer project
+ = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f|
+ .form-group
+ = label_tag :new_namespace_id, nil, class: 'label-light' do
+ %span Select a new namespace
+ .form-group
+ = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2'
+ %ul
+ %li Be careful. Changing the project's namespace can have unintended side effects.
+ %li You can only transfer the project to namespaces you manage.
+ %li You will need to update your local repositories to point to the new location.
+ %li Project visibility level will be changed to match namespace rules when transfering to a group.
+ = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
+ - if @project.forked? && can?(current_user, :remove_fork_project, @project)
+ .sub-section
+ %h4.danger-title
+ Remove fork relationship
%p
This will remove the fork relationship to source project
= succeed "." do
= link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
- .col-lg-8
- = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
- %p
- %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
- = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) }
- - if can?(current_user, :remove_project, @project)
- %hr
- .row.prepend-top-default.append-bottom-default
- .col-lg-4
- %h4.prepend-top-0.danger-title
- Remove project
- %p.append-bottom-0
- Removing the project will delete its repository and all related resources including issues, merge requests etc.
- .col-lg-8
- = form_tag(project_path(@project), method: :delete) do
+ = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
+ %p
+ %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
+ = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) }
+ - if can?(current_user, :remove_project, @project)
+ .sub-section
+ %h4.danger-title
+ Remove project
%p
- %strong Removed projects cannot be restored!
- = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
+ Removing the project will delete its repository and all related resources including issues, merge requests etc.
+ = form_tag(project_path(@project), method: :delete) do
+ %p
+ %strong Removed projects cannot be restored!
+ = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
.save-project-loader.hide
.center
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 7343d6e039c..bd8c38292d6 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -15,7 +15,7 @@
- else
= s_("PipelineSchedules|None")
%td.next-run-cell
- - if pipeline_schedule.active?
+ - if pipeline_schedule.active? && pipeline_schedule.next_run_at
= time_ago_with_tooltip(pipeline_schedule.real_next_run)
- else
= s_("PipelineSchedules|Inactive")
diff --git a/app/views/shared/icons/_container_registry.svg b/app/views/shared/icons/_container_registry.svg
new file mode 100644
index 00000000000..56d62aab670
--- /dev/null
+++ b/app/views/shared/icons/_container_registry.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m16 11.764v-8.764c0-1.657-1.343-3-3-3h-10c-1.657 0-3 1.343-3 3v8.764c.531-.475 1.232-.764 2-.764v-8c0-.552.448-1 1-1h10c.552 0 1 .448 1 1v8c.768 0 1.469.289 2 .764m-14 .236h12c1.105 0 2 .895 2 2 0 1.105-.895 2-2 2h-12c-1.105 0-2-.895-2-2 0-1.105.895-2 2-2m10 1c-.552 0-1 .448-1 1 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-.552-.448-1-1-1"/></svg>
diff --git a/app/views/shared/icons/_spam_logs.svg b/app/views/shared/icons/_spam_logs.svg
new file mode 100644
index 00000000000..80ee0eb3856
--- /dev/null
+++ b/app/views/shared/icons/_spam_logs.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 895fb8247b5..66ac8196f2f 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -67,7 +67,7 @@
.block.issues
.sidebar-collapsed-icon
%strong
- = icon('hashtag', 'aria-hidden': 'true')
+ = custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
Issues
diff --git a/changelogs/unreleased/35659-rename-pipeline.yml b/changelogs/unreleased/35659-rename-pipeline.yml
new file mode 100644
index 00000000000..0fe211868e4
--- /dev/null
+++ b/changelogs/unreleased/35659-rename-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Rename Pipelines tab to CI / CD in new navigation
+merge_request:
+author:
diff --git a/changelogs/unreleased/diff-changed-files-dropdown.yml b/changelogs/unreleased/diff-changed-files-dropdown.yml
new file mode 100644
index 00000000000..2d2a26ffea2
--- /dev/null
+++ b/changelogs/unreleased/diff-changed-files-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Moved diff changed files into a dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml b/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml
new file mode 100644
index 00000000000..07840205b6e
--- /dev/null
+++ b/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml
@@ -0,0 +1,4 @@
+---
+title: Fix pipeline_schedules pages when active schedule has an abnormal state
+merge_request: 13286
+author:
diff --git a/config/application.rb b/config/application.rb
index 1c13cc81270..f7145566262 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -23,13 +23,13 @@ module Gitlab
# https://github.com/rails/rails/blob/v4.2.6/railties/lib/rails/engine.rb#L687
# This is a nice reference article on autoloading/eager loading:
# http://blog.arkency.com/2014/11/dont-forget-about-eager-load-when-extending-autoload
- config.eager_load_paths.push(*%W(#{config.root}/lib
+ config.eager_load_paths.push(*%W[#{config.root}/lib
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services
#{config.root}/app/workers/concerns
#{config.root}/app/services/concerns
- #{config.root}/app/finders/concerns))
+ #{config.root}/app/finders/concerns])
config.generators.templates.push("#{config.root}/generator_templates")
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 9d2e5956029..558c624fe39 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -80,7 +80,7 @@ Install, upgrade, integrate, migrate to GitLab:
| :------------ | :------: | --------------: |
| [Video Tutorial: Idea to Production on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/) | Tutorial | 2017/01/23 |
| [How to Setup a GitLab Instance on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/) | Tutorial | 2016/07/13 |
-| [Get started with OpenShift Origin 3 and GitLab](https://about.gitlab.com/2016/06/28/get-started-with-openshift-origin-3-and-gitlab/) | Tutorial | 2016/06/28 |
+| [Get started with OpenShift Origin 3 and GitLab](openshift_and_gitlab/index.md) | Tutorial | 2016/06/28 |
| [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/) | Tutorial | 2016/04/27 |
## Software development
diff --git a/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png b/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png
new file mode 100644
index 00000000000..fcad4e59ae3
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/add-to-project.png b/doc/articles/openshift_and_gitlab/img/add-to-project.png
new file mode 100644
index 00000000000..bd915a229f6
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/add-to-project.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/create-project-ui.png b/doc/articles/openshift_and_gitlab/img/create-project-ui.png
new file mode 100644
index 00000000000..e72866f252a
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/create-project-ui.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-logs.png b/doc/articles/openshift_and_gitlab/img/gitlab-logs.png
new file mode 100644
index 00000000000..1e24080c7df
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/gitlab-logs.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-overview.png b/doc/articles/openshift_and_gitlab/img/gitlab-overview.png
new file mode 100644
index 00000000000..3c5df0ea101
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/gitlab-overview.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-running.png b/doc/articles/openshift_and_gitlab/img/gitlab-running.png
new file mode 100644
index 00000000000..c7db691cb30
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/gitlab-running.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-scale.png b/doc/articles/openshift_and_gitlab/img/gitlab-scale.png
new file mode 100644
index 00000000000..4903c7d7498
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/gitlab-scale.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-settings.png b/doc/articles/openshift_and_gitlab/img/gitlab-settings.png
new file mode 100644
index 00000000000..db4360ffef0
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/gitlab-settings.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/no-resources.png b/doc/articles/openshift_and_gitlab/img/no-resources.png
new file mode 100644
index 00000000000..480fb766468
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/no-resources.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png b/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png
new file mode 100644
index 00000000000..8b9f85aa341
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/pods-overview.png b/doc/articles/openshift_and_gitlab/img/pods-overview.png
new file mode 100644
index 00000000000..e1cf08bd217
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/pods-overview.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/rc-name.png b/doc/articles/openshift_and_gitlab/img/rc-name.png
new file mode 100644
index 00000000000..889e34adbec
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/rc-name.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/running-pods.png b/doc/articles/openshift_and_gitlab/img/running-pods.png
new file mode 100644
index 00000000000..3fd4e56662f
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/running-pods.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/storage-volumes.png b/doc/articles/openshift_and_gitlab/img/storage-volumes.png
new file mode 100644
index 00000000000..ae1e5381faa
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/storage-volumes.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/img/web-console.png b/doc/articles/openshift_and_gitlab/img/web-console.png
new file mode 100644
index 00000000000..aa1425d4f94
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/img/web-console.png
Binary files differ
diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md
new file mode 100644
index 00000000000..7f76e577efa
--- /dev/null
+++ b/doc/articles/openshift_and_gitlab/index.md
@@ -0,0 +1,510 @@
+# Getting started with OpenShift Origin 3 and GitLab
+
+> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial ||
+> **Level:** intermediary ||
+> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) ||
+> **Publication date:** 2016/06/28
+
+## Introduction
+
+[OpenShift Origin][openshift] is an open source container application
+platform created by [RedHat], based on [kubernetes] and [Docker]. That means
+you can host your own PaaS for free and almost with no hassle.
+
+In this tutorial, we will see how to deploy GitLab in OpenShift using GitLab's
+official Docker image while getting familiar with the web interface and CLI
+tools that will help us achieve our goal.
+
+---
+
+## Prerequisites
+
+OpenShift 3 is not yet deployed on RedHat's offered Online platform ([openshift.com]),
+so in order to test it, we will use an [all-in-one Virtualbox image][vm] that is
+offered by the OpenShift developers and managed by Vagrant. If you haven't done
+already, go ahead and install the following components as they are essential to
+test OpenShift easily:
+
+- [VirtualBox]
+- [Vagrant]
+- [OpenShift Client][oc] (`oc` for short)
+
+It is also important to mention that for the purposes of this tutorial, the
+latest Origin release is used:
+
+- **oc** `v1.3.0` (must be [installed][oc-gh] locally on your computer)
+- **openshift** `v1.3.0` (is pre-installed in the [VM image][vm-new])
+- **kubernetes** `v1.3.0` (is pre-installed in the [VM image][vm-new])
+
+>**Note:**
+If you intend to deploy GitLab on a production OpenShift cluster, there are some
+limitations to bare in mind. Read on the [limitations](#current-limitations)
+section for more information and follow the linked links for the relevant
+discussions.
+
+Now that you have all batteries, let's see how easy it is to test OpenShift
+on your computer.
+
+## Getting familiar with OpenShift Origin
+
+The environment we are about to use is based on CentOS 7 which comes with all
+the tools needed pre-installed: Docker, kubernetes, OpenShift, etcd.
+
+### Test OpenShift using Vagrant
+
+As of this writing, the all-in-one VM is at version 1.3, and that's
+what we will use in this tutorial.
+
+In short:
+
+1. Open a terminal and in a new directory run:
+ ```sh
+ vagrant init openshift/origin-all-in-one
+ ```
+1. This will generate a Vagrantfile based on the all-in-one VM image
+1. In the same directory where you generated the Vagrantfile
+ enter:
+
+ ```sh
+ vagrant up
+ ```
+
+This will download the VirtualBox image and fire up the VM with some preconfigured
+values as you can see in the Vagrantfile. As you may have noticed, you need
+plenty of RAM (5GB in our example), so make sure you have enough.
+
+Now that OpenShift is setup, let's see how the web console looks like.
+
+### Explore the OpenShift web console
+
+Once Vagrant finishes its thing with the VM, you will be presented with a
+message which has some important information. One of them is the IP address
+of the deployed OpenShift platform and in particular <https://10.2.2.2:8443/console/>.
+Open this link with your browser and accept the self-signed certificate in
+order to proceed.
+
+Let's login as admin with username/password `admin/admin`. This is what the
+landing page looks like:
+
+![openshift web console](img/web-console.png)
+
+You can see that a number of [projects] are already created for testing purposes.
+
+If you head over the `openshift-infra` project, a number of services with their
+respective pods are there to explore.
+
+![openshift web console](img/openshift-infra-project.png)
+
+We are not going to explore the whole interface, but if you want to learn about
+the key concepts of OpenShift, read the [core concepts reference][core] in the
+official documentation.
+
+### Explore the OpenShift CLI
+
+OpenShift Client (`oc`), is a powerful CLI tool that talks to the OpenShift API
+and performs pretty much everything you can do from the web UI and much more.
+
+Assuming you have [installed][oc] it, let's explore some of its main
+functionalities.
+
+Let's first see the version of `oc`:
+
+```sh
+$ oc version
+
+oc v1.3.0
+kubernetes v1.3.0+52492b4
+```
+
+With `oc help` you can see the top level arguments you can run with `oc` and
+interact with your cluster, kubernetes, run applications, create projects and
+much more.
+
+Let's login to the all-in-one VM and see how to achieve the same results like
+when we visited the web console earlier. The username/password for the
+administrator user is `admin/admin`. There is also a test user with username/
+password `user/user`, with limited access. Let's login as admin for the moment:
+
+```sh
+$ oc login https://10.2.2.2:8443
+
+Authentication required for https://10.2.2.2:8443 (openshift)
+Username: admin
+Password:
+Login successful.
+
+You have access to the following projects and can switch between them with 'oc project <projectname>':
+
+ * cockpit
+ * default (current)
+ * delete
+ * openshift
+ * openshift-infra
+ * sample
+
+Using project "default".
+```
+
+Switch to the `openshift-infra` project with:
+
+```sh
+oc project openshift-infra
+```
+
+And finally, see its status:
+
+```sh
+oc status
+```
+
+The last command should spit a bunch of information about the statuses of the
+pods and the services, which if you look closely is what we encountered in the
+second image when we explored the web console.
+
+You can always read more about `oc` in the [OpenShift CLI documentation][oc].
+
+### Troubleshooting the all-in-one VM
+
+Using the all-in-one VM gives you the ability to test OpenShift whenever you
+want. That means you get to play with it, shutdown the VM, and pick up where
+you left off.
+
+Sometimes though, you may encounter some issues, like OpenShift not running
+when booting up the VM. The web UI may not responding or you may see issues
+when trying to login with `oc`, like:
+
+```
+The connection to the server 10.2.2.2:8443 was refused - did you specify the right host or port?
+```
+
+In that case, the OpenShift service might not be running, so in order to fix it:
+
+1. SSH into the VM by going to the directory where the Vagrantfile is and then
+ run:
+
+ ```sh
+ vagrant ssh
+ ```
+
+1. Run `systemctl` and verify by the output that the `openshift` service is not
+ running (it will be in red color). If that's the case start the service with:
+
+ ```sh
+ sudo systemctl start openshift
+ ```
+
+1. Verify the service is up with:
+
+ ```sh
+ systemctl status openshift -l
+ ```
+
+Now you will be able to login using `oc` (like we did before) and visit the web
+console.
+
+## Deploy GitLab
+
+Now that you got a taste of what OpenShift looks like, let's deploy GitLab!
+
+### Create a new project
+
+First, we will create a new project to host our application. You can do this
+either by running the CLI client:
+
+```bash
+$ oc new-project gitlab
+```
+
+or by using the web interface:
+
+![Create a new project from the UI](img/create-project-ui.png)
+
+If you used the command line, `oc` automatically uses the new project and you
+can see its status with:
+
+```sh
+$ oc status
+
+In project gitlab on server https://10.2.2.2:8443
+
+You have no services, deployment configs, or build configs.
+Run 'oc new-app' to create an application.
+```
+
+If you visit the web console, you can now see `gitlab` listed in the projects list.
+
+The next step is to import the OpenShift template for GitLab.
+
+### Import the template
+
+The [template][templates] is basically a JSON file which describes a set of
+related object definitions to be created together, as well as a set of
+parameters for those objects.
+
+The template for GitLab resides in the Omnibus GitLab repository under the
+docker directory. Let's download it locally with `wget`:
+
+```bash
+wget https://gitlab.com/gitlab-org/omnibus-gitlab/raw/master/docker/openshift-template.json
+```
+
+And then let's import it in OpenShift:
+
+```bash
+oc create -f openshift-template.json -n openshift
+```
+
+>**Note:**
+The `-n openshift` namespace flag is a trick to make the template available to all
+projects. If you recall from when we created the `gitlab` project, `oc` switched
+to it automatically, and that can be verified by the `oc status` command. If
+you omit the namespace flag, the application will be available only to the
+current project, in our case `gitlab`. The `openshift` namespace is a global
+one that the administrators should use if they want the application to be
+available to all users.
+
+We are now ready to finally deploy GitLab!
+
+### Create a new application
+
+The next step is to use the template we previously imported. Head over to the
+`gitlab` project and hit the **Add to Project** button.
+
+![Add to project](img/add-to-project.png)
+
+This will bring you to the catalog where you can find all the pre-defined
+applications ready to deploy with the click of a button. Search for `gitlab`
+and you will see the previously imported template:
+
+![Add GitLab to project](img/add-gitlab-to-project.png)
+
+Select it, and in the following screen you will be presented with the predefined
+values used with the GitLab template:
+
+![GitLab settings](img/gitlab-settings.png)
+
+Notice at the top that there are three resources to be created with this
+template:
+
+- `gitlab-ce`
+- `gitlab-ce-redis`
+- `gitlab-ce-postgresql`
+
+While PostgreSQL and Redis are bundled in Omnibus GitLab, the template is using
+separate images as you can see from [this line][line] in the template.
+
+The predefined values have been calculated for the purposes of testing out
+GitLab in the all-in-one VM. You don't need to change anything here, hit
+**Create** to start the deployment.
+
+If you are deploying to production you will want to change the **GitLab instance
+hostname** and use greater values for the volume sizes. If you don't provide a
+password for PostgreSQL, it will be created automatically.
+
+>**Note:**
+The `gitlab.apps.10.2.2.2.xip.io` hostname that is used by default will
+resolve to the host with IP `10.2.2.2` which is the IP our VM uses. It is a
+trick to have distinct FQDNs pointing to services that are on our local network.
+Read more on how this works in <http://xip.io>.
+
+Now that we configured this, let's see how to manage and scale GitLab.
+
+## Manage and scale GitLab
+
+Setting up GitLab for the first time might take a while depending on your
+internet connection and the resources you have attached to the all-in-one VM.
+GitLab's docker image is quite big (~500MB), so you'll have to wait until
+it's downloaded and configured before you use it.
+
+### Watch while GitLab gets deployed
+
+Navigate to the `gitlab` project at **Overview**. You can notice that the
+deployment is in progress by the orange color. The Docker images are being
+downloaded and soon they will be up and running.
+
+![GitLab overview](img/gitlab-overview.png)
+
+Switch to the **Browse > Pods** and you will eventually see all 3 pods in a
+running status. Remember the 3 resources that were to be created when we first
+created the GitLab app? This is where you can see them in action.
+
+![Running pods](img/running-pods.png)
+
+You can see GitLab being reconfigured by taking look at the logs in realtime.
+Click on `gitlab-ce-2-j7ioe` (your ID will be different) and go to the **Logs**
+tab.
+
+![GitLab logs](img/gitlab-logs.png)
+
+At a point you should see a _**gitlab Reconfigured!**_ message in the logs.
+Navigate back to the **Overview** and hopefully all pods will be up and running.
+
+![GitLab running](img/gitlab-running.png)
+
+Congratulations! You can now navigate to your new shinny GitLab instance by
+visiting <http://gitlab.apps.10.2.2.2.xip.io> where you will be asked to
+change the root user password. Login using `root` as username and providing the
+password you just set, and start using GitLab!
+
+### Scale GitLab with the push of a button
+
+If you reach to a point where your GitLab instance could benefit from a boost
+of resources, you'd be happy to know that you can scale up with the push of a
+button.
+
+In the **Overview** page just click the up arrow button in the pod where
+GitLab is. The change is instant and you can see the number of [replicas] now
+running scaled to 2.
+
+![GitLab scale](img/gitlab-scale.png)
+
+Upping the GitLab pods is actually like adding new application servers to your
+cluster. You can see how that would work if you didn't use GitLab with
+OpenShift by following the [HA documentation][ha] for the application servers.
+
+Bare in mind that you may need more resources (CPU, RAM, disk space) when you
+scale up. If a pod is in pending state for too long, you can navigate to
+**Browse > Events** and see the reason and message of the state.
+
+![No resources](img/no-resources.png)
+
+### Scale GitLab using the `oc` CLI
+
+Using `oc` is super easy to scale up the replicas of a pod. You may want to
+skim through the [basic CLI operations][basic-cli] to get a taste how the CLI
+commands are used. Pay extra attention to the object types as we will use some
+of them and their abbreviated versions below.
+
+In order to scale up, we need to find out the name of the replication controller.
+Let's see how to do that using the following steps.
+
+1. Make sure you are in the `gitlab` project:
+
+ ```sh
+ oc project gitlab
+ ```
+
+1. See what services are used for this project:
+
+ ```sh
+ oc get svc
+ ```
+
+ The output will be similar to:
+
+ ```
+ NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+ gitlab-ce 172.30.243.177 <none> 22/TCP,80/TCP 5d
+ gitlab-ce-postgresql 172.30.116.75 <none> 5432/TCP 5d
+ gitlab-ce-redis 172.30.105.88 <none> 6379/TCP 5d
+ ```
+
+1. We need to see the replication controllers of the `gitlab-ce` service.
+ Get a detailed view of the current ones:
+
+ ```sh
+ oc describe rc gitlab-ce
+ ```
+
+ This will return a large detailed list of the current replication controllers.
+ Search for the name of the GitLab controller, usually `gitlab-ce-1` or if
+ that failed at some point and you spawned another one, it will be named
+ `gitlab-ce-2`.
+
+1. Scale GitLab using the previous information:
+
+ ```sh
+ oc scale --replicas=2 replicationcontrollers gitlab-ce-2
+ ```
+
+1. Get the new replicas number to make sure scaling worked:
+
+ ```sh
+ oc get rc gitlab-ce-2
+ ```
+
+ which will return something like:
+
+ ```
+ NAME DESIRED CURRENT AGE
+ gitlab-ce-2 2 2 5d
+ ```
+
+And that's it! We successfully scaled the replicas to 2 using the CLI.
+
+As always, you can find the name of the controller using the web console. Just
+click on the service you are interested in and you will see the details in the
+right sidebar.
+
+![Replication controller name](img/rc-name.png)
+
+### Autoscaling GitLab
+
+In case you were wondering whether there is an option to autoscale a pod based
+on the resources of your server, the answer is yes, of course there is.
+
+We will not expand on this matter, but feel free to read the documentation on
+OpenShift's website about [autoscaling].
+
+## Current limitations
+
+As stated in the [all-in-one VM][vm] page:
+
+> By default, OpenShift will not allow a container to run as root or even a
+non-random container assigned userid. Most Docker images in the Dockerhub do not
+follow this best practice and instead run as root.
+
+The all-in-one VM we are using has this security turned off so it will not
+bother us. In any case, it is something to keep in mind when deploying GitLab
+on a production cluster.
+
+In order to deploy GitLab on a production cluster, you will need to assign the
+GitLab service account to the `anyuid` Security Context.
+
+1. Edit the Security Context:
+ ```sh
+ oc edit scc anyuid
+ ```
+
+1. Add `system:serviceaccount:<project>:gitlab-ce-user` to the `users` section.
+ If you changed the Application Name from the default the user will
+ will be `<app-name>-user` instead of `gitlab-ce-user`
+
+1. Save and exit the editor
+
+## Conclusion
+
+By now, you should have an understanding of the basic OpenShift Origin concepts
+and a sense of how things work using the web console or the CLI.
+
+GitLab was hard to install in previous versions of OpenShift,
+but now that belongs to the past. Upload a template, create a project, add an
+application and you are done. You are ready to login to your new GitLab instance.
+
+And remember that in this tutorial we just scratched the surface of what Origin
+is capable of. As always, you can refer to the detailed
+[documentation][openshift-docs] to learn more about deploying your own OpenShift
+PaaS and managing your applications with the ease of containers.
+
+[RedHat]: https://www.redhat.com/en "RedHat website"
+[openshift]: https://www.openshift.org "OpenShift Origin website"
+[vm]: https://www.openshift.org/vm/ "OpenShift All-in-one VM"
+[vm-new]: https://atlas.hashicorp.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Atlas"
+[template]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/docker/openshift-template.json "OpenShift template for GitLab"
+[openshift.com]: https://openshift.com "OpenShift Online"
+[kubernetes]: http://kubernetes.io/ "Kubernetes website"
+[Docker]: https://www.docker.com "Docker website"
+[oc]: https://docs.openshift.org/latest/cli_reference/get_started_cli.html "Documentation - oc CLI documentation"
+[VirtualBox]: https://www.virtualbox.org/wiki/Downloads "VirtualBox downloads"
+[Vagrant]: https://www.vagrantup.com/downloads.html "Vagrant downloads"
+[projects]: https://docs.openshift.org/latest/dev_guide/projects.html "Documentation - Projects overview"
+[core]: https://docs.openshift.org/latest/architecture/core_concepts/index.html "Documentation - Core concepts of OpenShift Origin"
+[templates]: https://docs.openshift.org/latest/architecture/core_concepts/templates.html "Documentation - OpenShift templates"
+[old-post]: https://blog.openshift.com/deploy-gitlab-openshift/ "Old post - Deploy GitLab on OpenShift"
+[line]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/658c065c8d022ce858dd63eaeeadb0b2ddc8deea/docker/openshift-template.json#L239 "GitLab - OpenShift template"
+[oc-gh]: https://github.com/openshift/origin/releases/tag/v1.3.0 "Openshift 1.3.0 release on GitHub"
+[ha]: http://docs.gitlab.com/ce/administration/high_availability/gitlab.html "Documentation - GitLab High Availability"
+[replicas]: https://docs.openshift.org/latest/architecture/core_concepts/deployments.html#replication-controllers "Documentation - Replication controller"
+[autoscaling]: https://docs.openshift.org/latest/dev_guide/pod_autoscaling.html "Documentation - Autoscale"
+[basic-cli]: https://docs.openshift.org/latest/cli_reference/basic_cli_operations.html "Documentation - Basic CLI operations"
+[openshift-docs]: https://docs.openshift.org "OpenShift documentation"
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 149a0159680..6ade3231fac 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -11,7 +11,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
#### ESlint
-1. **Never** disable eslint rules unless you have a good reason.
+1. **Never** disable eslint rules unless you have a good reason.
You may see a lot of legacy files with `/* eslint-disable some-rule, some-other-rule */`
at the top, but legacy files are a special case. Any time you develop a new feature or
refactor an existing one, you should abide by the eslint rules.
@@ -100,26 +100,44 @@ followed by any global declarations, then a blank newline prior to any imports o
export default Foo;
```
-1. Relative paths: Unless you are writing a test, always reference other scripts using
-relative paths instead of `~`
- * In **app/assets/javascripts**:
+1. Relative paths: when importing a module in the same directory, a child
+directory, or an immediate parent directory prefer relative paths. When
+importing a module which is two or more levels up, prefer either `~/` or `ee/`
+.
- ```javascript
- // bad
- import Foo from '~/foo'
+In **app/assets/javascripts/my-feature/subdir**:
- // good
- import Foo from '../foo';
- ```
- * In **spec/javascripts**:
+``` javascript
+// bad
+import Foo from '~/my-feature/foo';
+import Bar from '~/my-feature/subdir/bar';
+import Bin from '~/my-feature/subdir/lib/bin';
- ```javascript
- // bad
- import Foo from '../../app/assets/javascripts/foo'
+// good
+import Foo from '../foo';
+import Bar from './bar';
+import Bin from './lib/bin';
+```
- // good
- import Foo from '~/foo';
- ```
+In **spec/javascripts**:
+
+``` javascript
+// bad
+import Foo from '../../app/assets/javascripts/my-feature/foo';
+
+// good
+import Foo from '~/my-feature/foo';
+```
+
+When referencing an **EE component**:
+
+``` javascript
+// bad
+import Foo from '../../../../../ee/app/assets/javascripts/my-feature/ee-foo';
+
+// good
+import Foo from 'ee/my-feature/foo';
+```
1. Avoid using IIFE. Although we have a lot of examples of files which wrap their
contents in IIFEs (immediately-invoked function expressions),
@@ -465,7 +483,7 @@ A forEach will cause side effects, it will be mutating the array being iterated.
#### Vue and Boostrap
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
- ```javascript
+ ```javascript
// bad
<span
class="has-tooltip"
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 8ded607bcab..22aedb5403e 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -168,8 +168,10 @@ are out of date, so we'll need to install through the following commands:
curl --location https://deb.nodesource.com/setup_7.x | sudo bash -
sudo apt-get install -y nodejs
- # install yarn
- curl --location https://yarnpkg.com/install.sh | bash -
+ curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+ sudo apt-get update
+ sudo apt-get install yarn
Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps.
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 6308317b1f2..4d3ababaa41 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies.
```bash
-curl --location https://yarnpkg.com/install.sh | bash -
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
```
More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index 2d597894517..2b4a7bed27f 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies.
```bash
-curl --location https://yarnpkg.com/install.sh | bash -
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
```
More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index 225a4dcc924..f38547bba1a 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies.
```bash
-curl --location https://yarnpkg.com/install.sh | bash -
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
```
More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md
index 910539acc70..373f43eb3bb 100644
--- a/doc/update/9.2-to-9.3.md
+++ b/doc/update/9.2-to-9.3.md
@@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies.
```bash
-curl --location https://yarnpkg.com/install.sh | bash -
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
```
More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md
index 9540c36e7d0..b167f0737aa 100644
--- a/doc/update/9.3-to-9.4.md
+++ b/doc/update/9.3-to-9.4.md
@@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
JavaScript dependencies.
```bash
-curl --location https://yarnpkg.com/install.sh | bash -
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
```
More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 170e2f16c80..0a89c1baf20 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -10,7 +10,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I save project' do
- click_button 'Save changes'
+ page.within '.general-settings' do
+ click_button 'Save changes'
+ end
end
step 'I should see project with new settings' do
@@ -31,7 +33,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps
:project_avatar,
File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
)
- click_button 'Save changes'
+ page.within '.general-settings' do
+ click_button 'Save changes'
+ end
@project.reload
end
@@ -50,7 +54,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps
:project_avatar,
File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
)
- click_button 'Save changes'
+ page.within '.general-settings' do
+ click_button 'Save changes'
+ end
@project.reload
end
@@ -69,7 +75,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'change project default branch' do
select 'fix', from: 'project_default_branch'
- click_button 'Save changes'
+ page.within '.general-settings' do
+ click_button 'Save changes'
+ end
end
step 'I should see project default branch changed' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index f6edf93b311..00f7cded2ae 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -103,7 +103,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_project_path(@project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Sharing & Permissions")
+ expect(page).to have_content("Sharing and permissions")
end
def current_project
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index c7801cb5baf..ad08c0905e2 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -132,6 +132,8 @@ module Banzai
end
def self.cacheless_render(text, context = {})
+ return text.to_s unless text.present?
+
Gitlab::Metrics.measure(:banzai_cacheless_render) do
result = render_result(text, context)
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb
index b1eb1a6cef1..ae65653645b 100644
--- a/lib/declarative_policy.rb
+++ b/lib/declarative_policy.rb
@@ -28,7 +28,13 @@ module DeclarativePolicy
subject = find_delegate(subject)
- class_for_class(subject.class)
+ policy_class = class_for_class(subject.class)
+ raise "no policy for #{subject.class.name}" if policy_class.nil?
+ policy_class
+ end
+
+ def has_policy?(subject)
+ !class_for_class(subject.class).nil?
end
private
@@ -51,9 +57,7 @@ module DeclarativePolicy
end
end
- policy_class = subject_class.instance_variable_get(CLASS_CACHE_IVAR)
- raise "no policy for #{subject.class.name}" if policy_class.nil?
- policy_class
+ subject_class.instance_variable_get(CLASS_CACHE_IVAR)
end
def compute_class_for_class(subject_class)
@@ -71,6 +75,8 @@ module DeclarativePolicy
nil
end
end
+
+ nil
end
def find_delegate(subject)
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 0deaab01b5b..8dbe25e55f6 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -1,5 +1,3 @@
-# Gitaly note: JV: needs 1 RPC for #load_blame.
-
module Gitlab
module Git
class Blame
@@ -26,15 +24,29 @@ module Gitlab
private
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/376
def load_blame
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
- # Read in binary mode to ensure ASCII-8BIT
- raw_output = IO.popen(cmd, 'rb') {|io| io.read }
+ raw_output = @repo.gitaly_migrate(:blame) do |is_enabled|
+ if is_enabled
+ load_blame_by_gitaly
+ else
+ load_blame_by_shelling_out
+ end
+ end
+
output = encode_utf8(raw_output)
process_raw_blame output
end
+ def load_blame_by_gitaly
+ @repo.gitaly_commit_client.raw_blame(@sha, @path)
+ end
+
+ def load_blame_by_shelling_out
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
+ # Read in binary mode to ensure ASCII-8BIT
+ IO.popen(cmd, 'rb') {|io| io.read }
+ end
+
def process_raw_blame(output)
lines, final = [], []
info, commits = {}, {}
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index ffe2c8b91bb..734aed8fbc1 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -683,6 +683,14 @@ module Gitlab
@gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self)
end
+ def gitaly_migrate(method, &block)
+ Gitlab::GitalyClient.migrate(method, &block)
+ rescue GRPC::NotFound => e
+ raise NoRepository.new(e)
+ rescue GRPC::BadStatus => e
+ raise CommandError.new(e)
+ end
+
private
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
@@ -998,6 +1006,11 @@ module Gitlab
end.sort_by(&:name)
end
+ def last_commit_for_path_by_rugged(sha, path)
+ sha = last_commit_id_for_path(sha, path)
+ commit(sha)
+ end
+
def tags_from_gitaly
gitaly_ref_client.tags
end
@@ -1017,14 +1030,6 @@ module Gitlab
raw_output.to_i
end
-
- def gitaly_migrate(method, &block)
- Gitlab::GitalyClient.migrate(method, &block)
- rescue GRPC::NotFound => e
- raise NoRepository.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
- end
end
end
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index b1424a458e9..ac6817e6d0e 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -97,6 +97,20 @@ module Gitlab
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request).count
end
+ def last_commit_for_path(revision, path)
+ request = Gitaly::LastCommitForPathRequest.new(
+ repository: @gitaly_repo,
+ revision: revision.force_encoding(Encoding::ASCII_8BIT),
+ path: path.to_s.force_encoding(Encoding::ASCII_8BIT)
+ )
+
+ gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request).commit
+ return unless gitaly_commit
+
+ commit = GitalyClient::Commit.new(@repository, gitaly_commit)
+ Gitlab::Git::Commit.new(commit)
+ end
+
def between(from, to)
request = Gitaly::CommitsBetweenRequest.new(
repository: @gitaly_repo,
@@ -128,6 +142,17 @@ module Gitlab
response.languages.map { |l| { value: l.share.round(2), label: l.name, color: l.color, highlight: l.color } }
end
+ def raw_blame(revision, path)
+ request = Gitaly::RawBlameRequest.new(
+ repository: @gitaly_repo,
+ revision: revision,
+ path: path
+ )
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request)
+ response.reduce("") { |memo, msg| memo << msg.data }
+ end
+
private
def commit_diff_request_params(commit, options = {})
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 4044202eb6b..24691629063 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -20,21 +20,25 @@ describe 'Edit Project Settings' do
visit edit_project_path(project)
select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level"
- click_button 'Save changes'
+ page.within('.sharing-permissions') do
+ click_button 'Save changes'
+ end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level"
- click_button 'Save changes'
+ page.within('.sharing-permissions') do
+ click_button 'Save changes'
+ end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level"
- click_button 'Save changes'
+ page.within('.sharing-permissions') do
+ click_button 'Save changes'
+ end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
-
- sleep 0.1
end
end
end
@@ -174,7 +178,11 @@ describe 'Edit Project Settings' do
it "disables repository related features" do
select "Disabled", from: "project_project_feature_attributes_repository_access_level"
- expect(find(".edit-project")).to have_selector("select.disabled", count: 2)
+ page.within('.sharing-permissions') do
+ click_button "Save changes"
+ end
+
+ expect(find(".sharing-permissions")).to have_selector("select.disabled", count: 2)
end
it "shows empty features project homepage" do
@@ -182,7 +190,9 @@ describe 'Edit Project Settings' do
select "Disabled", from: "project_project_feature_attributes_issues_access_level"
select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
- click_button "Save changes"
+ page.within('.sharing-permissions') do
+ click_button "Save changes"
+ end
wait_for_requests
visit project_path(project)
@@ -195,7 +205,9 @@ describe 'Edit Project Settings' do
select "Disabled", from: "project_project_feature_attributes_issues_access_level"
select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
- click_button "Save changes"
+ page.within('.sharing-permissions') do
+ click_button "Save changes"
+ end
wait_for_requests
visit activity_project_path(project)
@@ -236,7 +248,9 @@ describe 'Edit Project Settings' do
end
def save_changes_and_check_activity_tab
- click_button "Save changes"
+ page.within('.sharing-permissions') do
+ click_button "Save changes"
+ end
wait_for_requests
visit activity_project_path(project)
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 605415d2af4..24b335a7068 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -219,6 +219,25 @@ feature 'Pipeline Schedules', :js do
end
end
end
+
+ context 'when active is true and next_run_at is NULL' do
+ background do
+ create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
+ pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil
+ end
+ end
+
+ scenario 'user edit and recover the problematic pipeline schedule' do
+ visit_pipelines_schedules
+ find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
+ fill_in 'schedule_cron', with: '* 1 2 3 4'
+ click_button 'Save pipeline schedule'
+
+ page.within('.pipeline-schedule-table-row:nth-child(1)') do
+ expect(page).to have_css(".next-run-cell time")
+ end
+ end
+ end
end
context 'logged in as non-member' do
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 59603310f51..7d4ec2b4e68 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -14,7 +14,9 @@ describe 'Edit Project Settings' do
it 'shows errors for invalid project name' do
visit edit_project_path(project)
fill_in 'project_name_edit', with: 'foo&bar'
- click_button 'Save changes'
+ page.within('.general-settings') do
+ click_button 'Save changes'
+ end
expect(page).to have_field 'project_name_edit', with: 'foo&bar'
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_button 'Save changes'
@@ -23,7 +25,9 @@ describe 'Edit Project Settings' do
it 'shows a successful notice when the project is updated' do
visit edit_project_path(project)
fill_in 'project_name_edit', with: 'hello world'
- click_button 'Save changes'
+ page.within('.general-settings') do
+ click_button 'Save changes'
+ end
expect(page).to have_content "Project 'hello world' was successfully updated."
end
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index a011fab2333..104ce08d9f3 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -20,6 +20,9 @@ feature 'Project settings > Merge Requests', :js do
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level"
+ within('.sharing-permissions-form') do
+ click_on('Save changes')
+ end
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
@@ -37,6 +40,9 @@ feature 'Project settings > Merge Requests', :js do
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level"
+ within('.sharing-permissions-form') do
+ click_on('Save changes')
+ end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
@@ -55,6 +61,9 @@ feature 'Project settings > Merge Requests', :js do
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level"
+ within('.sharing-permissions-form') do
+ click_on('Save changes')
+ end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
@@ -73,7 +82,9 @@ feature 'Project settings > Merge Requests', :js do
scenario 'when unchecked sets :printing_merge_request_link_enabled to false' do
uncheck('project_printing_merge_request_link_enabled')
- click_on('Save')
+ within('.merge-request-settings-form') do
+ click_on('Save changes')
+ end
# Wait for save to complete and page to reload
checkbox = find_field('project_printing_merge_request_link_enabled')
diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/abuse_reports_spec.js
index 069d857eab6..13cab81dd60 100644
--- a/spec/javascripts/abuse_reports_spec.js
+++ b/spec/javascripts/abuse_reports_spec.js
@@ -6,10 +6,10 @@ import '~/abuse_reports';
const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
const MAX_MESSAGE_LENGTH = 500;
- let messages;
+ let $messages;
const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
- const findMessage = searchText => messages.filter(
+ const findMessage = searchText => $messages.filter(
(index, element) => element.innerText.indexOf(searchText) > -1,
).first();
@@ -18,7 +18,7 @@ import '~/abuse_reports';
beforeEach(function () {
loadFixtures(FIXTURE);
this.abuseReports = new global.AbuseReports();
- messages = $('.abuse-reports .message');
+ $messages = $('.abuse-reports .message');
});
it('should truncate long messages', () => {
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
index 1518ae68b0d..46e072a8ebb 100644
--- a/spec/javascripts/ajax_loading_spinner_spec.js
+++ b/spec/javascripts/ajax_loading_spinner_spec.js
@@ -1,4 +1,3 @@
-import '~/extensions/array';
import 'jquery';
import 'jquery-ujs';
import '~/ajax_loading_spinner';
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
deleted file mode 100644
index b1b81b4efc2..00000000000
--- a/spec/javascripts/extensions/array_spec.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/* eslint-disable space-before-function-paren, no-var */
-
-import '~/extensions/array';
-
-(function() {
- describe('Array extensions', function() {
- describe('first', function() {
- return it('returns the first item', function() {
- var arr;
- arr = [0, 1, 2, 3, 4, 5];
- return expect(arr.first()).toBe(0);
- });
- });
- describe('last', function() {
- return it('returns the last item', function() {
- var arr;
- arr = [0, 1, 2, 3, 4, 5];
- return expect(arr.last()).toBe(5);
- });
- });
- });
-}).call(window);
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index 244f170ab7a..b1b3d43f241 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -1,4 +1,3 @@
-import '~/extensions/array';
import '~/filtered_search/dropdown_utils';
import '~/filtered_search/filtered_search_tokenizer';
import '~/filtered_search/filtered_search_dropdown_manager';
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
index 9e2076dc383..5c7e9115aac 100644
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -1,4 +1,3 @@
-import '~/extensions/array';
import '~/filtered_search/filtered_search_visual_tokens';
import '~/filtered_search/filtered_search_tokenizer';
import '~/filtered_search/filtered_search_dropdown_manager';
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
index 1a7631994b4..69b424c3af5 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
@@ -1,4 +1,3 @@
-import '~/extensions/array';
import '~/filtered_search/filtered_search_token_keys';
describe('Filtered Search Token Keys', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
index e4a15c83c23..585bea9b499 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
@@ -1,4 +1,3 @@
-import '~/extensions/array';
import '~/filtered_search/filtered_search_token_keys';
import '~/filtered_search/filtered_search_tokenizer';
diff --git a/spec/javascripts/lib/utils/sticky_spec.js b/spec/javascripts/lib/utils/sticky_spec.js
new file mode 100644
index 00000000000..c3ee3ef9825
--- /dev/null
+++ b/spec/javascripts/lib/utils/sticky_spec.js
@@ -0,0 +1,52 @@
+import { isSticky } from '~/lib/utils/sticky';
+
+describe('sticky', () => {
+ const el = {
+ offsetTop: 0,
+ classList: {},
+ };
+
+ beforeEach(() => {
+ el.offsetTop = 0;
+ el.classList.add = jasmine.createSpy('spy');
+ el.classList.remove = jasmine.createSpy('spy');
+ });
+
+ describe('classList.remove', () => {
+ it('does not call classList.remove when stuck', () => {
+ isSticky(el, 0, 0);
+
+ expect(
+ el.classList.remove,
+ ).not.toHaveBeenCalled();
+ });
+
+ it('calls classList.remove when not stuck', () => {
+ el.offsetTop = 10;
+ isSticky(el, 0, 0);
+
+ expect(
+ el.classList.remove,
+ ).toHaveBeenCalledWith('is-stuck');
+ });
+ });
+
+ describe('classList.add', () => {
+ it('calls classList.add when stuck', () => {
+ isSticky(el, 0, 0);
+
+ expect(
+ el.classList.add,
+ ).toHaveBeenCalledWith('is-stuck');
+ });
+
+ it('does not call classList.add when not stuck', () => {
+ el.offsetTop = 10;
+ isSticky(el, 0, 0);
+
+ expect(
+ el.classList.add,
+ ).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 66c016d14b3..800c245b130 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -7,63 +7,73 @@ describe Gitlab::Git::Blame, seed_helper: true do
Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md")
end
- context "each count" do
- it do
- data = []
- blame.each do |commit, line|
- data << {
- commit: commit,
- line: line
- }
- end
-
- expect(data.size).to eq(95)
- expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
- expect(data.first[:line]).to eq("# Contribute to GitLab")
- expect(data.first[:line]).to be_utf8
- end
- end
+ shared_examples 'blaming a file' do
+ context "each count" do
+ it do
+ data = []
+ blame.each do |commit, line|
+ data << {
+ commit: commit,
+ line: line
+ }
+ end
- context "ISO-8859 encoding" do
- let(:blame) do
- Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
+ expect(data.size).to eq(95)
+ expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
+ expect(data.first[:line]).to eq("# Contribute to GitLab")
+ expect(data.first[:line]).to be_utf8
+ end
end
- it 'converts to UTF-8' do
- data = []
- blame.each do |commit, line|
- data << {
- commit: commit,
- line: line
- }
+ context "ISO-8859 encoding" do
+ let(:blame) do
+ Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
end
- expect(data.size).to eq(1)
- expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
- expect(data.first[:line]).to eq("Ä ü")
- expect(data.first[:line]).to be_utf8
- end
- end
+ it 'converts to UTF-8' do
+ data = []
+ blame.each do |commit, line|
+ data << {
+ commit: commit,
+ line: line
+ }
+ end
- context "unknown encoding" do
- let(:blame) do
- Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
+ expect(data.size).to eq(1)
+ expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
+ expect(data.first[:line]).to eq("Ä ü")
+ expect(data.first[:line]).to be_utf8
+ end
end
- it 'converts to UTF-8' do
- expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil)
- data = []
- blame.each do |commit, line|
- data << {
+ context "unknown encoding" do
+ let(:blame) do
+ Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
+ end
+
+ it 'converts to UTF-8' do
+ expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil)
+ data = []
+ blame.each do |commit, line|
+ data << {
commit: commit,
line: line
- }
- end
+ }
+ end
- expect(data.size).to eq(1)
- expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
- expect(data.first[:line]).to eq(" ")
- expect(data.first[:line]).to be_utf8
+ expect(data.size).to eq(1)
+ expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
+ expect(data.first[:line]).to eq(" ")
+ expect(data.first[:line]).to be_utf8
+ end
end
end
+
+ context 'when Gitaly blame feature is enabled' do
+ it_behaves_like 'blaming a file'
+ end
+
+ context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'blaming a file'
+ end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 204a00778a7..63bf131cfc5 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -153,6 +153,15 @@ describe JiraService do
expect(WebMock).not_to have_requested(:post, @remote_link_url)
end
+ it "does not send comment or remote links to issues with unknown resolution" do
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false)
+
+ @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ expect(WebMock).not_to have_requested(:post, @comment_url)
+ expect(WebMock).not_to have_requested(:post, @remote_link_url)
+ end
+
it "references the GitLab commit/merge request" do
stub_config_setting(base_url: custom_base_url)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 764f548be45..f876baaa805 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -139,24 +139,44 @@ describe Repository do
end
describe '#last_commit_for_path' do
- subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
+ shared_examples 'getting last commit for path' do
+ subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
- it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
+ it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
+ end
+
+ context 'when Gitaly feature last_commit_for_path is enabled' do
+ it_behaves_like 'getting last commit for path'
+ end
+
+ context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'getting last commit for path'
+ end
end
describe '#last_commit_id_for_path' do
- subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') }
+ shared_examples 'getting last commit ID for path' do
+ subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') }
- it "returns last commit id for a given path" do
- is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8')
+ it "returns last commit id for a given path" do
+ is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8')
+ end
+
+ it "caches last commit id for a given path" do
+ cache = repository.send(:cache)
+ key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}"
+
+ expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
+ is_expected.to eq('c1acaa5')
+ end
end
- it "caches last commit id for a given path" do
- cache = repository.send(:cache)
- key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}"
+ context 'when Gitaly feature last_commit_for_path is enabled' do
+ it_behaves_like 'getting last commit ID for path'
+ end
- expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
- is_expected.to eq('c1acaa5')
+ context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'getting last commit ID for path'
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index d8dfe71342d..9eda6836ded 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -293,6 +293,26 @@ describe API::MergeRequests do
expect(json_response.length).to eq(0)
end
+ it 'returns an array of labeled merge requests that are merged for a milestone' do
+ bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project)
+
+ mr1 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone)
+ mr2 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1)
+ mr3 = create(:merge_request, state: "closed", source_project: project, target_project: project, milestone: milestone1)
+ _mr = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1)
+
+ create(:label_link, label: bug_label, target: mr1)
+ create(:label_link, label: bug_label, target: mr2)
+ create(:label_link, label: bug_label, target: mr3)
+
+ get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(mr2.id)
+ end
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
diff --git a/spec/services/notification_recipient_service_spec.rb b/spec/services/notification_recipient_service_spec.rb
deleted file mode 100644
index 0eb0771fd29..00000000000
--- a/spec/services/notification_recipient_service_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe NotificationRecipientService do
- set(:user) { create(:user) }
- set(:project) { create(:project, :public) }
- set(:issue) { create(:issue, project: project) }
-
- set(:watcher) do
- watcher = create(:user)
- setting = watcher.notification_settings_for(project)
- setting.level = :watch
- setting.save
-
- watcher
- end
-
- subject { described_class.new(project) }
-
- describe '#build_recipients' do
- it 'does not modify the participants of the target' do
- expect { subject.build_recipients(issue, user, action: :new_issue) }
- .not_to change { issue.participants(user) }
- end
- end
-
- describe '#build_new_note_recipients' do
- set(:note) { create(:note_on_issue, noteable: issue, project: project) }
-
- it 'does not modify the participants of the target' do
- expect { subject.build_new_note_recipients(note) }
- .not_to change { note.noteable.participants(note.author) }
- end
- end
-end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 5354591642b..6af5c79135d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -306,6 +306,11 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
+
+ # make sure these users can read the project snippet!
+ project.add_guest(@u_guest_watcher)
+ project.add_guest(@u_guest_custom)
+
note.project.add_master(note.author)
reset_delivered_emails!
end
diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb
index 67599f77adb..6591d56e473 100644
--- a/spec/support/api/schema_matcher.rb
+++ b/spec/support/api/schema_matcher.rb
@@ -1,23 +1,25 @@
-def schema_path(schema)
- schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas"
- "#{schema_directory}/#{schema}.json"
+module SchemaPath
+ def self.expand(schema, dir = '')
+ Rails.root.join('spec', dir, "fixtures/api/schemas/#{schema}.json").to_s
+ end
end
-RSpec::Matchers.define :match_response_schema do |schema, **options|
+RSpec::Matchers.define :match_response_schema do |schema, dir: '', **options|
match do |response|
- @errors = JSON::Validator.fully_validate(schema_path(schema), response.body, options)
+ @errors = JSON::Validator.fully_validate(
+ SchemaPath.expand(schema, dir), response.body, options)
@errors.empty?
end
failure_message do |response|
- "didn't match the schema defined by #{schema_path(schema)}" \
+ "didn't match the schema defined by #{SchemaPath.expand(schema, dir)}" \
" The validation errors were:\n#{@errors.join("\n")}"
end
end
-RSpec::Matchers.define :match_schema do |schema, **options|
+RSpec::Matchers.define :match_schema do |schema, dir: '', **options|
match do |data|
- JSON::Validator.validate!(schema_path(schema), data, options)
+ JSON::Validator.validate!(SchemaPath.expand(schema, dir), data, options)
end
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index c714d1b08a6..3e117530151 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -1,3 +1,5 @@
+require_relative 'devise_helpers'
+
module LoginHelpers
include DeviseHelpers
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index bed78928f14..c1298ed9cae 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -229,7 +229,6 @@ module TestEnv
# Otherwise they'd be created by the first test, often timing out and
# causing a transient test failure
def eager_load_driver_server
- return unless ENV['CI']
return unless defined?(Capybara)
puts "Starting the Capybara driver server..."