diff options
102 files changed, 1792 insertions, 832 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 @@ -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 - → - = 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 & 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/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/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 Binary files differnew file mode 100644 index 00000000000..fcad4e59ae3 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png diff --git a/doc/articles/openshift_and_gitlab/img/add-to-project.png b/doc/articles/openshift_and_gitlab/img/add-to-project.png Binary files differnew file mode 100644 index 00000000000..bd915a229f6 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/add-to-project.png diff --git a/doc/articles/openshift_and_gitlab/img/create-project-ui.png b/doc/articles/openshift_and_gitlab/img/create-project-ui.png Binary files differnew file mode 100644 index 00000000000..e72866f252a --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/create-project-ui.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-logs.png b/doc/articles/openshift_and_gitlab/img/gitlab-logs.png Binary files differnew file mode 100644 index 00000000000..1e24080c7df --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/gitlab-logs.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-overview.png b/doc/articles/openshift_and_gitlab/img/gitlab-overview.png Binary files differnew file mode 100644 index 00000000000..3c5df0ea101 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/gitlab-overview.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-running.png b/doc/articles/openshift_and_gitlab/img/gitlab-running.png Binary files differnew file mode 100644 index 00000000000..c7db691cb30 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/gitlab-running.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-scale.png b/doc/articles/openshift_and_gitlab/img/gitlab-scale.png Binary files differnew file mode 100644 index 00000000000..4903c7d7498 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/gitlab-scale.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-settings.png b/doc/articles/openshift_and_gitlab/img/gitlab-settings.png Binary files differnew file mode 100644 index 00000000000..db4360ffef0 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/gitlab-settings.png diff --git a/doc/articles/openshift_and_gitlab/img/no-resources.png b/doc/articles/openshift_and_gitlab/img/no-resources.png Binary files differnew file mode 100644 index 00000000000..480fb766468 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/no-resources.png diff --git a/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png b/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png Binary files differnew file mode 100644 index 00000000000..8b9f85aa341 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png diff --git a/doc/articles/openshift_and_gitlab/img/pods-overview.png b/doc/articles/openshift_and_gitlab/img/pods-overview.png Binary files differnew file mode 100644 index 00000000000..e1cf08bd217 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/pods-overview.png diff --git a/doc/articles/openshift_and_gitlab/img/rc-name.png b/doc/articles/openshift_and_gitlab/img/rc-name.png Binary files differnew file mode 100644 index 00000000000..889e34adbec --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/rc-name.png diff --git a/doc/articles/openshift_and_gitlab/img/running-pods.png b/doc/articles/openshift_and_gitlab/img/running-pods.png Binary files differnew file mode 100644 index 00000000000..3fd4e56662f --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/running-pods.png diff --git a/doc/articles/openshift_and_gitlab/img/storage-volumes.png b/doc/articles/openshift_and_gitlab/img/storage-volumes.png Binary files differnew file mode 100644 index 00000000000..ae1e5381faa --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/storage-volumes.png diff --git a/doc/articles/openshift_and_gitlab/img/web-console.png b/doc/articles/openshift_and_gitlab/img/web-console.png Binary files differnew file mode 100644 index 00000000000..aa1425d4f94 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/img/web-console.png 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/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..." |