Commit 49b835c5 authored by Douwe Maan's avatar Douwe Maan

Merge branch '13035-rename-blocking-mrs' into 'master'

Rename blocking MRs to cross-project MR dependencies

Closes #13035

See merge request gitlab-org/gitlab-ee!14934
parents 82d5bb43 e5004b70
......@@ -33,8 +33,6 @@
= 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
......
......@@ -23,6 +23,7 @@
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
= render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
= render_if_exists "shared/issuable/form/merge_request_blocks", issuable: issuable, form: form
- if has_due_date
.col-lg-6
......
---
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.
......@@ -47,7 +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)**
- Specify merge order dependencies with [Cross-project Merge Request Dependencies](#cross-project-merge-request-dependencies-premium) **(PREMIUM)**
## Use cases
......@@ -451,20 +451,20 @@ 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)**
## Cross-project Merge Request Dependencies **(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.
A single logical change may be split across several merge requests, 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
GitLab allows you to specify that a merge request depends on 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.
dependencies have also been merged, helping to maintain the consistency of a
single logical change.
[Read more about Blocking Merge Requests.](blocking_merge_requests.md)
[Read more about cross-project merge request dependencies.](merge_request_dependencies.md)
## Security reports **(ULTIMATE)**
......
---
type: reference, concepts
---
# Cross-project merge request dependencies **(PREMIUM)**
> Introduced in GitLab Premium 12.2
Cross-project merge request dependencies allows a required order of merging
between merge requests in different projects to be expressed. If a
merge request "depends on" another, then it cannot be merged until its
dependency is itself merged.
NOTE: **Note:**
Merge requests dependencies are a **PREMIUM** feature, but this restriction is
only enforced for the dependent merge request. A merge request in a **CORE** or
**STARTER** project can be a dependency of a **PREMIUM** merge request, but not
vice-versa.
NOTE: **Note:**
A merge request can only depend on merge requests in a different project. Two
merge requests in the same project cannot depend on each other.
## 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 merge request
implementing the feature to be documented
* Require an merge request updating a permissions matrix to be merged before merging an
merge request from someone who hasn't yet been granted permissions
It is common for a single logical change to span several merge requests, 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` merge request before the `awesome-lib` one would
break the `master`branch.
The `awesome-project` merge request 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` merge request to be manually
tracked, and doesn't scale well if the `awesome-project` merge request
depends on changes to **several** other projects.
By making the `awesome-project` merge request depend on the
`awesome-lib` merge request instead, this relationship is
automatically tracked by GitLab, and the WIP state can be used to
communicate the readiness of the code in each individual merge request
instead.
## Configuration
To continue the above example, you can configure a dependency when creating the
new merge request in `awesome-project` (or by editing it, if it already exists).
The dependency needs to be configured on the **dependent** merge
request. There is a "Cross-project dependencies" section in the form:
![Cross-project dependencies form control](img/cross-project-dependencies-edit.png)
Anyone who can edit a merge request can change the list of dependencies.
New dependencies can be added by reference, or by URL. To remove a dependency,
press the "X" by its reference.
As dependencies are specified across projects, it's possible that someone else
has added a dependency for a merge request in a project you don't have access to.
These are shown as a simple count:
![Cross-project dependencies form control with inaccessible merge requests](img/cross-project-dependencies-edit-inaccessible.png)
If necessary, you can remove all the dependencies like this by pressing the "X",
just as you would for a single, visible dependency.
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 dependencies, and the status of each one, is shown in the
merge request widget:
![Cross-project dependencies in merge request widget](img/cross-project-dependencies-view.png)
Until all dependencies have, themselves, been merged, the "Merge"
button will be disabled for the dependent merge request. In
particular, note that **closed** merge request still prevent their
dependents from being merged - it is impossible to automatically
determine whether the dependency expressed by a closed merge request
has been satisfied in some other way or not.
If a merge request has been closed **and** the dependency is no longer relevant,
it must be removed as a dependency, following the instructions above, before
merge.
## Limitations
* API support: [gitlab-ee#12551](https://gitlab.com/gitlab-org/gitlab-ee/issues/12551)
* Dependencies 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. Dependencies between merge
requests can be described as a graph of relationships. The simplest possible
graph has one merge request that depends upon another:
```mermaid
graph LR;
myfriend/awesome-lib!10-->mycorp/awesome-project!100;
```
A more complex (and still supported) graph might have one merge request that
directly depends upon several others:
```mermaid
graph LR;
myfriend/awesome-lib!10-->mycorp/awesome-project!100;
herfriend/another-lib!1-->mycorp/awesome-project!100;
```
Several different merge requests can also directly depend upon the
same merge request:
```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` depends on `herfriend/another-lib!1`,
and is itself a dependent of `mycorp/awesome-project!100`. This means that
`myfriend/awesome-lib!10` becomes an **indirect** dependency of
`mycorp/awesome-project!100`, which is not yet supported.
......@@ -26,11 +26,6 @@ export default {
hasFieldBeenTouched: false,
};
},
computed: {
autoCompleteSources() {
return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources;
},
},
methods: {
onAddIssuable({ untouchedRawReferences, touchedReference }) {
this.hasFieldBeenTouched = true;
......@@ -77,8 +72,6 @@ export default {
:references="references"
:input-value="inputValue"
:issuable-type="$options.issuableTypesMap.MERGE_REQUEST"
:auto-complete-options="{ mergeRequests: true }"
:auto-complete-sources="autoCompleteSources"
@addIssuableFormInput="onAddIssuable"
@pendingIssuableRemoveRequest="removeReference"
@addIssuableFormBlur="onBlur"
......
......@@ -10,19 +10,19 @@ export const autoCompleteTextMap = {
true: {
[issuableTypesMap.ISSUE]: __(' or <#issue id>'),
[issuableTypesMap.EPIC]: __(' or <#epic id>'),
[issuableTypesMap.MERGE_REQUEST]: __(' or <#merge request id>'),
[issuableTypesMap.MERGE_REQUEST]: __(' or <!merge request id>'),
},
false: {
[issuableTypesMap.ISSUE]: '',
[issuableTypesMap.EPIC]: '',
[issuableTypesMap.MERGE_REQUEST]: '',
[issuableTypesMap.MERGE_REQUEST]: __(' or references (e.g. path/to/project!merge_request_id)'),
},
};
export const inputPlaceholderTextMap = {
[issuableTypesMap.ISSUE]: __('Paste issue link'),
[issuableTypesMap.EPIC]: __('Paste epic link'),
[issuableTypesMap.MERGE_REQUEST]: __('Paste a merge request link'),
[issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'),
};
export const relatedIssuesRemoveErrorMap = {
......
......@@ -65,15 +65,15 @@ export default {
blockedByText() {
if (this.closedCount > 0 && this.closedCount === this.unmergedCount) {
return n__(
'Blocked by <strong>%d closed</strong> merge request.',
'Blocked by <strong>%d closed</strong> merge requests.',
'Depends on <strong>%d closed</strong> merge request.',
'Depends on <strong>%d closed</strong> merge requests.',
this.closedCount,
);
}
const mainText = n__(
'Blocked by %d merge request',
'Blocked by %d merge requests',
'Depends on %d merge request being merged',
'Depends on %d merge requests being merged',
this.unmergedCount,
);
......@@ -104,7 +104,7 @@ export default {
issue-item-class="p-0"
>
<template v-slot:success>
{{ __('No blocking merge requests ') }}
{{ __('All cross-project dependencies have merged') }}
<span class="text-secondary">
{{
sprintf(__('(%{mrCount} merged)'), {
......
......@@ -44,5 +44,8 @@ class MergeRequestBlock < ApplicationRecord
errors.add(:blocked_merge_request, _('cannot block others')) if
blocked_merge_request.blocks_as_blocker.any?
errors.add(:blocked_merge_request, _('cannot be in the same project')) if
blocked_merge_request.target_project_id == blocking_merge_request.target_project_id
end
end
......@@ -8,10 +8,12 @@
- 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'
= form.label :blocking_merge_request_references, _('Cross-project dependencies'), 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",
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) }
.form-text.text-muted
= _('List the merge requests that must be merged before this one.')
......@@ -23,10 +23,10 @@ describe 'User creates a merge request with blocking MRs', :js do
visit(project_new_merge_request_path(project, merge_request: mr_params))
fill_in 'Blocking merge requests', with: other_mr.to_reference(full: true)
fill_in 'Cross-project dependencies', with: other_mr.to_reference(full: true)
click_button('Submit merge request')
expect(page).to have_content('Blocked by 1 merge request')
expect(page).to have_content('Depends on 1 merge request')
end
end
......@@ -38,7 +38,7 @@ describe 'User creates a merge request with blocking MRs', :js do
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')
expect(page).not_to have_content('Cross-project dependencies')
end
end
end
......@@ -19,6 +19,8 @@ describe "User edits merge request with blocking MRs", :js do
end
context 'user can see the other MR' do
let(:blocked_text) { 'Depends on 1 merge request' }
before do
other_mr.target_project.team.add_developer(user)
end
......@@ -26,11 +28,11 @@ describe "User edits merge request with blocking MRs", :js do
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)
fill_in 'Cross-project dependencies', with: other_mr.to_reference(full: true)
click_button 'Save changes'
expect(page).to have_content('Blocked by 1 merge request')
expect(page).to have_content(blocked_text)
end
it 'can see and remove an existing blocking MR' do
......@@ -43,7 +45,7 @@ describe "User edits merge request with blocking MRs", :js do
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(blocked_text)
expect(page).not_to have_content(other_mr.to_reference(full: true))
end
end
......@@ -52,11 +54,11 @@ describe "User edits merge request with blocking MRs", :js 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)
fill_in 'Cross-project dependencies', with: 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('Depends on 1 merge request')
end
it 'sees the existing MR as hidden and can remove it' do
......@@ -69,7 +71,7 @@ describe "User edits merge request with blocking MRs", :js do
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('Depends on 1 merge request')
expect(page).not_to have_content(other_mr.to_reference(full: true))
end
end
......
......@@ -38,7 +38,7 @@ describe 'Merge Request > User views blocked MR', :js do
it 'is not mergeable' do
visit project_merge_request_path(project, blocked_mr)
expect(page).to have_content('Blocked by 1 merge request')
expect(page).to have_content('Depends on 1 merge request')
expect(page).to have_button('Merge', disabled: true)
click_button 'Expand'
......@@ -56,7 +56,7 @@ describe 'Merge Request > User views blocked MR', :js do
it 'is not mergeable' do
visit project_merge_request_path(project, blocked_mr)
expect(page).to have_content('Blocked by 1 merge request')
expect(page).to have_content('Depends on 1 merge request')
expect(page).to have_button('Merge', disabled: true)
click_button 'Expand'
......
......@@ -90,7 +90,7 @@ describe('BlockingMergeRequestsReport', () => {
createComponent();
expect(wrapper.vm.blockedByText).toBe(
'Blocked by 2 merge requests <strong>(1 closed)</strong>',
'Depends on 2 merge requests being merged <strong>(1 closed)</strong>',
);
});
......@@ -106,7 +106,7 @@ describe('BlockingMergeRequestsReport', () => {
createComponent();
expect(wrapper.vm.blockedByText).toEqual(
'Blocked by <strong>1 closed</strong> merge request.',
'Depends on <strong>1 closed</strong> merge request.',
);
});
});
......
......@@ -59,6 +59,14 @@ describe MergeRequestBlock do
expect(new_block).not_to be_valid
end
it 'forbids blocks from being intra-project' do
project = blocking_mr.target_project
intra_project_mr = create(:merge_request, :rebased, source_project: project, target_project: project)
block.blocked_merge_request = intra_project_mr
is_expected.not_to be_valid
end
end
describe '.bulk_insert' do
......
......@@ -50,13 +50,16 @@ msgstr[1] ""
msgid " or "
msgstr ""
msgid " or <!merge request id>"
msgstr ""
msgid " or <#epic id>"
msgstr ""
msgid " or <#issue id>"
msgstr ""
msgid " or <#merge request id>"
msgid " or references (e.g. path/to/project!merge_request_id)"
msgstr ""
msgid "%d comment"
......@@ -1162,6 +1165,9 @@ msgstr ""
msgid "All changes are committed"
msgstr ""
msgid "All cross-project dependencies have merged"
msgstr ""
msgid "All email addresses will be used to identify your commits."
msgstr ""
......@@ -2218,19 +2224,6 @@ msgstr ""
msgid "Blocked"
msgstr ""
msgid "Blocked by %d merge request"
msgid_plural "Blocked by %d merge requests"
msgstr[0] ""
msgstr[1] ""
msgid "Blocked by <strong>%d closed</strong> merge request."
msgid_plural "Blocked by <strong>%d closed</strong> merge requests."
msgstr[0] ""
msgstr[1] ""
msgid "Blocking merge requests"
msgstr ""
msgid "Blog"
msgstr ""
......@@ -4306,6 +4299,9 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
msgid "Cross-project dependencies"
msgstr ""
msgid "Current Branch"
msgstr ""
......@@ -4665,6 +4661,16 @@ msgstr ""
msgid "DependencyProxy|Toggle Dependency Proxy"
msgstr ""
msgid "Depends on %d merge request being merged"
msgid_plural "Depends on %d merge requests being merged"
msgstr[0] ""
msgstr[1] ""
msgid "Depends on <strong>%d closed</strong> merge request."
msgid_plural "Depends on <strong>%d closed</strong> merge requests."
msgstr[0] ""
msgstr[1] ""
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
......@@ -5360,6 +5366,9 @@ msgstr ""
msgid "Enter in your Phabricator Server URL and personal access token below"
msgstr ""
msgid "Enter merge request URLs"
msgstr ""
msgid "Enter the issue description"
msgstr ""
......@@ -8610,6 +8619,9 @@ msgstr ""
msgid "List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. \"1.1.1.1, 2.2.2.0/24\""
msgstr ""
msgid "List the merge requests that must be merged before this one."
msgstr ""
msgid "List view"
msgstr ""
......@@ -9599,9 +9611,6 @@ msgstr ""
msgid "No available namespaces to fork the project."
msgstr ""
msgid "No blocking merge requests "
msgstr ""
msgid "No branches found"
msgstr ""
......@@ -10204,9 +10213,6 @@ 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 ""
......@@ -17299,6 +17305,9 @@ msgstr ""
msgid "cannot be enabled unless all domains have TLS certificates"
msgstr ""
msgid "cannot be in the same project"
msgstr ""
msgid "cannot block others"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment