From 9e9d064727b20255ce7451ba3cc418cc9007ca8f Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Mon, 15 Jul 2019 14:16:55 +0100 Subject: [PATCH 1/3] Add Aria support to capybara --- spec/features/projects/tree/create_directory_spec.rb | 4 +++- spec/features/projects/tree/create_file_spec.rb | 4 +++- spec/support/capybara.rb | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 2cb2a23b7be811..8585e24bc35059 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -47,7 +47,9 @@ fill_in('commit-message', with: 'commit message ide') - click_button('Commit') + page.within '.multi-file-commit-form' do + click_button('Commit') + end find('.js-ide-edit-mode').click diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index 9f5524da8e9b37..8623b10562d38f 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -39,7 +39,9 @@ fill_in('commit-message', with: 'commit message ide') - click_button('Commit') + page.within '.multi-file-commit-form' do + click_button('Commit') + end expect(page).to have_content('file name') end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 56ac208a025ed2..60990879fe2cc3 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -58,6 +58,7 @@ Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true Capybara.default_normalize_ws = true +Capybara.enable_aria_label = true # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run -- GitLab From 5aee2203681f9c99e243271fdc7318702bdffa03 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 28 May 2019 18:24:21 +0100 Subject: [PATCH 2/3] Allow blocking merge requests to be configured GitLab has model-level support for the idea of one merge request "blocking" another from being merged - the second MR can only be merged once the first one has, itself, been merged. We also show any existing blocks in the merge request widget. This commit adds support for adding and removing these blocks when creating or editing a merge request. Frontend changes mostly by Sam Bigelow --- app/views/shared/issuable/_form.html.haml | 2 + .../merge_requests/shared/init_form.js | 2 + .../merge_requests/blocking_mr_input.js | 38 +++++ .../merge_requests/blocking_mr_input_root.vue | 105 ++++++++++++++ .../components/related_issuable_input.vue | 8 +- .../javascripts/related_issues/constants.js | 4 + .../merge_requests/application_controller.rb | 3 + ee/app/models/ee/merge_request.rb | 23 +++ ee/app/models/merge_request_block.rb | 21 +++ .../ee/merge_request_widget_entity.rb | 19 +-- .../ee/merge_requests/base_service.rb | 5 + .../ee/merge_requests/create_service.rb | 4 + .../ee/merge_requests/update_service.rb | 4 + .../merge_requests/update_blocks_service.rb | 96 +++++++++++++ .../form/_merge_request_blocks.html.haml | 17 +++ .../9688-add-remove-blocking-mrs.yml | 5 + ...es_merge_request_with_blocking_mrs_spec.rb | 44 ++++++ ...r_edits_merge_request_blocking_mrs_spec.rb | 89 ++++++++++++ .../blocking_mr_input_root_spec.js | 136 ++++++++++++++++++ .../merge_requests/blocking_mr_input_spec.js | 50 +++++++ ee/spec/models/merge_request/blocking_spec.rb | 76 ++++++++++ ee/spec/models/merge_request_block_spec.rb | 41 ++++++ .../merge_requests/create_service_spec.rb | 32 +++++ .../update_blocks_service_spec.rb | 115 +++++++++++++++ .../merge_requests/update_service_spec.rb | 18 +++ locale/gitlab.pot | 14 ++ 26 files changed, 957 insertions(+), 14 deletions(-) create mode 100644 ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input.js create mode 100644 ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue create mode 100644 ee/app/services/merge_requests/update_blocks_service.rb create mode 100644 ee/app/views/shared/issuable/form/_merge_request_blocks.html.haml create mode 100644 ee/changelogs/unreleased/9688-add-remove-blocking-mrs.yml create mode 100644 ee/spec/features/merge_request/user_creates_merge_request_with_blocking_mrs_spec.rb create mode 100644 ee/spec/features/merge_request/user_edits_merge_request_blocking_mrs_spec.rb create mode 100644 ee/spec/frontend/projects/merge_requests/blocking_mr_input_root_spec.js create mode 100644 ee/spec/frontend/projects/merge_requests/blocking_mr_input_spec.js create mode 100644 ee/spec/services/merge_requests/create_service_spec.rb create mode 100644 ee/spec/services/merge_requests/update_blocks_service_spec.rb diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 214e87052da66d..07a7b5ce9de1f5 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -33,6 +33,8 @@ = render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form += render_if_exists "shared/issuable/form/merge_request_blocks", issuable: issuable, form: form + = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form = render 'shared/issuable/form/merge_params', issuable: issuable diff --git a/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js b/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js index 9a5adf1e184cf0..755746f74ba9a6 100644 --- a/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js +++ b/ee/app/assets/javascripts/pages/projects/merge_requests/shared/init_form.js @@ -1,5 +1,7 @@ import mountApprovals from 'ee/approvals/mount_mr_edit'; +import mountBlockingMergeRequestsInput from 'ee/projects/merge_requests/blocking_mr_input'; export default () => { mountApprovals(document.getElementById('js-mr-approvals-input')); + mountBlockingMergeRequestsInput(document.getElementById('js-blocking-merge-requests-input')); }; diff --git a/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input.js b/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input.js new file mode 100644 index 00000000000000..af479f27e3ee90 --- /dev/null +++ b/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import BlockingMrInput from 'ee/projects/merge_requests/blocking_mr_input_root.vue'; +import { n__ } from '~/locale'; + +export default el => { + if (!el) { + return null; + } + const { hiddenBlockingMrsCount, visibleBlockingMrRefs } = el.dataset; + const parsedVisibleBlockingMrRefs = JSON.parse(visibleBlockingMrRefs); + const containsHiddenBlockingMrs = hiddenBlockingMrsCount > 0; + + const references = containsHiddenBlockingMrs + ? [ + ...parsedVisibleBlockingMrRefs, + { + text: n__( + '%d inaccessible merge request', + '%d inaccessible merge requests', + hiddenBlockingMrsCount, + ), + isHiddenRef: true, + }, + ] + : parsedVisibleBlockingMrRefs; + + return new Vue({ + el, + render(h) { + return h(BlockingMrInput, { + props: { + existingRefs: references, + containsHiddenBlockingMrs, + }, + }); + }, + }); +}; diff --git a/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue b/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue new file mode 100644 index 00000000000000..9979d84dd87225 --- /dev/null +++ b/ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue @@ -0,0 +1,105 @@ + + + diff --git a/ee/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/ee/app/assets/javascripts/related_issues/components/related_issuable_input.vue index bdaa8311ad17a5..c7256da97a48ba 100644 --- a/ee/app/assets/javascripts/related_issues/components/related_issuable_input.vue +++ b/ee/app/assets/javascripts/related_issues/components/related_issuable_input.vue @@ -12,6 +12,11 @@ export default { issueToken, }, props: { + inputId: { + type: String, + required: false, + default: '', + }, references: { type: Array, required: false, @@ -162,7 +167,7 @@ export default { >
  • '), [issuableTypesMap.EPIC]: __(' or <#epic id>'), + [issuableTypesMap.MERGE_REQUEST]: __(' or <#merge request id>'), }, false: { [issuableTypesMap.ISSUE]: '', [issuableTypesMap.EPIC]: '', + [issuableTypesMap.MERGE_REQUEST]: '', }, }; export const inputPlaceholderTextMap = { [issuableTypesMap.ISSUE]: __('Paste issue link'), [issuableTypesMap.EPIC]: __('Paste epic link'), + [issuableTypesMap.MERGE_REQUEST]: __('Paste a merge request link'), }; export const relatedIssuesRemoveErrorMap = { diff --git a/ee/app/controllers/ee/projects/merge_requests/application_controller.rb b/ee/app/controllers/ee/projects/merge_requests/application_controller.rb index 4ca80117f3d103..e84f7c29a44303 100644 --- a/ee/app/controllers/ee/projects/merge_requests/application_controller.rb +++ b/ee/app/controllers/ee/projects/merge_requests/application_controller.rb @@ -23,6 +23,9 @@ def merge_request_params def merge_request_params_attributes attrs = super.push( + { blocking_merge_request_references: [] }, + :update_blocking_merge_request_refs, + :remove_hidden_blocking_merge_requests, approval_rule_attributes, :approvals_before_merge, :approver_group_ids, diff --git a/ee/app/models/ee/merge_request.rb b/ee/app/models/ee/merge_request.rb index 25a1a1d9849237..09605a849af63a 100644 --- a/ee/app/models/ee/merge_request.rb +++ b/ee/app/models/ee/merge_request.rb @@ -6,6 +6,7 @@ module MergeRequest extend ::Gitlab::Utils::Override include ::Approvable + include ::Gitlab::Allowable include ::Gitlab::Utils::StrongMemoize include FromUnion @@ -83,6 +84,28 @@ def supports_weight? false end + def visible_blocking_merge_requests(user) + Ability.merge_requests_readable_by_user(blocking_merge_requests, user) + end + + def visible_blocking_merge_request_refs(user) + visible_blocking_merge_requests(user).map do |mr| + mr.to_reference(target_project) + end + end + + # Unlike +visible_blocking_merge_requests+, this method doesn't include + # blocking MRs that have been merged. This simplifies output, since we don't + # need to tell the user that there are X hidden blocking MRs, of which only + # Y are an obstacle. Pass include_merged: true to override this behaviour. + def hidden_blocking_merge_requests_count(user, include_merged: false) + hidden = blocking_merge_requests - visible_blocking_merge_requests(user) + + hidden.delete_if(&:merged?) unless include_merged + + hidden.count + end + def validate_approval_rule_source return unless approval_rules.any? diff --git a/ee/app/models/merge_request_block.rb b/ee/app/models/merge_request_block.rb index 7432a05710d1a8..691ffb7ed97954 100644 --- a/ee/app/models/merge_request_block.rb +++ b/ee/app/models/merge_request_block.rb @@ -10,6 +10,27 @@ class MergeRequestBlock < ApplicationRecord validate :check_block_constraints + scope :with_blocking_mr_ids, -> (ids) do + where(blocking_merge_request_id: ids).includes(:blocking_merge_request) + end + + # Add a number of blocks at once. Pass a hash of blocking_id => blocked_id, or + # an Array of [blocking_id, blocked_id] tuples + def self.bulk_insert(pairs) + now = Time.current + + rows = pairs.map do |blocking, blocked| + { + blocking_merge_request_id: blocking, + blocked_merge_request_id: blocked, + created_at: now, + updated_at: now + } + end + + ::Gitlab::Database.bulk_insert(table_name, rows) + end + private def check_block_constraints diff --git a/ee/app/serializers/ee/merge_request_widget_entity.rb b/ee/app/serializers/ee/merge_request_widget_entity.rb index 8d46aeb84f49ad..9c18dafabd207e 100644 --- a/ee/app/serializers/ee/merge_request_widget_entity.rb +++ b/ee/app/serializers/ee/merge_request_widget_entity.rb @@ -173,21 +173,14 @@ module MergeRequestWidgetEntity private def blocking_merge_requests - visible_mrs_by_state = Hash.new { |h, k| h[k] = [] } - visible_count = 0 - hidden_blocking_count = 0 - - object.blocking_merge_requests.each do |mr| - if can?(current_user, :read_merge_request, mr) - visible_mrs_by_state[mr.state_name] << represent_blocking_mr(mr) - visible_count += 1 - elsif !mr.merged? # Ignore merged hidden MRs to make display simpler - hidden_blocking_count += 1 - end - end + hidden_blocking_count = object.hidden_blocking_merge_requests_count(current_user) + visible_mrs = object.visible_blocking_merge_requests(current_user) + visible_mrs_by_state = visible_mrs + .map { |mr| represent_blocking_mr(mr) } + .group_by { |blocking_mr| blocking_mr.object.state_name } { - total_count: visible_count + hidden_blocking_count, + total_count: visible_mrs.count + hidden_blocking_count, hidden_count: hidden_blocking_count, visible_merge_requests: visible_mrs_by_state } diff --git a/ee/app/services/ee/merge_requests/base_service.rb b/ee/app/services/ee/merge_requests/base_service.rb index 3e9b7a7abea1c7..5f96392f895de1 100644 --- a/ee/app/services/ee/merge_requests/base_service.rb +++ b/ee/app/services/ee/merge_requests/base_service.rb @@ -5,6 +5,8 @@ module MergeRequests module BaseService private + attr_accessor :blocking_merge_requests_params + def filter_params(merge_request) unless current_user.can?(:update_approvers, merge_request) params.delete(:approvals_before_merge) @@ -14,6 +16,9 @@ def filter_params(merge_request) self.params = ApprovalRules::ParamsFilteringService.new(merge_request, current_user, params).execute + self.blocking_merge_requests_params = + ::MergeRequests::UpdateBlocksService.extract_params!(params) + super end end diff --git a/ee/app/services/ee/merge_requests/create_service.rb b/ee/app/services/ee/merge_requests/create_service.rb index c151cf76ad4364..45b535c4310350 100644 --- a/ee/app/services/ee/merge_requests/create_service.rb +++ b/ee/app/services/ee/merge_requests/create_service.rb @@ -17,6 +17,10 @@ def after_create(issuable) if pipeline ::SyncSecurityReportsToReportApprovalRulesWorker.perform_async(pipeline.id) end + + ::MergeRequests::UpdateBlocksService + .new(issuable, current_user, blocking_merge_requests_params) + .execute end end end diff --git a/ee/app/services/ee/merge_requests/update_service.rb b/ee/app/services/ee/merge_requests/update_service.rb index 8e8a6c2de0c2c4..f471e43965acac 100644 --- a/ee/app/services/ee/merge_requests/update_service.rb +++ b/ee/app/services/ee/merge_requests/update_service.rb @@ -31,6 +31,10 @@ def execute(merge_request) notification_service.add_merge_request_approvers(merge_request, new_approvers, current_user) end + ::MergeRequests::UpdateBlocksService + .new(merge_request, current_user, blocking_merge_requests_params) + .execute + merge_request end diff --git a/ee/app/services/merge_requests/update_blocks_service.rb b/ee/app/services/merge_requests/update_blocks_service.rb new file mode 100644 index 00000000000000..fdc3afb3cdb7e9 --- /dev/null +++ b/ee/app/services/merge_requests/update_blocks_service.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module MergeRequests + class UpdateBlocksService + include ::Gitlab::Allowable + include ::Gitlab::Utils::StrongMemoize + + class << self + def extract_params!(mutable_params) + { + update: mutable_params.delete(:update_blocking_merge_request_refs), + remove_hidden: mutable_params.delete(:remove_hidden_blocking_merge_requests), + references: mutable_params.delete(:blocking_merge_request_references) + } + end + end + + attr_reader :merge_request, :current_user, :params + + def initialize(merge_request, current_user, params = {}) + @merge_request = merge_request + @current_user = current_user + @params = params + + DeclarativePolicy.user_scope do + @visible_blocks, @hidden_blocks = merge_request.blocks_as_blockee.partition do |block| + can?(current_user, :read_merge_request, block.blocking_merge_request) + end + end + end + + def execute + return unless update? + return unless merge_request.target_project.feature_available?(:blocking_merge_requests) + + merge_request + .blocks_as_blockee + .with_blocking_mr_ids(ids_to_del) + .delete_all + + ::MergeRequestBlock.bulk_insert( + ids_to_add.map { |blocking_id| [blocking_id, merge_request.id] } + ) + + true + end + + private + + attr_reader :visible_blocks, :hidden_blocks + + def update? + params.fetch(:update, false) + end + + def remove_hidden? + params.fetch(:remove_hidden, false) + end + + def references + params.fetch(:references, []) + end + + def requested_ids + strong_memoize(:requested_ids) do + next [] unless references.present? + + # The analyzer will only return references the current user can see + analyzer = ::Gitlab::ReferenceExtractor.new(merge_request.target_project, current_user) + analyzer.analyze(references.join(" ")) + + analyzer.merge_requests.map(&:id) + end + end + + def visible_ids + strong_memoize(:visible_ids) { visible_blocks.map(&:blocking_merge_request_id) } + end + + def hidden_ids + strong_memoize(:hidden_ids) { hidden_blocks.map(&:blocking_merge_request_id) } + end + + def ids_to_add + strong_memoize(:ids_to_add) { requested_ids - visible_ids } + end + + def ids_to_del + strong_memoize(:ids_to_del) do + (visible_ids - requested_ids).tap do |ary| + ary.push(*hidden_ids) if remove_hidden? + end + end + end + end +end diff --git a/ee/app/views/shared/issuable/form/_merge_request_blocks.html.haml b/ee/app/views/shared/issuable/form/_merge_request_blocks.html.haml new file mode 100644 index 00000000000000..13677a7ca0523e --- /dev/null +++ b/ee/app/views/shared/issuable/form/_merge_request_blocks.html.haml @@ -0,0 +1,17 @@ +- merge_request = local_assigns.fetch(:issuable) + +- return unless merge_request.is_a?(MergeRequest) + +- form = local_assigns.fetch(:form) +- project = merge_request.target_project + +- return unless project&.feature_available?(:blocking_merge_requests) + +.form-group.row.blocking-merge-requests + = form.label :blocking_merge_request_references, _('Blocking merge requests'), class: 'col-form-label col-sm-2' + .col-sm-10 + = text_field_tag 'blocking_merge_request_refs', nil, + class: "form-control ", + id: "js-blocking-merge-requests-input", + data: { hidden_blocking_mrs_count: merge_request.hidden_blocking_merge_requests_count(current_user), + visible_blocking_mr_refs: merge_request.visible_blocking_merge_request_refs(current_user) } diff --git a/ee/changelogs/unreleased/9688-add-remove-blocking-mrs.yml b/ee/changelogs/unreleased/9688-add-remove-blocking-mrs.yml new file mode 100644 index 00000000000000..f8120dac0cd6e4 --- /dev/null +++ b/ee/changelogs/unreleased/9688-add-remove-blocking-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Support for blocking merge requests +merge_request: 13506 +author: +type: added diff --git a/ee/spec/features/merge_request/user_creates_merge_request_with_blocking_mrs_spec.rb b/ee/spec/features/merge_request/user_creates_merge_request_with_blocking_mrs_spec.rb new file mode 100644 index 00000000000000..3bfd1e1430ed63 --- /dev/null +++ b/ee/spec/features/merge_request/user_creates_merge_request_with_blocking_mrs_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'User creates a merge request with blocking MRs', :js do + let(:project) { create(:project, :repository) } + let(:user) { project.owner } + + let(:mr_params) { { title: 'Some feature', source_branch: 'fix', target_branch: 'feature' } } + + before do + sign_in(user) + end + + context 'feature is enabled' do + before do + stub_licensed_features(blocking_merge_requests: true) + end + + it 'creates a merge request with a blocking MR' do + other_mr = create(:merge_request) + other_mr.target_project.team.add_maintainer(user) + + visit(project_new_merge_request_path(project, merge_request: mr_params)) + + fill_in 'Blocking merge requests', with: other_mr.to_reference(full: true) + click_button('Submit merge request') + + expect(page).to have_content('Blocked by 1 merge request') + end + end + + context 'feature is disabled' do + before do + stub_licensed_features(blocking_merge_requests: false) + end + + it 'does not show blocking MRs controls' do + visit(project_new_merge_request_path(project, merge_request: mr_params)) + + expect(page).not_to have_content('Blocking merge requests') + end + end +end diff --git a/ee/spec/features/merge_request/user_edits_merge_request_blocking_mrs_spec.rb b/ee/spec/features/merge_request/user_edits_merge_request_blocking_mrs_spec.rb new file mode 100644 index 00000000000000..cad8cc5ebde3c3 --- /dev/null +++ b/ee/spec/features/merge_request/user_edits_merge_request_blocking_mrs_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe "User edits merge request with blocking MRs", :js do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.target_project } + let(:user) { merge_request.target_project.owner } + + let(:other_mr) { create(:merge_request) } + + before do + sign_in(user) + end + + context 'feature is enabled' do + before do + stub_licensed_features(blocking_merge_requests: true) + end + + context 'user can see the other MR' do + before do + other_mr.target_project.team.add_developer(user) + end + + it 'can add the other MR' do + visit edit_project_merge_request_path(project, merge_request) + + fill_in 'Blocking merge requests', with: other_mr.to_reference(full: true) + + click_button 'Save changes' + + expect(page).to have_content('Blocked by 1 merge request') + end + + it 'can see and remove an existing blocking MR' do + create(:merge_request_block, blocking_merge_request: other_mr, blocked_merge_request: merge_request) + + visit edit_project_merge_request_path(project, merge_request) + + expect(page).to have_content(other_mr.to_reference(full: true)) + + click_button "Remove #{other_mr.to_reference(full: true)}" + click_button 'Save changes' + + expect(page).not_to have_content('Blocked by 1 merge request') + expect(page).not_to have_content(other_mr.to_reference(full: true)) + end + end + + context 'user cannot see the other MR' do + it 'cannot add the other MR' do + visit edit_project_merge_request_path(project, merge_request) + + fill_in 'Blocking merge requests', with: other_mr.to_reference(full: true) + + click_button 'Save changes' + + expect(page).not_to have_content('Blocked by 1 merge request') + end + + it 'sees the existing MR as hidden and can remove it' do + create(:merge_request_block, blocking_merge_request: other_mr, blocked_merge_request: merge_request) + + visit edit_project_merge_request_path(project, merge_request) + + expect(page).to have_content('1 inaccessible merge request') + + click_button 'Remove 1 inaccessible merge request' + click_button 'Save changes' + + expect(page).not_to have_content('Blocked by 1 merge request') + expect(page).not_to have_content(other_mr.to_reference(full: true)) + end + end + end + + context 'feature is disabled' do + before do + stub_licensed_features(blocking_merge_requests: false) + end + + it 'cannot see the blocking MR controls' do + visit edit_project_merge_request_path(project, merge_request) + + expect(page).not_to have_content('Blocking merge requests') + end + end +end diff --git a/ee/spec/frontend/projects/merge_requests/blocking_mr_input_root_spec.js b/ee/spec/frontend/projects/merge_requests/blocking_mr_input_root_spec.js new file mode 100644 index 00000000000000..45dae3d6dcf815 --- /dev/null +++ b/ee/spec/frontend/projects/merge_requests/blocking_mr_input_root_spec.js @@ -0,0 +1,136 @@ +import { shallowMount } from '@vue/test-utils'; +import BlockingMrInputRoot from 'ee/projects/merge_requests/blocking_mr_input_root.vue'; +import RelatedIssuableInput from 'ee/related_issues/components/related_issuable_input.vue'; + +describe('blocking mr input root', () => { + let wrapper; + + const getInput = () => wrapper.find(RelatedIssuableInput); + const addTokenizedInput = input => { + getInput().vm.$emit('addIssuableFormInput', { + untouchedRawReferences: [input], + touchedReference: '', + }); + }; + const addInput = input => { + getInput().vm.$emit('addIssuableFormInput', { + untouchedRawReferences: [], + touchedReference: input, + }); + }; + const removeRef = index => { + getInput().vm.$emit('pendingIssuableRemoveRequest', index); + }; + const createComponent = (propsData = {}) => { + wrapper = shallowMount(BlockingMrInputRoot, { propsData }); + }; + + it('does not keep duplicate references', () => { + createComponent(); + const input = '!1'; + + addTokenizedInput(input); + addTokenizedInput(input); + + expect(wrapper.vm.references).toEqual(['!1']); + }); + + it('updates input value to empty string when adding a tokenized input', () => { + createComponent(); + + addTokenizedInput('foo'); + + expect(wrapper.vm.inputValue).toBe(''); + }); + + it('updates input value to ref when typing into input (before adding whitespace)', () => { + createComponent(); + + addInput('foo'); + + expect(wrapper.vm.inputValue).toBe('foo'); + }); + + it('does not reorder when adding a ref that already exists', () => { + const input = '!1'; + createComponent({ + existingRefs: [input, '!2'], + }); + + addTokenizedInput(input, wrapper); + + expect(wrapper.vm.references).toEqual(['!1', '!2']); + }); + + it('does not add empty reference on blur', () => { + createComponent(); + + getInput().vm.$emit('addIssuableFormBlur', ''); + + expect(wrapper.vm.references).toHaveLength(0); + }); + + describe('hidden inputs', () => { + const createHiddenInputExpectation = selector => bool => { + expect(wrapper.find(selector).element.value).toBe(`${bool}`); + }; + + describe('update_blocking_merge_request_refs', () => { + const expectShouldUpdateRefsToBe = createHiddenInputExpectation( + 'input[name="merge_request[update_blocking_merge_request_refs]"]', + ); + + it('is false when nothing happens', () => { + createComponent(); + + expectShouldUpdateRefsToBe(false); + }); + + it('is true after a ref is removed', () => { + createComponent({ existingRefs: ['!1'] }); + removeRef(0); + + expectShouldUpdateRefsToBe(true); + }); + + it('is true after a ref is added', () => { + createComponent(); + addTokenizedInput('foo'); + + expectShouldUpdateRefsToBe(true); + }); + }); + + describe('remove_hidden_blocking_merge_requests', () => { + const expectRemoveHiddenBlockingMergeRequestsToBe = createHiddenInputExpectation( + 'input[name="merge_request[update_blocking_merge_request_refs]"]', + ); + const makeComponentWithHiddenMrs = () => { + const hiddenMrsRef = '2 inaccessible merge requests'; + createComponent({ + containsHiddenBlockingMrs: true, + existingRefs: ['!1', '!2', hiddenMrsRef], + }); + }; + + it('is true when nothing has happened', () => { + makeComponentWithHiddenMrs(); + + expectRemoveHiddenBlockingMergeRequestsToBe(false); + }); + + it('is false when removing any other MRs', () => { + makeComponentWithHiddenMrs(); + + expectRemoveHiddenBlockingMergeRequestsToBe(false); + }); + + it('is false when ref has been removed', () => { + makeComponentWithHiddenMrs(); + removeRef(2); + + expectRemoveHiddenBlockingMergeRequestsToBe(true); + }); + }); + }); +}); diff --git a/ee/spec/frontend/projects/merge_requests/blocking_mr_input_spec.js b/ee/spec/frontend/projects/merge_requests/blocking_mr_input_spec.js new file mode 100644 index 00000000000000..d23d11d15e7b80 --- /dev/null +++ b/ee/spec/frontend/projects/merge_requests/blocking_mr_input_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import initBlockingMrInput from 'ee/projects/merge_requests/blocking_mr_input'; + +jest.mock('vue'); + +describe('BlockingMrInput', () => { + let h; + const refs = ['!1']; + const getProps = () => h.mock.calls[0][1].props; + const callRender = () => { + Vue.mock.calls[0][0].render(h); + }; + const setInnerHtml = (visibleMrs = refs, hiddenCount = 2) => { + document.body.innerHTML += `
    `; + }; + + beforeEach(() => { + h = jest.fn(); + }); + + afterEach(() => { + document.querySelector('#test').remove(); + jest.clearAllMocks(); + }); + + it('adds hidden references block when hidden count is greater than 0', () => { + setInnerHtml(); + initBlockingMrInput(document.querySelector('#test')); + callRender(); + expect(getProps().existingRefs[refs.length].text).toBe('2 inaccessible merge requests'); + }); + + it('containsHiddenBlockingMrs is true when count is greater than one', () => { + setInnerHtml(); + initBlockingMrInput(document.querySelector('#test')); + + callRender(); + expect(getProps().containsHiddenBlockingMrs).toBe(true); + }); + + it('containsHiddenBlockingMrs is false when count is zero', () => { + setInnerHtml(refs, 0); + initBlockingMrInput(document.querySelector('#test')); + + callRender(); + expect(getProps().containsHiddenBlockingMrs).toBe(false); + }); +}); diff --git a/ee/spec/models/merge_request/blocking_spec.rb b/ee/spec/models/merge_request/blocking_spec.rb index ce9432d9e2e385..b01d1290ddb7a9 100644 --- a/ee/spec/models/merge_request/blocking_spec.rb +++ b/ee/spec/models/merge_request/blocking_spec.rb @@ -68,4 +68,80 @@ end end end + + describe '#visible_blocking_merge_requests' do + let(:block) { create(:merge_request_block) } + let(:blocking_mr) { block.blocking_merge_request } + let(:blocked_mr) { block.blocked_merge_request } + let(:user) { create(:user) } + + it 'shows blocking MR to developer' do + blocking_mr.target_project.team.add_developer(user) + + expect(blocked_mr.visible_blocking_merge_requests(user)).to contain_exactly(blocking_mr) + end + + it 'hides block from guest' do + blocking_mr.target_project.team.add_guest(user) + + expect(blocked_mr.visible_blocking_merge_requests(user)).to be_empty + end + + it 'hides block from anonymous user' do + expect(blocked_mr.visible_blocking_merge_requests(nil)).to be_empty + end + end + + describe '#visible_blocking_merge_request_refs' do + let(:merge_request) { create(:merge_request) } + let(:other_mr) { create(:merge_request) } + let(:user) { create(:user) } + + it 'returns the references for the result of #visible_blocking_merge_requests' do + expect(merge_request) + .to receive(:visible_blocking_merge_requests) + .with(user) + .and_return([other_mr]) + + expect(merge_request.visible_blocking_merge_request_refs(user)) + .to eq([other_mr.to_reference(full: true)]) + end + end + + describe '#hidden_blocking_merge_requests_count' do + let(:block) { create(:merge_request_block) } + let(:blocking_mr) { block.blocking_merge_request } + let(:blocked_mr) { block.blocked_merge_request } + let(:user) { create(:user) } + + it 'returns 0 when all MRs are visible' do + blocking_mr.target_project.team.add_developer(user) + + expect(blocked_mr.hidden_blocking_merge_requests_count(user)).to eq(0) + end + + context 'MR is hidden' do + before do + blocking_mr.target_project.team.add_guest(user) + end + + it 'returns 1 when MR is unmerged by default' do + expect(blocked_mr.hidden_blocking_merge_requests_count(user)).to eq(1) + end + + context 'MR is merged' do + before do + blocking_mr.update_columns(state: 'merged') + end + + it 'returns 0 by default' do + expect(blocked_mr.hidden_blocking_merge_requests_count(user)).to eq(0) + end + + it 'returns 1 when include_merged: true' do + expect(blocked_mr.hidden_blocking_merge_requests_count(user, include_merged: true)).to eq(1) + end + end + end + end end diff --git a/ee/spec/models/merge_request_block_spec.rb b/ee/spec/models/merge_request_block_spec.rb index 207be321f26b27..78c76187ad82c5 100644 --- a/ee/spec/models/merge_request_block_spec.rb +++ b/ee/spec/models/merge_request_block_spec.rb @@ -60,4 +60,45 @@ expect(new_block).not_to be_valid end end + + describe '.bulk_insert' do + let(:mrs) { create_list(:merge_request, 4) } + + it 'inserts multiple blocks specified as a Hash' do + described_class.bulk_insert( + mrs[0].id => mrs[1].id, + mrs[2].id => mrs[3].id + ) + + expect(described_class.all).to contain_exactly( + having_attributes(blocking_merge_request_id: mrs[0].id, blocked_merge_request_id: mrs[1].id), + having_attributes(blocking_merge_request_id: mrs[2].id, blocked_merge_request_id: mrs[3].id) + ) + end + + it 'inserts multiple blocks specified as an Array' do + described_class.bulk_insert([[mrs[0].id, mrs[1].id], [mrs[2].id, mrs[3].id]]) + + expect(described_class.all).to contain_exactly( + having_attributes(blocking_merge_request_id: mrs[0].id, blocked_merge_request_id: mrs[1].id), + having_attributes(blocking_merge_request_id: mrs[2].id, blocked_merge_request_id: mrs[3].id) + ) + end + end + + describe '.with_blocking_mr_ids' do + let!(:block) { create(:merge_request_block) } + let!(:other_block) { create(:merge_request_block) } + + subject(:result) { described_class.with_blocking_mr_ids([block.blocking_merge_request_id]) } + + it 'returns blocks with a matching blocking_merge_request_id' do + is_expected.to contain_exactly(block) + end + + it 'eager-loads the blocking MRs' do + association = result.first.association(:blocking_merge_request) + expect(association.loaded?).to be(true) + end + end end diff --git a/ee/spec/services/merge_requests/create_service_spec.rb b/ee/spec/services/merge_requests/create_service_spec.rb new file mode 100644 index 00000000000000..24ab451b080a0a --- /dev/null +++ b/ee/spec/services/merge_requests/create_service_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::CreateService do + let(:project) { create(:project, :repository) } + let(:user) { project.owner } + + subject(:service) { described_class.new(project, user, params) } + + describe '#execute' do + context 'with blocking merge requests' do + let(:params) { { title: 'Blocked MR', source_branch: 'feature', target_branch: 'master' } } + + it 'delegates to MergeRequests::UpdateBlocksService' do + expect(MergeRequests::UpdateBlocksService) + .to receive(:extract_params!) + .and_return(:extracted_params) + + expect_next_instance_of(MergeRequests::UpdateBlocksService) do |block_service| + expect(block_service.merge_request.title).to eq('Blocked MR') + expect(block_service.current_user).to eq(user) + expect(block_service.params).to eq(:extracted_params) + + expect(block_service).to receive(:execute) + end + + service.execute + end + end + end +end diff --git a/ee/spec/services/merge_requests/update_blocks_service_spec.rb b/ee/spec/services/merge_requests/update_blocks_service_spec.rb new file mode 100644 index 00000000000000..a67e8bb2214388 --- /dev/null +++ b/ee/spec/services/merge_requests/update_blocks_service_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::UpdateBlocksService do + describe '.extract_params!' do + it 'removes and reformats merge request params' do + mr_params = { + unrelated: true, + update_blocking_merge_request_refs: true, + remove_hidden_blocking_merge_requests: true, + blocking_merge_request_references: ['!1'] + } + + block_params = described_class.extract_params!(mr_params) + + expect(block_params).to eq( + update: true, + remove_hidden: true, + references: ['!1'] + ) + + expect(mr_params).to eq(unrelated: true) + end + end + + describe '#execute' do + let(:merge_request) { create(:merge_request) } + let(:user) { merge_request.target_project.owner } + + let(:mr_to_ignore) { create(:merge_request) } + let(:mr_to_add) { create(:merge_request) } + let(:mr_to_keep) { create(:merge_request) } + let(:mr_to_del) { create(:merge_request) } + let(:hidden_mr) { create(:merge_request) } + + let(:refs) do + [mr_to_ignore, mr_to_add, mr_to_keep].map { |mr| mr.to_reference(full: true) } + end + + let(:params) do + { + remove_hidden: remove_hidden, + references: refs, + update: update + } + end + + subject(:service) { described_class.new(merge_request, user, params) } + + before do + [mr_to_add, mr_to_keep, mr_to_del].each do |mr| + mr.target_project.team.add_maintainer(user) + end + + create(:merge_request_block, blocking_merge_request: mr_to_keep, blocked_merge_request: merge_request) + create(:merge_request_block, blocking_merge_request: mr_to_del, blocked_merge_request: merge_request) + create(:merge_request_block, blocking_merge_request: hidden_mr, blocked_merge_request: merge_request) + end + + context 'licensed' do + before do + stub_licensed_features(blocking_merge_requests: true) + end + + context 'with update: false' do + let(:update) { false } + let(:remove_hidden) { true } + + it 'does nothing' do + expect { service.execute }.not_to change { MergeRequestBlock.count } + end + end + + context 'with update: true' do + let(:update) { true } + + context 'with remove_hidden: false' do + let(:remove_hidden) { false } + + it 'adds only the requested MRs the user can see' do + service.execute + + expect(merge_request.blocking_merge_requests) + .to contain_exactly(mr_to_add, mr_to_keep, hidden_mr) + end + end + + context 'with remove_hidden: true' do + let(:remove_hidden) { true } + + it 'adds visible MRs and removes the hidden MR' do + service.execute + + expect(merge_request.blocking_merge_requests) + .to contain_exactly(mr_to_add, mr_to_keep) + end + end + end + end + + context 'unlicensed' do + let(:update) { true } + let(:remove_hidden) { true } + + before do + stub_licensed_features(blocking_merge_requests: false) + end + + it 'does nothing' do + expect { service.execute }.not_to change { MergeRequestBlock.count } + end + end + end +end diff --git a/ee/spec/services/merge_requests/update_service_spec.rb b/ee/spec/services/merge_requests/update_service_spec.rb index 2034e43e13942a..eea67ba15b545a 100644 --- a/ee/spec/services/merge_requests/update_service_spec.rb +++ b/ee/spec/services/merge_requests/update_service_spec.rb @@ -179,5 +179,23 @@ def update_merge_request(opts) expect(merge_request.reload.approvals).to be_empty end end + + context 'updating blocking merge requests' do + it 'delegates to MergeRequests::UpdateBlocksService' do + expect(MergeRequests::UpdateBlocksService) + .to receive(:extract_params!) + .and_return(:extracted_params) + + expect_next_instance_of(MergeRequests::UpdateBlocksService) do |service| + expect(service.merge_request).to eq(merge_request) + expect(service.current_user).to eq(user) + expect(service.params).to eq(:extracted_params) + + expect(service).to receive(:execute) + end + + update_merge_request({}) + end + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 396cd1340e4b35..3644023b65a0d2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -56,6 +56,9 @@ msgstr "" msgid " or <#issue id>" msgstr "" +msgid " or <#merge request id>" +msgstr "" + msgid "%d comment" msgid_plural "%d comments" msgstr[0] "" @@ -99,6 +102,11 @@ msgid_plural "%d fixed test results" msgstr[0] "" msgstr[1] "" +msgid "%d inaccessible merge request" +msgid_plural "%d inaccessible merge requests" +msgstr[0] "" +msgstr[1] "" + msgid "%d issue" msgid_plural "%d issues" msgstr[0] "" @@ -2117,6 +2125,9 @@ msgid_plural "Blocked by %d closed merge requests." msgstr[0] "" msgstr[1] "" +msgid "Blocking merge requests" +msgstr "" + msgid "Blog" msgstr "" @@ -9750,6 +9761,9 @@ msgstr "" msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}" msgstr "" +msgid "Paste a merge request link" +msgstr "" + msgid "Paste epic link" msgstr "" -- GitLab From 32661e5bbb130a1fa0e5cfc71b7284be0b765348 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 18 Jul 2019 16:30:22 +0100 Subject: [PATCH 3/3] Add documentation for blocking MRs This feature needs to be documented, so here is the documentation --- .../merge_requests/blocking_merge_requests.md | 133 ++++++++++++++++++ .../img/edit_blocking_merge_requests.png | Bin 0 -> 9926 bytes ...t_blocking_merge_requests_inaccessible.png | Bin 0 -> 10867 bytes ...w_blocking_merge_requests_in_mr_widget.png | Bin 0 -> 27089 bytes doc/user/project/merge_requests/index.md | 16 +++ 5 files changed, 149 insertions(+) create mode 100644 doc/user/project/merge_requests/blocking_merge_requests.md create mode 100644 doc/user/project/merge_requests/img/edit_blocking_merge_requests.png create mode 100644 doc/user/project/merge_requests/img/edit_blocking_merge_requests_inaccessible.png create mode 100644 doc/user/project/merge_requests/img/show_blocking_merge_requests_in_mr_widget.png diff --git a/doc/user/project/merge_requests/blocking_merge_requests.md b/doc/user/project/merge_requests/blocking_merge_requests.md new file mode 100644 index 00000000000000..0506a7cb4a5b2b --- /dev/null +++ b/doc/user/project/merge_requests/blocking_merge_requests.md @@ -0,0 +1,133 @@ +--- +type: reference, concepts +--- + +# Blocking merge requests **(PREMIUM)** + +> Introduced in GitLab Premium 12.2 + +Blocking merge requests allow dependencies between MRs to be expressed. If a +merge request is blocked by another MR, it cannot be merged until that blocking +MR is itself merged. + +NOTE: **Note:** +Blocking merge requests are a **PREMIUM** feature, but this restriction is only +enforced for the blocked merge request. A merge request in a **CORE** or +**STARTER** project can block a **PREMIUM** merge request, but not vice-versa. + +## Use cases + +* Ensure changes to a library are merged before changes to a project that + imports the library +* Prevent a documentation-only merge request from being merged before the MR + implementing the feature to be documented +* Require an MR updating a permissions matrix to be merged before merging an + MR from someone who hasn't yet been granted permissions + +It is common for a single logical change to span several merge requests. These +MRs may all be in a single project, or they may be spread out across multiple +projects, and the order in which they are merged can be significant. + +For example, given a project `mycorp/awesome-project` that imports a library +at `myfriend/awesome-lib`, adding a feature in `awesome-project` may **also** +require changes to `awesome-lib`, and so necessitate two merge requests. Merging +the `awesome-project` MR before the `awesome-lib` one would break the `master` +branch. + +The `awesome-project` MR could be [marked as WIP](work_in_progress_merge_requests.md), +and the reason for the WIP stated included in the comments. However, this +requires the state of the `awesome-lib` MR to be manually tracked, and doesn't +scale well if the `awesome-project` MR depends on changes to **several** other +projects. + +By marking the `awesome-project` MR as blocked on the `awesome-lib` MR instead, +the status of the dependency is automatically tracked by GitLab, and the WIP +state can be used to communicate the readiness of the code in each individual +MR instead. + +## Configuration + +To continue the above example, you can configure a block when creating the +new MR in `awesome-project` (or by editing it, if it already exists). The block +needs to be configured on the MR that will be **blocked**, rather than on the +**blocking** MR. There is a "Blocking merge requests" section in the form: + +![Blocking merge requests form control](img/edit_blocking_merge_requests.png) + +Anyone who can edit a merge request can change the list of blocking merge +requests. + +New blocks can be added by reference, by URL, or by using autcompletion. To +remove a block, press the "X" by its reference. + +As blocks can be specified across projects, it's possible that someone else has +added a block for a merge request in a project you don't have access to. These +are shown as a simple count: + +![Blocking merge requests form control with inaccessible MRs](img/edit_blocking_merge_requests_inaccessible.png) + +If necessary, you can remove all the blocks like this by pressing the "X", just +as you would for a single, visible block. + +Once you're finished, press the "Save changes" button to submit the request, or +"Cancel" to return without making any changes. + +The list of configured blocks, and the status of each one, is shown in the merge +request widget: + +![Blocking merge requests in merge request widget](img/show_blocking_merge_requests_in_mr_widget.png) + +Until all blocking merge requests have, themselves, been merged, the "Merge" +button will be disabled. In particular, note that **closed** merge requests +still block their dependents - it is impossible to automatically determine if +merge requests that were blocked by that MR when it was open, are still blocked +when it is closed. + +If a merge request has been closed **and** the block is no longer relevant, it +must be removed as a blocking MR, following the instructions above, before +merge. + +## Limitations + +* API support: [gitlab-ee#12551](https://gitlab.com/gitlab-org/gitlab-ee/issues/12551) +* Blocking relationships are not preserved across project export/import: [gitlab-ee#12549](https://gitlab.com/gitlab-org/gitlab-ee/issues/12549) +* Complex merge order dependencies are not supported: [gitlab-ee#11393](https://gitlab.com/gitlab-org/gitlab-ee/issues/11393) + +The last item merits a little more explanation. Blocking merge requests can be +described as a graph of dependencies. The simplest possible graph has one +merge request blocking another: + +```mermaid +graph LR; + myfriend/awesome-lib!10-->mycorp/awesome-project!100; +``` + +A more complex (and still supported) graph might have several MRs blocking +another from being merged: + +```mermaid +graph LR; + myfriend/awesome-lib!10-->mycorp/awesome-project!100; + herfriend/another-lib!1-->mycorp/awesome-project!100; +``` + +We also support one MR blocking several others from being merged: + +```mermaid +graph LR; + herfriend/another-lib!1-->myfriend/awesome-lib!10; + herfriend/another-lib!1-->mycorp/awesome-project!100; +``` + +What is **not** supported is a "deep", or "nested" graph of dependencies, e.g.: + +```mermaid +graph LR; + herfriend/another-lib!1-->myfriend/awesome-lib!10; + myfriend/awesome-lib!10-->mycorp/awesome-project!100; +``` + +In this example, `myfriend/awesome-lib!10` would be blocked from being merged by +`herfriend/another-lib!1`, and would also block `mycorp/awesome-project!100` +from being merged. This is **not** yet supported. + diff --git a/doc/user/project/merge_requests/img/edit_blocking_merge_requests.png b/doc/user/project/merge_requests/img/edit_blocking_merge_requests.png new file mode 100644 index 0000000000000000000000000000000000000000..0fe26d602e38ee508437f676908c710e27168dde GIT binary patch literal 9926 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 xCTYTfSx`TNjL%hQ1h@)|)r|lE literal 0 HcmV?d00001 diff --git a/doc/user/project/merge_requests/img/edit_blocking_merge_requests_inaccessible.png b/doc/user/project/merge_requests/img/edit_blocking_merge_requests_inaccessible.png new file mode 100644 index 0000000000000000000000000000000000000000..a1003c41c2285809a1c49f653ccdfd46f78e7ffb GIT binary patch literal 10867 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 literal 0 HcmV?d00001 diff --git a/doc/user/project/merge_requests/img/show_blocking_merge_requests_in_mr_widget.png b/doc/user/project/merge_requests/img/show_blocking_merge_requests_in_mr_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..a1241f9e38c37e31adf9b801f1048cff7149d7c3 GIT binary patch literal 27089 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 RFfcGMc)I$ztaD0e0ss^!Wqkku literal 0 HcmV?d00001 diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 8c28e24683f0c5..08a5d2e03a396c 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -47,6 +47,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also: - Analyze your dependencies for vulnerabilities with [Dependency Scanning](../../application_security/dependency_scanning/index.md) **(ULTIMATE)** - Analyze your Docker images for vulnerabilities with [Container Scanning](../../application_security/container_scanning/index.md) **(ULTIMATE)** - Determine the performance impact of changes with [Browser Performance Testing](#browser-performance-testing-premium) **(PREMIUM)** +- Specify merge order dependencies with [Blocking Merge Requests](#blocking-merge-requests-premium) **(PREMIUM)** ## Use cases @@ -412,6 +413,21 @@ GitLab runs the [Sitespeed.io container][sitespeed-container] and displays the d [Read more about Browser Performance Testing.](browser_performance_testing.md) +## Blocking Merge Requests **(PREMIUM)** + +> Introduced in [GitLab Premium][products] 12.2. + +A single logical change may be split across several merge requests, and perhaps +even across several projects. When this happens, the order in which MRs are +merged is important. + +GitLab allows you to specify that a merge request is blocked by other MRs. With +this relationship in place, the merge request cannot be merged until all of its +blockers have also been merged, helping to maintain the consistency of a single +logical change. + +[Read more about Blocking Merge Requests.](blocking_merge_requests.md) + ## Security reports **(ULTIMATE)** GitLab can scan and report any vulnerabilities found in your project. -- GitLab