diff options
author | Fatih Acet <acetfatih@gmail.com> | 2016-10-04 18:06:30 +0300 |
---|---|---|
committer | Fatih Acet <acetfatih@gmail.com> | 2016-10-04 18:06:30 +0300 |
commit | a04378a5e7332efc05fba2d31d77c1e306533147 (patch) | |
tree | c6c733642795b556a14c16ebf20202dce931d67d | |
parent | 94d887485a00b15657478619324e4cdc39c5c251 (diff) | |
parent | b8005b6112d7322ff8b2cf0a1e55e6c56f0fcba3 (diff) | |
download | gitlab-ce-a04378a5e7332efc05fba2d31d77c1e306533147.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into revert-c676283b-existing
49 files changed, 520 insertions, 169 deletions
diff --git a/CHANGELOG b/CHANGELOG index 806196d811d..dd3b15f513b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,9 +8,11 @@ v 8.13.0 (unreleased) - Replaced the check sign to arrow in the show build view. !6501 - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Speed-up group milestones show page + - Keep refs for each deployment - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Add more tests for calendar contribution (ClemMakesApps) - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references + - Simplify Mentionable concern instance methods - Fix permission for setting an issue's due date - Expose expires_at field when sharing project on API - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) @@ -27,6 +29,7 @@ v 8.13.0 (unreleased) - Only update issuable labels if they have been changed - Take filters in account in issuable counters. !6496 - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) + - Append issue template to existing description !6149 (Joseph Frazier) - Revoke button in Applications Settings underlines on hover. - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Fix Long commit messages overflow viewport in file tree @@ -42,9 +45,13 @@ v 8.13.0 (unreleased) - Notify the Merger about merge after successful build (Dimitris Karakasilis) - Fix broken repository 500 errors in project list - Close todos when accepting merge requests via the API !6486 (tonygambone) - - Changed Slack service user referencing from full name to username (Sebastian Poxhofer) + - Changed Slack service user referencing from full name to username (Sebastian Poxhofer) + - Add Container Registry on/off status to Admin Area !6638 (the-undefined) v 8.12.4 (unreleased) + - Fix type mismatch bug when closing Jira issue + - Fix issues importing services via Import/Export + - Restrict failed login attempts for users with 2FA enabled - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell) v 8.12.3 @@ -104,6 +111,7 @@ v 8.12.0 - Fix long comments in diffs messing with table width - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Fix pagination on user snippets page + - Honor "fixed layout" preference in more places !6422 - Run CI builds with the permissions of users !5735 - Fix sorting of issues in API - Fix download artifacts button links !6407 @@ -144,6 +152,7 @@ v 8.12.0 - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Add textarea autoresize after comment (ClemMakesApps) - Do not write SSH public key 'comments' to authorized_keys !6381 + - Add due date to issue todos - Refresh todos count cache when an Issue/MR is deleted - Fix branches page dropdown sort alignment (ClemMakesApps) - Hides merge request button on branches page is user doesn't have permissions @@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0' gem 'after_commit_queue', '~> 1.3.0' # Issue tags -gem 'acts-as-taggable-on', '~> 3.4' +gem 'acts-as-taggable-on', '~> 4.0' # Background jobs gem 'sidekiq', '~> 4.2' diff --git a/Gemfile.lock b/Gemfile.lock index ca06b21ae65..6f8a8236866 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,8 +44,8 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - acts-as-taggable-on (3.5.0) - activerecord (>= 3.2, < 5) + acts-as-taggable-on (4.0.0) + activerecord (>= 4.0) addressable (2.3.8) after_commit_queue (1.3.0) activerecord (>= 3.0) @@ -802,7 +802,7 @@ DEPENDENCIES RedCloth (~> 4.3.2) ace-rails-ap (~> 4.1.0) activerecord-session_store (~> 1.0.0) - acts-as-taggable-on (~> 3.4) + acts-as-taggable-on (~> 4.0) addressable (~> 2.3.8) after_commit_queue (~> 1.3.0) akismet (~> 2.0) @@ -986,4 +986,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.1 + 1.13.2 diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 95352164d76..6d41442cdfc 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -72,9 +72,17 @@ // To be implemented on the extending class // e.g. // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { - this.editor.setValue(file.content, 1); - if (!skipFocus) this.editor.focus(); + TemplateSelector.prototype.requestFileSuccess = function(file, opts) { + var oldValue = this.editor.getValue(); + var newValue = file.content; + if (opts == null) { + opts = {}; + } + if (opts.append && oldValue.length && oldValue !== newValue) { + newValue = oldValue + '\n\n' + newValue; + } + this.editor.setValue(newValue, 1); + if (!opts.skipFocus) this.editor.focus(); if (this.editor instanceof jQuery) { this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index c8634b78f2b..8086c10ad6b 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -7,6 +7,9 @@ function Diff() { $('.files .diff-file').singleFileDiff(); this.filesCommentButton = $('.files .diff-file').filesCommentButton(); + if (this.diffViewType() === 'parallel') { + $('.content-wrapper .container-fluid').removeClass('container-limited'); + } $(document).off('click', '.js-unfold'); $(document).on('click', '.js-unfold', (function(_this) { return function(event) { @@ -52,6 +55,10 @@ })(this)); } + Diff.prototype.diffViewType = function() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + Diff.prototype.lineNumbers = function(line) { if (!line.children().length) { return [0, 0]; diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6 index cd92df8ddc5..13ee794ba38 100644 --- a/app/assets/javascripts/merge_conflict_data_provider.js.es6 +++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6 @@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs'; class MergeConflictDataProvider { getInitialData() { + // TODO: remove reliance on jQuery and DOM state introspection const diffViewType = $.cookie('diff_view'); + const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited'); return { isLoading : true, hasError : false, isParallel : diffViewType === 'parallel', diffViewType : diffViewType, + fixedLayout : fixedLayout, isSubmitting : false, conflictsData : {}, resolutionData : {} @@ -192,14 +195,17 @@ class MergeConflictDataProvider { updateViewType(newType) { const vi = this.vueInstance; - if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { + if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) { return; } - vi.diffView = newType; - vi.isParallel = newType === 'parallel'; - $.cookie('diff_view', newType); // TODO: Make sure that cookie path added. - $('.content-wrapper .container-fluid').toggleClass('container-limited'); + vi.diffViewType = newType; + vi.isParallel = newType === 'parallel'; + $.cookie('diff_view', newType, { + path: (gon && gon.relative_url_root) || '/' + }); + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', !vi.isParallel && vi.fixedLayout); } diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6 index b56fd5aa658..7e756433bf5 100644 --- a/app/assets/javascripts/merge_conflict_resolver.js.es6 +++ b/app/assets/javascripts/merge_conflict_resolver.js.es6 @@ -60,9 +60,8 @@ class MergeConflictResolver { $('#conflicts .js-syntax-highlight').syntaxHighlight(); }); - if (this.vue.diffViewType === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout); }) } diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 05644b3d03c..02ff5a382e2 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -36,13 +36,10 @@ }; MergeRequest.prototype.initTabs = function() { - if (this.opts.action !== 'new') { - // `MergeRequests#new` has no tab-persisting or lazy-loading behavior - window.mrTabs = new MergeRequestTabs(this.opts); - } else { - // Show the first tab (Commits) - return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show'); + if (window.mrTabs) { + window.mrTabs.unbindEvents(); } + window.mrTabs = new MergeRequestTabs(this.opts); }; MergeRequest.prototype.showAllCommits = function() { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 18bbfa7a459..bec11a198a1 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -56,6 +56,8 @@ MergeRequestTabs.prototype.commitsLoaded = false; + MergeRequestTabs.prototype.fixedLayoutPref = null; + function MergeRequestTabs(opts) { this.opts = opts != null ? opts : {}; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; @@ -70,7 +72,12 @@ MergeRequestTabs.prototype.bindEvents = function() { $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - return $(document).on('click', '.js-show-tab', this.showTab); + $(document).on('click', '.js-show-tab', this.showTab); + }; + + MergeRequestTabs.prototype.unbindEvents = function() { + $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); + $(document).off('click', '.js-show-tab', this.showTab); }; MergeRequestTabs.prototype.showTab = function(event) { @@ -85,11 +92,15 @@ if (action === 'commits') { this.loadCommits($target.attr('href')); this.expandView(); + this.resetViewContainer(); } else if (action === 'diffs') { this.loadDiff($target.attr('href')); if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { this.shrinkView(); } + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); + } navBarHeight = $('.navbar-gitlab').outerHeight(); $.scrollTo(".merge-request-details .merge-request-tabs", { offset: -navBarHeight @@ -97,11 +108,14 @@ } else if (action === 'builds') { this.loadBuilds($target.attr('href')); this.expandView(); + this.resetViewContainer(); } else if (action === 'pipelines') { this.loadPipelines($target.attr('href')); this.expandView(); + this.resetViewContainer(); } else { this.expandView(); + this.resetViewContainer(); } if (this.opts.setUrl) { this.setCurrentAction(action); @@ -126,7 +140,7 @@ if (action === 'show') { action = 'notes'; } - return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab'); }; // Replaces the current Merge Request-specific action in the URL with a new one @@ -209,7 +223,7 @@ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); $('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .diff-file').singleFileDiff(); - if (_this.diffViewType() === 'parallel') { + if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') { _this.expandViewContainer(); } _this.diffsLoaded = true; @@ -308,11 +322,21 @@ MergeRequestTabs.prototype.diffViewType = function() { return $('.inline-parallel-buttons a.active').data('view-type'); - // Returns diff view type }; MergeRequestTabs.prototype.expandViewContainer = function() { - return $('.container-fluid').removeClass('container-limited'); + var $wrapper = $('.content-wrapper .container-fluid'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); + } + $wrapper.removeClass('container-limited'); + }; + + MergeRequestTabs.prototype.resetViewContainer = function() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); + } }; MergeRequestTabs.prototype.shrinkView = function() { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index c32ddf80219..017008c8438 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -16,7 +16,7 @@ if (initialQuery.name) this.requestFile(initialQuery); $('.reset-template', this.dropdown.parent()).on('click', () => { - if (this.currentTemplate) this.setInputValueToTemplateContent(); + if (this.currentTemplate) this.setInputValueToTemplateContent(false); }); } @@ -26,22 +26,24 @@ this.currentTemplate = currentTemplate; if (err) return; // Error handled by global AJAX error handler this.stopLoadingSpinner(); - this.setInputValueToTemplateContent(); + this.setInputValueToTemplateContent(true); }); return; } - setInputValueToTemplateContent() { + setInputValueToTemplateContent(append) { // `this.requestFileSuccess` sets the value of the description input field - // to the content of the template selected. + // to the content of the template selected. If `append` is true, the + // template content will be appended to the previous value of the field, + // separated by a blank line if the previous value is non-empty. if (this.titleInput.val() === '') { // If the title has not yet been set, focus the title input and - // skip focusing the description input by setting `true` as the 2nd - // argument to `requestFileSuccess`. - this.requestFileSuccess(this.currentTemplate, true); + // skip focusing the description input by setting `true` as the + // `skipFocus` option to `requestFileSuccess`. + this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append}); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate); + this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append}); } return; } diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index d5a8a962662..4c497711fc0 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor # # Returns nil def prompt_for_two_factor(user) + return locked_user_redirect(user) if user.access_locked? + session[:otp_user_id] = user.id setup_u2f_authentication(user) render 'devise/sessions/two_factor' end + def locked_user_redirect(user) + flash.now[:alert] = 'Invalid Login or password' + render 'devise/sessions/new' + end + def authenticate_with_two_factor user = self.resource = find_user - if user_params[:otp_attempt].present? && session[:otp_user_id] + if user.access_locked? + locked_user_redirect(user) + elsif user_params[:otp_attempt].present? && session[:otp_user_id] authenticate_with_two_factor_via_otp(user) elsif user_params[:device_response].present? && session[:otp_user_id] authenticate_with_two_factor_via_u2f(user) @@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor remember_me(user) if user_params[:remember_me] == '1' sign_in(user) else + user.increment_failed_attempts! flash.now[:alert] = 'Invalid two-factor code.' - render :two_factor + prompt_for_two_factor(user) end end @@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor remember_me(user) if user_params[:remember_me] == '1' sign_in(user) else + user.increment_failed_attempts! flash.now[:alert] = 'Authentication via U2F device failed.' prompt_for_two_factor(user) end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index b8ed2c159a7..c13333641d3 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -15,18 +15,17 @@ module MembershipActions end def leave - @member = membershipable.members.find_by(user_id: current_user) || - membershipable.requesters.find_by(user_id: current_user) - Members::DestroyService.new(@member, current_user).execute + member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id). + execute(:all) - source_type = @member.real_source_type.humanize(capitalize: false) + source_type = membershipable.class.to_s.humanize(capitalize: false) notice = - if @member.request? + if member.request? "Your access request to the #{source_type} has been withdrawn." else - "You left the \"#{@member.source.human_name}\" #{source_type}." + "You left the \"#{membershipable.human_name}\" #{source_type}." end - redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] + redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize] redirect_to redirect_path, notice: notice end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 9c323d7705a..18cd800c619 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController end def destroy - @group_member = @group.members.find_by(id: params[:id]) || - @group.requesters.find_by(id: params[:id]) - - Members::DestroyService.new(@group_member, current_user).execute + Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all) respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 2343c7d20ec..f56b256984b 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def destroy - @project_member = @project.members.find_by(id: params[:id]) || - @project.requesters.find_by(id: params[:id]) - - Members::DestroyService.new(@project_member, current_user).execute + Members::DestroyService.new(@project, current_user, params). + execute(:all) respond_to do |format| format.html do diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 22387d66451..7d4d049101a 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -92,12 +92,8 @@ module PageLayoutHelper end end - def fluid_layout(enabled = false) - if @fluid_layout.nil? - @fluid_layout = (current_user && current_user.layout == "fluid") || enabled - else - @fluid_layout - end + def fluid_layout + current_user && current_user.layout == "fluid" end def blank_container(enabled = false) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 1e86f648203..a9db8bb2b82 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -114,6 +114,26 @@ module TodosHelper selected_type ? selected_type[:text] : default_type end + def todo_due_date(todo) + return unless todo.target.try(:due_date) + + is_due_today = todo.target.due_date.today? + is_overdue = todo.target.overdue? + css_class = + if is_due_today + 'text-warning' + elsif is_overdue + 'text-danger' + else + '' + end + + html = "· ".html_safe + html << content_tag(:span, class: css_class) do + "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}" + end + end + private def show_todo_state?(todo) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index ec9e0f1b1d0..eb2ff0428f6 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -43,19 +43,15 @@ module Mentionable self end - def all_references(current_user = nil, text = nil, extractor: nil) + def all_references(current_user = nil, extractor: nil) extractor ||= Gitlab::ReferenceExtractor. new(project, current_user) - if text - extractor.analyze(text, author: author) - else - self.class.mentionable_attrs.each do |attr, options| - text = __send__(attr) - options = options.merge(cache_key: [self, attr], author: author) + self.class.mentionable_attrs.each do |attr, options| + text = __send__(attr) + options = options.merge(cache_key: [self, attr], author: author) - extractor.analyze(text, options) - end + extractor.analyze(text, options) end extractor @@ -66,8 +62,8 @@ module Mentionable end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def referenced_mentionables(current_user = self.author, text = nil) - refs = all_references(current_user, text) + def referenced_mentionables(current_user = self.author) + refs = all_references(current_user) refs = (refs.issues + refs.merge_requests + refs.commits) # We're using this method instead of Array diffing because that requires @@ -77,8 +73,8 @@ module Mentionable end # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. - def create_cross_references!(author = self.author, without = [], text = nil) - refs = referenced_mentionables(author, text) + def create_cross_references!(author = self.author, without = []) + refs = referenced_mentionables(author) # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the @@ -97,10 +93,7 @@ module Mentionable return if changes.empty? - original_text = changes.collect { |_, vals| vals.first }.join(' ') - - preexisting = referenced_mentionables(author, original_text) - create_cross_references!(author, preexisting) + create_cross_references!(author) end private diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 07d7e19e70d..82b27b78229 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_save :keep_around_commit + after_save :create_ref def commit project.commit(sha) @@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base self == environment.last_deployment end - def keep_around_commit - project.repository.keep_around(self.sha) + def create_ref + project.repository.create_ref(ref, ref_path) end def manual_actions @@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base where.not(id: self.id). take end + + private + + def ref_path + File.join(environment.ref_path, 'deployments', id.to_s) + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 49e0a20640c..f0f3ee23223 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base def update_merge_request_metrics? self.name == "production" end + + def ref_path + "refs/environments/#{Shellwords.shellescape(name)}" + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a431d46cc9e..071dfe54ef9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base # `MergeRequestsClosingIssues` model. This is a performance optimization. # Calculating this information for a number of merge requests requires # running `ReferenceExtractor` on each of them separately. + # This optimization does not apply to issues from external sources. def cache_merge_request_closes_issues!(current_user = self.author) + return if project.has_external_issue_tracker? + transaction do self.merge_requests_closing_issues.delete_all + closes_issues(current_user).each do |issue| self.merge_requests_closing_issues.create!(issue: issue) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 51557228ab9..eb574555df6 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -997,6 +997,10 @@ class Repository Gitlab::Popen.popen(args, path_to_repo) end + def create_ref(ref, ref_path) + fetch_ref(path_to_repo, ref, ref_path) + end + def update_branch_with_hooks(current_user, branch) update_autocrlf_option diff --git a/app/models/service.rb b/app/models/service.rb index 80de7175565..66c804f2b06 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -136,6 +136,7 @@ class Service < ActiveRecord::Base end def #{arg}=(value) + self.properties ||= {} updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? self.properties['#{arg}'] = value end diff --git a/app/models/user.rb b/app/models/user.rb index 6996740eebd..7f5a8562907 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -827,6 +827,22 @@ class User < ActiveRecord::Base todos_pending_count(force: true) end + # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth + # flow means we don't call that automatically (and can't conveniently do so). + # + # See: + # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92> + # + def increment_failed_attempts! + self.failed_attempts ||= 0 + self.failed_attempts += 1 + if attempts_exceeded? + lock_access! unless access_locked? + else + save(validate: false) + end + end + private def projects_union(min_access_level = nil) diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index ca9db59cac7..b7a244c2029 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -14,6 +14,8 @@ module Members if member.request? && member.user != user notification_service.decline_access_request(member) end + + member end end end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 9a2bf82ef51..431da8372c9 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -1,17 +1,42 @@ module Members class DestroyService < BaseService - attr_accessor :member, :current_user + include MembersHelper - def initialize(member, current_user) - @member = member + attr_accessor :source + + ALLOWED_SCOPES = %i[members requesters all] + + def initialize(source, current_user, params = {}) + @source = source @current_user = current_user + @params = params end - def execute - unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) - raise Gitlab::Access::AccessDeniedError - end + def execute(scope = :members) + raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope) + + member = find_member!(scope) + + raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member) + AuthorizedDestroyService.new(member, current_user).execute end + + private + + def find_member!(scope) + condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } + case scope + when :all + source.members.find_by(condition) || + source.requesters.find_by!(condition) + else + source.public_send(scope).find_by!(condition) + end + end + + def can_destroy_member?(member) + member && can?(current_user, action_member_permission(:destroy, member), member) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index bf251816e7e..1ce66d50368 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -347,7 +347,7 @@ module SystemNoteService notes = notes.where(noteable_id: noteable.id) end - notes_for_mentioner(mentioner, noteable, notes).count > 0 + notes_for_mentioner(mentioner, noteable, notes).exists? end # Build an Array of lines detailing each commit added in a merge request diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e6687f43816..90798c47d97 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -63,6 +63,11 @@ Reply by email %span.light.pull-right = boolean_to_icon Gitlab::IncomingEmail.enabled? + %p + Container Registry + %span.light.pull-right + = boolean_to_icon Gitlab.config.registry.enabled + .col-md-4 %h4 Components diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index b40395c74de..cc077fad32a 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -19,6 +19,7 @@ (removed) · #{time_ago_with_tooltip(todo.created_at)} + = todo_due_date(todo) .todo-body .todo-note diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 94c53882623..237280872f1 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,5 +1,5 @@ %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } + %div{ class: "container-fluid" } .header-content %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %span.sr-only Toggle navigation diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 4433cab7782..8966dd3fd86 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>') // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { - $('.content-wrapper').find('.container-fluid').removeClass('container-limited') + $('.content-wrapper .container-fluid').removeClass('container-limited') } else { - $('.content-wrapper').find('.container-fluid').addClass('container-limited') + $('.content-wrapper .container-fluid').addClass('container-limited') } // Re-enable the "Save" button diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 62aff36aadd..576e7ef021a 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,7 +1,5 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - diff_files = diffs.diff_files -- if diff_view == :parallel - - fluid_layout true .content-block.oneline-block.files-changed .inline-parallel-buttons diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index d03ff9ec7e8..9f34ca9ff4e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -4,9 +4,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') -- if diff_view == :parallel - - fluid_layout true - .merge-request{'data-url' => merge_request_path(@merge_request)} = render "projects/merge_requests/show/mr_title" diff --git a/doc/ci/environments.md b/doc/ci/environments.md index d85b8a34ced..e070302fb82 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track Deployments are created when [jobs] deploy versions of code to [environments]. +### Checkout deployments locally + +Since 8.13, a reference in the git repository is saved for each deployment. So +knowing what the state is of your current environments is only a `git fetch` +away. + +In your git config, append the `[remote "<your-remote>"]` block with an extra +fetch line: + +``` +fetch = +refs/environments/*:refs/remotes/origin/environments/* +``` + ## Defining environments You can create and delete environments manually in the web interface, but we diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 7b9de7c9598..d3db7740830 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -75,9 +75,8 @@ module API required_attributes! [:user_id] source = find_source(source_type, params[:id]) - access_requester = source.requesters.find_by!(user_id: params[:user_id]) - - ::Members::DestroyService.new(access_requester, current_user).execute + ::Members::DestroyService.new(source, current_user, params). + execute(:requesters) end end end diff --git a/lib/api/members.rb b/lib/api/members.rb index a18ce769e29..34df55fe192 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -134,7 +134,7 @@ module API if member.nil? { message: "Access revoked", id: params[:user_id].to_i } else - ::Members::DestroyService.new(member, current_user).execute + ::Members::DestroyService.new(source, current_user, params).execute present member.user, with: Entities::Member, member: member end diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 50d3729449e..fe981d7b9fa 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -4,20 +4,18 @@ module API before { authenticate! } resource :namespaces do - # Get a namespaces list - # - # Example Request: - # GET /namespaces + desc 'Get a namespaces list' do + success Entities::Namespace + end + params do + optional :search, type: String, desc: "Search query for namespaces" + end get do - @namespaces = if current_user.admin - Namespace.all - else - current_user.namespaces - end - @namespaces = @namespaces.search(params[:search]) if params[:search].present? - @namespaces = paginate @namespaces + namespaces = current_user.admin ? Namespace.all : current_user.namespaces + + namespaces = namespaces.search(params[:search]) if params[:search].present? - present @namespaces, with: Entities::Namespace + present paginate(namespaces), with: Entities::Namespace end end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 92b97bf3d0c..a0870891cf4 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -87,10 +87,10 @@ describe Groups::GroupMembersController do context 'when member is not found' do before { sign_in(user) } - it 'returns 403' do + it 'returns 404' do delete :leave, group_id: group - expect(response).to have_http_status(403) + expect(response).to have_http_status(404) end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 5e2a8cf3849..074f85157de 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do context 'when member is not found' do before { sign_in(user) } - it 'returns 403' do + it 'returns 404' do delete :leave, namespace_id: project.namespace, project_id: project - expect(response).to have_http_status(403) + expect(response).to have_http_status(404) end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 8f27e616c3e..48d69377461 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -109,6 +109,44 @@ describe SessionsController do end end + context 'when the user is on their last attempt' do + before do + user.update(failed_attempts: User.maximum_attempts.pred) + end + + context 'when OTP is valid' do + it 'authenticates correctly' do + authenticate_2fa(otp_attempt: user.current_otp) + + expect(subject.current_user).to eq user + end + end + + context 'when OTP is invalid' do + before { authenticate_2fa(otp_attempt: 'invalid') } + + it 'does not authenticate' do + expect(subject.current_user).not_to eq user + end + + it 'warns about invalid login' do + expect(response).to set_flash.now[:alert] + .to /Invalid Login or password/ + end + + it 'locks the user' do + expect(user.reload).to be_access_locked + end + + it 'keeps the user locked on future login attempts' do + post(:create, user: { login: user.username, password: user.password }) + + expect(response) + .to set_flash.now[:alert].to /Invalid Login or password/ + end + end + end + context 'when another user does not have 2FA enabled' do let(:another_user) { create(:user) } diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index f76c4fe8b57..cd79c4f512d 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' wait_for_ajax - preview_template + preview_template(template_content) save_changes end @@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do end end + context 'user creates an issue using templates, with a prior description' do + let(:prior_description) { 'test issue description' } + let(:template_content) { 'this is a test "bug" template' } + let(:issue) { create(:issue, author: user, assignee: user, project: project) } + + background do + project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) + visit edit_namespace_project_issue_path project.namespace, project, issue + fill_in :'issue[title]', with: 'test issue title' + fill_in :'issue[description]', with: prior_description + end + + scenario 'user selects "bug" template' do + select_template 'bug' + wait_for_ajax + preview_template("#{prior_description}\n\n#{template_content}") + save_changes + end + end + context 'user creates a merge request using templates' do let(:template_content) { 'this is a test "feature-proposal" template' } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } @@ -55,7 +75,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "feature-proposal" template' do select_template 'feature-proposal' wait_for_ajax - preview_template + preview_template(template_content) save_changes end end @@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects template' do select_template 'feature-proposal' wait_for_ajax - preview_template + preview_template(template_content) save_changes end end end end - def preview_template + def preview_template(expected_content) click_link 'Preview' - expect(page).to have_content template_content + expect(page).to have_content expected_content end def save_changes diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index fc555a74f30..bf93c1d1251 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do let(:user) { create(:user) } let(:author) { create(:user) } let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } - let(:issue) { create(:issue) } + let(:issue) { create(:issue, due_date: Date.today) } describe 'GET /dashboard/todos' do context 'User does not have todos' do @@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_selector('.todos-list .todo', count: 1) end + it 'shows due date as today' do + page.within first('.todo') do + expect(page).to have_content 'Due today' + end + end + describe 'deleting the todo' do before do first('.done-todo').click diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 549b0042038..132858950d5 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -1,18 +1,27 @@ require 'spec_helper' describe Mentionable do - include Mentionable + class Example + include Mentionable - def author - nil + attr_accessor :project, :message + attr_mentionable :message + + def author + nil + end end describe 'references' do let(:project) { create(:project) } + let(:mentionable) { Example.new } it 'excludes JIRA references' do allow(project).to receive_messages(jira_tracker?: true) - expect(referenced_mentionables(project, 'JIRA-123')).to be_empty + + mentionable.project = project + mentionable.message = 'JIRA-123' + expect(mentionable.referenced_mentionables).to be_empty end end end @@ -39,9 +48,8 @@ describe Issue, "Mentionable" do let(:user) { create(:user) } def referenced_issues(current_user) - text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}" - - issue.referenced_mentionables(current_user, text) + issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}" + issue.referenced_mentionables(current_user) end context 'when the current user can see the issue' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 9d7be2429ed..38b6da50168 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -86,6 +86,30 @@ describe MergeRequest, models: true do end end + describe '#cache_merge_request_closes_issues!' do + before do + subject.project.team << [subject.author, :developer] + subject.target_branch = subject.project.default_branch + end + + it 'caches closed issues' do + issue = create :issue, project: subject.project + commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") + allow(subject).to receive(:commits).and_return([commit]) + + expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1) + end + + it 'does not cache issues from external trackers' do + subject.project.update_attribute(:has_external_issue_tracker, true) + issue = ExternalIssue.new('JIRA-123', subject.project) + commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") + allow(subject).to receive(:commits).and_return([commit]) + + expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count) + end + end + describe '#source_branch_sha' do let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } @@ -522,7 +546,7 @@ describe MergeRequest, models: true do end it_behaves_like 'an editable mentionable' do - subject { create(:merge_request) } + subject { create(:merge_request, :simple) } let(:backref_text) { "merge request #{subject.to_reference}" } let(:set_mentionable_text) { ->(txt){ subject.description = txt } } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index db29f4d353b..98c64c079b9 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -320,6 +320,16 @@ describe Repository, models: true do end end + describe '#create_ref' do + it 'redirects the call to fetch_ref' do + ref, ref_path = '1', '2' + + expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path) + + repository.create_ref(ref, ref_path) + end + end + describe "#changelog" do before do repository.send(:cache).expire(:changelog) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 05056a4bb47..ed1bc9271ae 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -203,6 +203,23 @@ describe Service, models: true do end end + describe 'initialize service with no properties' do + let(:service) do + GitlabIssueTrackerService.create( + project: create(:project), + title: 'random title' + ) + end + + it 'does not raise error' do + expect { service }.not_to raise_error + end + + it 'creates the properties' do + expect(service.properties).to eq({ "title" => "random title" }) + end + end + describe "callbacks" do let(:project) { create(:project) } let!(:service) do diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 905a7311372..b467890a403 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -195,7 +195,7 @@ describe API::AccessRequests, api: true do end context 'when authenticated as the access requester' do - it 'returns 200' do + it 'deletes the access requester' do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) @@ -205,7 +205,7 @@ describe API::AccessRequests, api: true do end context 'when authenticated as a master/owner' do - it 'returns 200' do + it 'deletes the access requester' do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) @@ -213,6 +213,16 @@ describe API::AccessRequests, api: true do end.to change { source.requesters.count }.by(-1) end + context 'user_id matches a member, not an access requester' do + it 'returns 404' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master) + + expect(response).to have_http_status(404) + end.not_to change { source.requesters.count } + end + end + context 'user_id does not match an existing access requester' do it 'returns 404' do expect do diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 2395445e7fd..9995f3488af 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -2,70 +2,111 @@ require 'spec_helper' describe Members::DestroyService, services: true do let(:user) { create(:user) } - let(:project) { create(:project) } - let!(:member) { create(:project_member, source: project) } + let(:member_user) { create(:user) } + let(:project) { create(:project, :public) } + let(:group) { create(:group, :public) } - context 'when member is nil' do - before do - project.team << [user, :developer] + shared_examples 'a service raising ActiveRecord::RecordNotFound' do + it 'raises ActiveRecord::RecordNotFound' do + expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) end + end - it 'does not destroy the member' do - expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError) + shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do + it 'raises Gitlab::Access::AccessDeniedError' do + expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError) end end - context 'when current user cannot destroy the given member' do - before do - project.team << [user, :developer] + shared_examples 'a service destroying a member' do + it 'destroys the member' do + expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1) + end + + context 'when the given member is an access requester' do + before do + source.members.find_by(user_id: member_user).destroy + source.request_access(member_user) + end + let(:access_requester) { source.requesters.find_by(user_id: member_user) } + + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' + + %i[requesters all].each do |scope| + context "and #{scope} scope is passed" do + it 'destroys the access requester' do + expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1) + end + + it 'calls Member#after_decline_request' do + expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester) + + described_class.new(source, user, params).execute(scope) + end + + context 'when current user is the member' do + it 'does not call Member#after_decline_request' do + expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester) + + described_class.new(source, member_user, params).execute(scope) + end + end + end + end end + end + + context 'when no member are found' do + let(:params) { { user_id: 42 } } - it 'does not destroy the member' do - expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError) + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do + let(:source) { project } + end + + it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do + let(:source) { group } end end - context 'when current user can destroy the given member' do + context 'when a member is found' do before do - project.team << [user, :master] + project.team << [member_user, :developer] + group.add_developer(member_user) end + let(:params) { { user_id: member_user.id } } - it 'destroys the member' do - destroy_member(member, user) + context 'when current user cannot destroy the given member' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end - expect(member).to be_destroyed + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end end - context 'when the given member is a requester' do + context 'when current user can destroy the given member' do before do - member.update_column(:requested_at, Time.now) + project.team << [user, :master] + group.add_owner(user) end - it 'calls Member#after_decline_request' do - expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) - - destroy_member(member, user) + it_behaves_like 'a service destroying a member' do + let(:source) { project } end - context 'when current user is the member' do - it 'does not call Member#after_decline_request' do - expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) - - destroy_member(member, member.user) - end + it_behaves_like 'a service destroying a member' do + let(:source) { group } end - context 'when current user is the member and ' do - it 'does not call Member#after_decline_request' do - expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) + context 'when given a :id' do + let(:params) { { id: project.members.find_by!(user_id: user.id).id } } - destroy_member(member, member.user) + it 'destroys the member' do + expect { described_class.new(project, user, params).execute }. + to change { project.members.count }.by(-1) end end end end - - def destroy_member(member, user) - Members::DestroyService.new(member, user).execute - end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 31167675d07..e49a0d5e553 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do end end + context 'closes related issues' do + let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } + + before do + allow(project).to receive(:default_branch).and_return(merge_request.target_branch) + end + + it 'closes GitLab issue tracker issues' do + issue = create :issue, project: project + commit = double('commit', safe_message: "Fixes #{issue.to_reference}") + allow(merge_request).to receive(:commits).and_return([commit]) + + service.execute(merge_request) + + expect(issue.reload.closed?).to be_truthy + end + + context 'with JIRA integration' do + include JiraServiceHelper + + let(:jira_tracker) { project.create_jira_service } + + before { jira_service_settings } + + it 'closes issues on JIRA issue tracker' do + jira_issue = ExternalIssue.new('JIRA-123', project) + commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") + allow(merge_request).to receive(:commits).and_return([commit]) + + expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once + + service.execute(merge_request) + end + end + end + context 'closes related todos' do let(:merge_request) { create(:merge_request, assignee: user, author: user) } let(:project) { merge_request.project } diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index e876d44c166..f57c82809a6 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -9,7 +9,7 @@ shared_context 'mentionable context' do let(:author) { subject.author } let(:mentioned_issue) { create(:issue, project: project) } - let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } + let!(:mentioned_mr) { create(:merge_request, source_project: project) } let(:mentioned_commit) { project.commit("HEAD~1") } let(:ext_proj) { create(:project, :public) } @@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do it 'creates new cross-reference notes when the mentionable text is edited' do subject.save + subject.create_cross_references! new_text = <<-MSG.strip_heredoc These references already existed: @@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do end # These two issues are new and should receive reference notes + # In the case of MergeRequests remember that cannot mention commits included in the MergeRequest new_issues.each do |newref| expect(SystemNoteService).to receive(:cross_reference). with(newref, subject.local_reference, author) |