From c389a705f7b35a9ddcd36057ec6006d3612d6310 Mon Sep 17 00:00:00 2001 From: Max Coplan <mchcopl@yahoo.com> Date: Sun, 31 Jan 2021 17:00:54 -0500 Subject: [PATCH] Set default MR title/description to first multi-line commit Closes https://gitlab.com/gitlab-org/gitlab/-/issues/300479 When creating a merge request, the behaviour of the prefilled title and discription is different than the behaviour of squashed commit messages. For squashed commit messages, the behaviour is described in the [docs](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html#overview) as > Taken from the first multi-line commit message in the merge. However, for merge requests, the current behavior is to use the branch name. This commit sets the prefilled MR title/description to the first multi-line commit in the MR, if applicable. Otherwise falling back to the previous behaviour. --- app/models/merge_request.rb | 9 +- app/services/merge_requests/build_service.rb | 16 ++- ...-change-prefilled-mr-title-description.yml | 5 + .../merge_requests/creating_merge_requests.md | 9 +- .../merge_requests/build_service_spec.rb | 123 +++++++++++++----- 5 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 changelogs/unreleased/300479-change-prefilled-mr-title-description.yml diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7b8c4e844d447..07092e5068044 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1282,7 +1282,14 @@ def default_merge_commit_message(include_description: false) # Returns the oldest multi-line commit message, or the MR title if none found def default_squash_commit_message strong_memoize(:default_squash_commit_message) do - recent_commits.without_merge_commits.reverse_each.find(&:description?)&.safe_message || title + first_multiline_commit&.safe_message || title + end + end + + # Returns the oldest multi-line commit + def first_multiline_commit + strong_memoize(:first_multiline_commit) do + recent_commits.without_merge_commits.reverse_each.find(&:description?) end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 12c901aa1a153..e4d3c91d13e1f 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -58,6 +58,7 @@ def execute :compare_commits, :wip_title, :description, + :first_multiline_commit, :errors, to: :merge_request @@ -196,7 +197,8 @@ def target_branch_exists? # interpreted as the user wants to close that issue on this project. # # For example: - # - Issue 112 exists, title: Emoji don't show up in commit title + # - Issue 112 exists + # - title: Emoji don't show up in commit title # - Source branch is: 112-fix-mep-mep # # Will lead to: @@ -205,7 +207,7 @@ def target_branch_exists? # more than one commit in the MR # def assign_title_and_description - assign_title_and_description_from_single_commit + assign_title_and_description_from_commits merge_request.title ||= title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker merge_request.title ||= source_branch.titleize.humanize merge_request.title = wip_title if compare_commits.empty? @@ -240,12 +242,16 @@ def append_closes_description end end - def assign_title_and_description_from_single_commit + def assign_title_and_description_from_commits commits = compare_commits - return unless commits&.count == 1 + if commits&.count == 1 + commit = commits.first + else + commit = first_multiline_commit + return unless commit + end - commit = commits.first merge_request.title ||= commit.title merge_request.description ||= commit.description.try(:strip) end diff --git a/changelogs/unreleased/300479-change-prefilled-mr-title-description.yml b/changelogs/unreleased/300479-change-prefilled-mr-title-description.yml new file mode 100644 index 0000000000000..6b360bde0ae26 --- /dev/null +++ b/changelogs/unreleased/300479-change-prefilled-mr-title-description.yml @@ -0,0 +1,5 @@ +--- +title: Prefill first multiline commit message for new MRs +merge_request: 52984 +author: Max Coplan @vegerot +type: changed diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md index dfd160fc91b8e..9bd520b66a7d6 100644 --- a/doc/user/project/merge_requests/creating_merge_requests.md +++ b/doc/user/project/merge_requests/creating_merge_requests.md @@ -32,9 +32,12 @@ button and start a merge request from there. On the **New Merge Request** page, start by filling in the title and description for the merge request. If there are already -commits on the branch, the title is prefilled with the first -line of the first commit message, and the description is -prefilled with any additional lines in the commit message. +commits on the branch, the title is prefilled with either: +- The first line of the first multi-line commit message + and the description is prefilled with any additional lines + in that commit message. +- The branch name, if there are no multi-line commit messages. + The title is the only field that is mandatory in all cases. From there, you can fill it with information (title, description, diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 22b3456708f88..8adf6d69f7316 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -19,8 +19,21 @@ let(:label_ids) { [] } let(:merge_request) { service.execute } let(:compare) { double(:compare, commits: commits) } - let(:commit_1) { double(:commit_1, sha: 'f00ba7', safe_message: "Initial commit\n\nCreate the app") } - let(:commit_2) { double(:commit_2, sha: 'f00ba7', safe_message: 'This is a bad commit message!') } + let(:commit_1) do + double(:commit_1, sha: 'f00ba6', safe_message: 'Initial commit', + gitaly_commit?: false, id: 'f00ba6', parent_ids: ['f00ba5']) + end + + let(:commit_2) do + double(:commit_2, sha: 'f00ba7', safe_message: "Closes #1234 Second commit\n\nCreate the app", + gitaly_commit?: false, id: 'f00ba7', parent_ids: ['f00ba6']) + end + + let(:commit_3) do + double(:commit_3, sha: 'f00ba8', safe_message: 'This is a bad commit message!', + gitaly_commit?: false, id: 'f00ba8', parent_ids: ['f00ba7']) + end + let(:commits) { nil } let(:params) do @@ -47,6 +60,7 @@ def stub_compare allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) allow(project).to receive(:commit).and_return(commit_1) allow(project).to receive(:commit).and_return(commit_2) + allow(project).to receive(:commit).and_return(commit_3) end shared_examples 'allows the merge request to be created' do @@ -137,7 +151,7 @@ def stub_compare context 'when target branch is missing' do let(:target_branch) { nil } - let(:commits) { Commit.decorate([commit_1], project) } + let(:commits) { Commit.decorate([commit_2], project) } before do stub_compare @@ -199,8 +213,8 @@ def stub_compare end context 'one commit in the diff' do - let(:commits) { Commit.decorate([commit_1], project) } - let(:commit_description) { commit_1.safe_message.split(/\n+/, 2).last } + let(:commits) { Commit.decorate([commit_2], project) } + let(:commit_description) { commit_2.safe_message.split(/\n+/, 2).last } before do stub_compare @@ -209,7 +223,7 @@ def stub_compare it_behaves_like 'allows the merge request to be created' it 'uses the title of the commit as the title of the merge request' do - expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) + expect(merge_request.title).to eq(commit_2.safe_message.split("\n").first) end it 'uses the description of the commit as the description of the merge request' do @@ -225,10 +239,10 @@ def stub_compare end context 'commit has no description' do - let(:commits) { Commit.decorate([commit_2], project) } + let(:commits) { Commit.decorate([commit_3], project) } it 'uses the title of the commit as the title of the merge request' do - expect(merge_request.title).to eq(commit_2.safe_message) + expect(merge_request.title).to eq(commit_3.safe_message) end it 'sets the description to nil' do @@ -257,7 +271,7 @@ def stub_compare end it 'uses the title of the commit as the title of the merge request' do - expect(merge_request.title).to eq('Initial commit') + expect(merge_request.title).to eq('Closes #1234 Second commit') end it 'appends the closing description' do @@ -310,8 +324,8 @@ def stub_compare end end - context 'more than one commit in the diff' do - let(:commits) { Commit.decorate([commit_1, commit_2], project) } + context 'no multi-line commit messages in the diff' do + let(:commits) { Commit.decorate([commit_1, commit_3], project) } before do stub_compare @@ -365,6 +379,55 @@ def stub_compare end end end + end + + context 'a multi-line commit message in the diff' do + let(:commits) { Commit.decorate([commit_1, commit_2, commit_3], project) } + + before do + stub_compare + end + + it_behaves_like 'allows the merge request to be created' + + it 'uses the first line of the first multi-line commit message as the title' do + expect(merge_request.title).to eq('Closes #1234 Second commit') + end + + it 'adds the remaining lines of the first multi-line commit message as the description' do + expect(merge_request.description).to eq('Create the app') + end + + context 'when the source branch matches an issue' do + where(:issue_tracker, :source_branch, :title, :closing_message) do + :jira | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123' + :jira | 'fix-issue' | 'Fix issue' | nil + :custom_issue_tracker | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123' + :custom_issue_tracker | 'fix-issue' | 'Fix issue' | nil + :internal | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123' + :internal | 'fix-issue' | 'Fix issue' | nil + :internal | '124-fix-issue' | '124 fix issue' | nil + end + + with_them do + before do + if issue_tracker == :internal + issue.update!(iid: 123) + else + create(:"#{issue_tracker}_service", project: project) + project.reload + end + end + + it 'sets the correct title' do + expect(merge_request.title).to eq('Closes #1234 Second commit') + end + + it 'sets the closing description' do + expect(merge_request.description).to eq("Create the app#{closing_message ? "\n\n" + closing_message : ''}") + end + end + end context 'when the issue is not accessible to user' do let(:source_branch) { "#{issue.iid}-fix-issue" } @@ -373,12 +436,12 @@ def stub_compare project.team.truncate end - it 'uses branch title as the merge request title' do - expect(merge_request.title).to eq("#{issue.iid} fix issue") + it 'uses the first line of the first multi-line commit message as the title' do + expect(merge_request.title).to eq('Closes #1234 Second commit') end - it 'does not set a description' do - expect(merge_request.description).to be_nil + it 'adds the remaining lines of the first multi-line commit message as the description' do + expect(merge_request.description).to eq('Create the app') end end @@ -386,12 +449,12 @@ def stub_compare let(:source_branch) { "#{issue.iid}-fix-issue" } let(:issue_confidential) { true } - it 'uses the title of the branch as the merge request title' do - expect(merge_request.title).to eq("#{issue.iid} fix issue") + it 'uses the first line of the first multi-line commit message as the title' do + expect(merge_request.title).to eq('Closes #1234 Second commit') end - it 'does not set a description' do - expect(merge_request.description).to be_nil + it 'adds the remaining lines of the first multi-line commit message as the description' do + expect(merge_request.description).to eq('Create the app') end end end @@ -399,7 +462,7 @@ def stub_compare context 'source branch does not exist' do before do allow(project).to receive(:commit).with(source_branch).and_return(nil) - allow(project).to receive(:commit).with(target_branch).and_return(commit_1) + allow(project).to receive(:commit).with(target_branch).and_return(commit_2) end it_behaves_like 'forbids the merge request from being created' do @@ -409,7 +472,7 @@ def stub_compare context 'target branch does not exist' do before do - allow(project).to receive(:commit).with(source_branch).and_return(commit_1) + allow(project).to receive(:commit).with(source_branch).and_return(commit_2) allow(project).to receive(:commit).with(target_branch).and_return(nil) end @@ -433,7 +496,7 @@ def stub_compare context 'upstream project has disabled merge requests' do let(:upstream_project) { create(:project, :merge_requests_disabled) } let(:project) { create(:project, forked_from_project: upstream_project) } - let(:commits) { Commit.decorate([commit_1], project) } + let(:commits) { Commit.decorate([commit_2], project) } it 'sets target project correctly' do expect(merge_request.target_project).to eq(project) @@ -441,8 +504,8 @@ def stub_compare end context 'target_project is set and accessible by current_user' do - let(:target_project) { create(:project, :public, :repository)} - let(:commits) { Commit.decorate([commit_1], project) } + let(:target_project) { create(:project, :public, :repository) } + let(:commits) { Commit.decorate([commit_2], project) } it 'sets target project correctly' do expect(merge_request.target_project).to eq(target_project) @@ -450,8 +513,8 @@ def stub_compare end context 'target_project is set but not accessible by current_user' do - let(:target_project) { create(:project, :private, :repository)} - let(:commits) { Commit.decorate([commit_1], project) } + let(:target_project) { create(:project, :private, :repository) } + let(:commits) { Commit.decorate([commit_2], project) } it 'sets target project correctly' do expect(merge_request.target_project).to eq(project) @@ -469,8 +532,8 @@ def stub_compare end context 'source_project is set and accessible by current_user' do - let(:source_project) { create(:project, :public, :repository)} - let(:commits) { Commit.decorate([commit_1], project) } + let(:source_project) { create(:project, :public, :repository) } + let(:commits) { Commit.decorate([commit_2], project) } before do # To create merge requests _from_ a project the user needs at least @@ -484,8 +547,8 @@ def stub_compare end context 'source_project is set but not accessible by current_user' do - let(:source_project) { create(:project, :private, :repository)} - let(:commits) { Commit.decorate([commit_1], project) } + let(:source_project) { create(:project, :private, :repository) } + let(:commits) { Commit.decorate([commit_2], project) } it 'sets source project correctly' do expect(merge_request.source_project).to eq(project) -- GitLab