Commit a0860573 authored by Kamil Trzciński's avatar Kamil Trzciński

Automatically retarget merge requests

This retargets another MR once a one
that it was targeting was merged.
parent 02d31976
......@@ -9,6 +9,8 @@ module MergeRequests
class PostMergeService < MergeRequests::BaseService
include RemovesRefs
MAX_RETARGET_MERGE_REQUESTS = 4
def execute(merge_request)
merge_request.mark_as_merged
close_issues(merge_request)
......@@ -18,6 +20,7 @@ module MergeRequests
merge_request_activity_counter.track_merge_mr_action(user: current_user)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
retarget_chain_merge_requests(merge_request)
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
merge_request.update_project_counter_caches
delete_non_latest_diffs(merge_request)
......@@ -28,6 +31,31 @@ module MergeRequests
private
def retarget_chain_merge_requests(merge_request)
return unless Feature.enabled?(:retarget_merge_requests, merge_request.target_project)
# we can only retarget MRs that are targeting the same project
return unless merge_request.for_same_project?
# find another merge requests that
# - as a target have a current source project and branch
other_merge_requests = merge_request.source_project
.merge_requests
.opened
.by_target_branch(merge_request.source_branch)
.preload_source_project
.at_most(MAX_RETARGET_MERGE_REQUESTS)
other_merge_requests.find_each do |other_merge_request|
# Update only MRs on projects that we have access to
next unless can?(current_user, :update_merge_request, other_merge_request.source_project)
::MergeRequests::UpdateService
.new(other_merge_request.source_project, current_user, target_branch: merge_request.target_branch)
.execute(other_merge_request)
end
end
def close_issues(merge_request)
return unless merge_request.target_branch == project.default_branch
......
---
title: Automatically retarget merge requests
merge_request: 53710
author:
type: added
---
name: retarget_merge_requests
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53710
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/320895
milestone: '13.9'
type: development
group: group::memory
default_enabled: false
......@@ -626,3 +626,12 @@ Set the limit to `0` to allow any file size.
### Package versions returned
When asking for versions of a given NuGet package name, the GitLab Package Registry returns a maximum of 300 versions.
## Automatically retargeting merge requests
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320902) in GitLab 13.9.
Maximum of 4 merge requests point to the currently being merged
will be automatically retargeted upon merge.
More information can be found in the [Automatically retargeting merge requests](../user/project/merge_requests/getting_started.md#automatically-retargeting-merge-requests).
......@@ -194,6 +194,33 @@ is set for deletion, the merge request widget displays the
![Delete source branch status](img/remove_source_branch_status.png)
### Automatically retargeting merge requests
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320902) in GitLab 13.9.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-automatically-retargeting-merge-requests). **(FREE SELF)**
It is common to have a number of merge requests in a chain,
when one depends on another. For example:
- Merge Request A: merge `feature-A` into `master`
- Merge Request B: merge `feature-B` into `feature-A`
We can distinguish two workflows:
- the MR A is merged into `master` first, and then MR B is retargeted onto `master`
- the MR B is merged into `feature-A` branch, and then MR A is merged into `master`
Upon merge of `MR A` does automatically retarget the `MR B` onto `master`.
This relieves user from having to perform this operation manually.
This feature works only in following cases:
- The MR A needs to be in a main project (forks are not supported), as we, GitLab, cannot change the target project of MR B
- Only 4 MR's of type B are retargeted automatically this way
## Recommendations and best practices for Merge Requests
- When working locally in your branch, add multiple commits and only push when
......@@ -230,3 +257,22 @@ Feature.disable(:reviewer_approval_rules)
# For a single project
Feature.disable(:reviewer_approval_rules, Project.find(<project id>))
```
### Enable or disable Automatically retargeting merge requests **(FREE SELF)**
Automatically retargeting merge requests is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to disable it.
To enable it:
```ruby
Feature.enable(:retarget_merge_requests)
```
To disable it:
```ruby
Feature.disable(:retarget_merge_requests)
```
......@@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe MergeRequests::PostMergeService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:project) { merge_request.project }
include ProjectForksHelper
let_it_be(:user) { create(:user) }
let_it_be(:merge_request, reload: true) { create(:merge_request, assignees: [user]) }
let_it_be(:project) { merge_request.project }
subject { described_class.new(project, user).execute(merge_request) }
......@@ -128,5 +130,125 @@ RSpec.describe MergeRequests::PostMergeService do
expect(deploy_job.reload.canceled?).to be false
end
end
context 'for a merge request chain' do
context 'when there is another MR' do
let!(:another_merge_request) do
create(:merge_request,
source_project: source_project,
source_branch: 'my-awesome-feature',
target_project: merge_request.source_project,
target_branch: merge_request.source_branch
)
end
shared_examples 'retargets merge request' do
it 'another merge request is retargeted' do
expect(SystemNoteService)
.to receive(:change_branch).once
.with(another_merge_request, another_merge_request.project, user, 'target',
merge_request.source_branch, merge_request.target_branch)
expect { subject }.to change { another_merge_request.reload.target_branch }
.from(merge_request.source_branch)
.to(merge_request.target_branch)
end
context 'when FF retarget_merge_requests is disabled' do
before do
stub_feature_flags(retarget_merge_requests: false)
end
it 'another merge request is unchanged' do
expect { subject }.not_to change { another_merge_request.reload.target_branch }
.from(merge_request.source_branch)
end
end
end
shared_examples 'does not retarget merge request' do
it 'another merge request is unchanged' do
expect { subject }.not_to change { another_merge_request.reload.target_branch }
.from(merge_request.source_branch)
end
end
context 'in the same project' do
let(:source_project) { project }
it_behaves_like 'retargets merge request'
context 'and is closed' do
before do
another_merge_request.close
end
it_behaves_like 'does not retarget merge request'
end
context 'and is merged' do
before do
another_merge_request.mark_as_merged
end
it_behaves_like 'does not retarget merge request'
end
end
context 'in forked project' do
let!(:source_project) { fork_project(project) }
context 'when user has access to source project' do
before do
source_project.add_developer(user)
end
it_behaves_like 'retargets merge request'
end
context 'when user does not have access to source project' do
it_behaves_like 'does not retarget merge request'
end
end
context 'and current and another MR is from a fork' do
let(:project) { create(:project) }
let(:source_project) { fork_project(project) }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
target_project: project
)
end
before do
source_project.add_developer(user)
end
it_behaves_like 'does not retarget merge request'
end
end
context 'when many merge requests are to be retargeted' do
let!(:many_merge_requests) do
create_list(:merge_request, 10, :unique_branches,
source_project: merge_request.source_project,
target_project: merge_request.source_project,
target_branch: merge_request.source_branch
)
end
it 'retargets only 4 of them' do
subject
expect(many_merge_requests.each(&:reload).pluck(:target_branch).tally)
.to eq(
merge_request.source_branch => 6,
merge_request.target_branch => 4
)
end
end
end
end
end
......@@ -633,31 +633,37 @@ RSpec.describe MergeRequests::RefreshService do
end
context 'merge request metrics' do
let(:issue) { create :issue, project: @project }
let(:commit_author) { create :user }
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project) }
let(:commit) { project.commit }
before do
project.add_developer(commit_author)
project.add_developer(user)
allow(commit).to receive_messages(
safe_message: "Closes #{issue.to_reference}",
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email,
author_name: user.name,
author_email: user.email,
committed_date: Time.current
)
allow_any_instance_of(MergeRequest).to receive(:commits).and_return(CommitCollection.new(@project, [commit], 'feature'))
end
context 'when the merge request is sourced from the same project' do
it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project)
refresh_service = service.new(@project, @user)
allow_any_instance_of(MergeRequest).to receive(:commits).and_return(
CommitCollection.new(project, [commit], 'close-by-commit')
)
merge_request = create(:merge_request,
target_branch: 'master',
source_branch: 'close-by-commit',
source_project: project)
refresh_service = service.new(project, user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit')
issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
expect(issue_ids).to eq([issue.id])
......@@ -666,16 +672,21 @@ RSpec.describe MergeRequests::RefreshService do
context 'when the merge request is sourced from a different project' do
it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
forked_project = fork_project(@project, @user, repository: true)
forked_project = fork_project(project, user, repository: true)
allow_any_instance_of(MergeRequest).to receive(:commits).and_return(
CommitCollection.new(forked_project, [commit], 'close-by-commit')
)
merge_request = create(:merge_request,
target_branch: 'master',
source_branch: 'feature',
target_project: @project,
target_project: project,
source_branch: 'close-by-commit',
source_project: forked_project)
refresh_service = service.new(@project, @user)
refresh_service = service.new(forked_project, user)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit')
issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
expect(issue_ids).to eq([issue.id])
......
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