Skip to content
Snippets Groups Projects
Commit 2b7dd017 authored by Luke Duncalfe's avatar Luke Duncalfe Committed by Nick Thomas
Browse files

Allow custom squash commit messages

parent 5bfa8e2f
No related branches found
No related tags found
No related merge requests found
Showing
with 191 additions and 54 deletions
Loading
Loading
@@ -32,10 +32,10 @@ export default class MergeRequestStore {
this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
this.mergeCommitSha = data.merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
Loading
Loading
Loading
Loading
@@ -240,7 +240,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
 
def merge_params_attributes
[:should_remove_source_branch, :commit_message, :squash]
[:should_remove_source_branch, :commit_message, :squash_commit_message, :squash]
end
 
def merge_when_pipeline_succeeds_active?
Loading
Loading
Loading
Loading
@@ -39,7 +39,8 @@ module Types
field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
field :diff_head_sha, GraphQL::STRING_TYPE, null: true
field :merge_commit_message, GraphQL::STRING_TYPE, null: true
field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage"
field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true
field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false
field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true
Loading
Loading
Loading
Loading
@@ -379,7 +379,7 @@ class Commit
end
 
def merge_commit?
parents.size > 1
parent_ids.size > 1
end
 
def merged_merge_request(current_user)
Loading
Loading
Loading
Loading
@@ -3,6 +3,7 @@
# A collection of Commit instances for a specific project and Git reference.
class CommitCollection
include Enumerable
include Gitlab::Utils::StrongMemoize
 
attr_reader :project, :ref, :commits
 
Loading
Loading
@@ -20,11 +21,17 @@ class CommitCollection
end
 
def committers
emails = commits.reject(&:merge_commit?).map(&:committer_email).uniq
emails = without_merge_commits.map(&:committer_email).uniq
 
User.by_any_email(emails)
end
 
def without_merge_commits
strong_memoize(:without_merge_commits) do
commits.reject(&:merge_commit?)
end
end
# Sets the pipeline status for every commit.
#
# Setting this status ahead of time removes the need for running a query for
Loading
Loading
Loading
Loading
@@ -939,7 +939,7 @@ class MergeRequest < ActiveRecord::Base
self.target_project.repository.branch_exists?(self.target_branch)
end
 
def merge_commit_message(include_description: false)
def default_merge_commit_message(include_description: false)
closes_issues_references = visible_closing_issues_for.map do |issue|
issue.to_reference(target_project)
end
Loading
Loading
@@ -959,6 +959,13 @@ class MergeRequest < ActiveRecord::Base
message.join("\n\n")
end
 
# 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
commits.without_merge_commits.reverse.find(&:description?)&.safe_message || title
end
end
def reset_merge_when_pipeline_succeeds
return unless merge_when_pipeline_succeeds?
 
Loading
Loading
@@ -967,6 +974,7 @@ class MergeRequest < ActiveRecord::Base
if merge_params
merge_params.delete('should_remove_source_branch')
merge_params.delete('commit_message')
merge_params.delete('squash_commit_message')
end
 
self.save
Loading
Loading
Loading
Loading
@@ -1030,12 +1030,12 @@ class Repository
remote_branch: merge_request.target_branch)
end
 
def squash(user, merge_request)
def squash(user, merge_request, message)
raw.squash(user, merge_request.id, branch: merge_request.target_branch,
start_sha: merge_request.diff_start_sha,
end_sha: merge_request.diff_head_sha,
author: merge_request.author,
message: merge_request.title)
message: message)
end
 
def update_submodule(user, submodule, commit_sha, message:, branch:)
Loading
Loading
# frozen_string_literal: true
class MergeRequestWidgetCommitEntity < Grape::Entity
expose :safe_message, as: :message
expose :short_id
expose :title
end
Loading
Loading
@@ -56,10 +56,23 @@ class MergeRequestWidgetEntity < IssuableEntity
merge_request.diff_head_sha.presence
end
 
expose :merge_commit_message
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
 
expose :default_squash_commit_message
expose :default_merge_commit_message
expose :default_merge_commit_message_with_description do |merge_request|
merge_request.default_merge_commit_message(include_description: true)
end
expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity do |merge_request|
merge_request.commits.without_merge_commits
end
expose :commits_count
# Booleans
expose :merge_ongoing?, as: :merge_ongoing
expose :work_in_progress?, as: :work_in_progress
Loading
Loading
@@ -77,7 +90,6 @@ class MergeRequestWidgetEntity < IssuableEntity
end
 
expose :branch_missing?, as: :branch_missing
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :mergeable?, as: :mergeable
Loading
Loading
@@ -205,10 +217,6 @@ class MergeRequestWidgetEntity < IssuableEntity
ci_environments_status_project_merge_request_path(merge_request.project, merge_request)
end
 
expose :merge_commit_message_with_description do |merge_request|
merge_request.merge_commit_message(include_description: true)
end
expose :diverged_commits_count do |merge_request|
if merge_request.open? && merge_request.diverged_from_target_branch?
merge_request.diverged_commits_count
Loading
Loading
Loading
Loading
@@ -8,6 +8,8 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::BaseService
include Gitlab::Utils::StrongMemoize
MergeError = Class.new(StandardError)
 
attr_reader :merge_request, :source
Loading
Loading
@@ -37,15 +39,10 @@ module MergeRequests
end
 
def source
return merge_request.diff_head_sha unless merge_request.squash
squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request)
case squash_result[:status]
when :success
squash_result[:squash_sha]
when :error
raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
if merge_request.squash
squash_sha!
else
merge_request.diff_head_sha
end
end
 
Loading
Loading
@@ -82,8 +79,22 @@ module MergeRequests
merge_request.update!(merge_commit_sha: commit_id)
end
 
def squash_sha!
strong_memoize(:squash_sha) do
params[:merge_request] = merge_request
squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute
case squash_result[:status]
when :success
squash_result[:squash_sha]
when :error
raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
end
end
end
def try_merge
message = params[:commit_message] || merge_request.merge_commit_message
message = params[:commit_message] || merge_request.default_merge_commit_message
 
repository.merge(current_user, source, merge_request, message)
rescue Gitlab::Git::PreReceiveError => e
Loading
Loading
Loading
Loading
@@ -2,15 +2,10 @@
 
module MergeRequests
class SquashService < MergeRequests::WorkingCopyBaseService
def execute(merge_request)
@merge_request = merge_request
@repository = target_project.repository
squash || error('Failed to squash. Should be done manually.')
end
def squash
if merge_request.commits_count < 2
def execute
# If performing a squash would result in no change, then
# immediately return a success message without performing a squash
if merge_request.commits_count < 2 && message.nil?
return success(squash_sha: merge_request.diff_head_sha)
end
 
Loading
Loading
@@ -18,7 +13,13 @@ module MergeRequests
return error('Squash task canceled: another squash is already in progress.')
end
 
squash_sha = repository.squash(current_user, merge_request)
squash! || error('Failed to squash. Should be done manually.')
end
private
def squash!
squash_sha = repository.squash(current_user, merge_request, message || merge_request.default_squash_commit_message)
 
success(squash_sha: squash_sha)
rescue => e
Loading
Loading
@@ -26,5 +27,17 @@ module MergeRequests
log_error(e.message)
false
end
def repository
target_project.repository
end
def merge_request
params[:merge_request]
end
def message
params[:squash_commit_message].presence
end
end
end
---
title: Default squash commit message is now selected from the longest commit when
squashing merge requests
merge_request: 24518
author:
type: changed
Loading
Loading
@@ -18,10 +18,14 @@ Into a single commit on merge:
 
![A squashed commit followed by a merge commit][squashed-commit]
 
The squashed commit's commit message is the merge request title. And note that
the squashed commit is still followed by a merge commit, as the merge
method for this example repository uses a merge commit. Squashing also works
with the fast-forward merge strategy, see
The squashed commit's commit message will be either:
- Taken from the first multi-line commit message in the merge.
- The merge request's title if no multi-line commit message is found.
Note that the squashed commit is still followed by a merge commit,
as the merge method for this example repository uses a merge commit.
Squashing also works with the fast-forward merge strategy, see
[squashing and fast-forward merge](#squash-and-fast-forward-merge) for more
details.
 
Loading
Loading
@@ -34,7 +38,7 @@ you'd rather not include them in your target branch.
 
With squash and merge, when the merge request is ready to be merged,
all you have to do is enable squashing before you press merge to join
the commits include in the merge request into a single commit.
the commits in the merge request into a single commit.
 
This way, the history of your base branch remains clean with
meaningful commit messages and is simpler to [revert] if necessary.
Loading
Loading
@@ -56,7 +60,7 @@ This can then be overridden at the time of accepting the merge request:
 
The squashed commit has the following metadata:
 
- Message: the title of the merge request.
- Message: the message of the squash commit.
- Author: the author of the merge request.
- Committer: the user who initiated the squash.
 
Loading
Loading
Loading
Loading
@@ -387,6 +387,23 @@ describe Projects::MergeRequestsController do
end
end
 
context 'when a squash commit message is passed' do
let(:message) { 'My custom squash commit message' }
it 'passes the same message to SquashService' do
params = { squash: '1', squash_commit_message: message }
expect_next_instance_of(MergeRequests::SquashService, project, user, params.merge(merge_request: merge_request)) do |squash_service|
expect(squash_service).to receive(:execute).and_return({
status: :success,
squash_sha: SecureRandom.hex(20)
})
end
merge_with_sha(params)
end
end
context 'when the pipeline succeeds is passed' do
let!(:head_pipeline) do
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
Loading
Loading
Loading
Loading
@@ -16,14 +16,24 @@ FactoryBot.define do
 
commit
end
project
 
skip_create # Commits cannot be persisted
initialize_with do
new(git_commit, project)
end
 
after(:build) do |commit, evaluator|
allow(commit).to receive(:author).and_return(evaluator.author || build_stubbed(:author))
allow(commit).to receive(:parent_ids).and_return([])
end
trait :merge_commit do
after(:build) do |commit|
allow(commit).to receive(:parent_ids).and_return(Array.new(2) { SecureRandom.hex(20) })
end
end
 
trait :without_author do
Loading
Loading
Loading
Loading
@@ -14,7 +14,7 @@ describe 'User squashes a merge request', :js do
latest_master_commits = project.repository.commits_between(original_head.sha, 'master').map(&:raw)
 
squash_commit = an_object_having_attributes(sha: a_string_matching(/\h{40}/),
message: "Csv\n",
message: a_string_starting_with(project.merge_requests.first.default_squash_commit_message),
author_name: user.name,
committer_name: user.name)
 
Loading
Loading
Loading
Loading
@@ -44,7 +44,7 @@
"merge_user": { "type": ["object", "null"] },
"diff_head_sha": { "type": ["string", "null"] },
"diff_head_commit_short_id": { "type": ["string", "null"] },
"merge_commit_message": { "type": ["string", "null"] },
"default_merge_commit_message": { "type": ["string", "null"] },
"pipeline": { "type": ["object", "null"] },
"merge_pipeline": { "type": ["object", "null"] },
"work_in_progress": { "type": "boolean" },
Loading
Loading
@@ -102,7 +102,9 @@
"new_blob_path": { "type": ["string", "null"] },
"merge_check_path": { "type": "string" },
"ci_environments_status_path": { "type": "string" },
"merge_commit_message_with_description": { "type": "string" },
"default_merge_commit_message_with_description": { "type": "string" },
"default_squash_commit_message": { "type": "string" },
"commits_without_merge_commits": { "type": "array" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
"merge_commit_path": { "type": ["string", "null"] },
Loading
Loading
Loading
Loading
@@ -58,7 +58,7 @@ export default {
merge_user: null,
diff_head_sha: '104096c51715e12e7ae41f9333e9fa35b73f385d',
diff_head_commit_short_id: '104096c5',
merge_commit_message:
default_merge_commit_message:
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
pipeline: {
id: 172,
Loading
Loading
@@ -213,7 +213,7 @@ export default {
merge_check_path: '/root/acets-app/merge_requests/22/merge_check',
ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status',
project_archived: false,
merge_commit_message_with_description:
default_merge_commit_message_with_description:
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
diverged_commits_count: 0,
only_allow_merge_if_pipeline_succeeds: false,
Loading
Loading
Loading
Loading
@@ -35,6 +35,17 @@ describe CommitCollection do
end
end
 
describe '#without_merge_commits' do
it 'returns all commits except merge commits' do
collection = described_class.new(project, [
build(:commit),
build(:commit, :merge_commit)
])
expect(collection.without_merge_commits.size).to eq(1)
end
end
describe '#with_pipeline_status' do
it 'sets the pipeline status for every commit so no additional queries are necessary' do
create(
Loading
Loading
Loading
Loading
@@ -82,6 +82,38 @@ describe MergeRequest do
end
end
 
describe '#default_squash_commit_message' do
let(:project) { subject.project }
def commit_collection(commit_hashes)
raw_commits = commit_hashes.map { |raw| Commit.from_hash(raw, project) }
CommitCollection.new(project, raw_commits)
end
it 'returns the oldest multiline commit message' do
commits = commit_collection([
{ message: 'Singleline', parent_ids: [] },
{ message: "Second multiline\nCommit message", parent_ids: [] },
{ message: "First multiline\nCommit message", parent_ids: [] }
])
expect(subject).to receive(:commits).and_return(commits)
expect(subject.default_squash_commit_message).to eq("First multiline\nCommit message")
end
it 'returns the merge request title if there are no multiline commits' do
commits = commit_collection([
{ message: 'Singleline', parent_ids: [] }
])
expect(subject).to receive(:commits).and_return(commits)
expect(subject.default_squash_commit_message).to eq(subject.title)
end
end
describe 'modules' do
subject { described_class }
 
Loading
Loading
@@ -920,18 +952,18 @@ describe MergeRequest do
end
end
 
describe '#merge_commit_message' do
describe '#default_merge_commit_message' do
it 'includes merge information as the title' do
request = build(:merge_request, source_branch: 'source', target_branch: 'target')
 
expect(request.merge_commit_message)
expect(request.default_merge_commit_message)
.to match("Merge branch 'source' into 'target'\n\n")
end
 
it 'includes its title in the body' do
request = build(:merge_request, title: 'Remove all technical debt')
 
expect(request.merge_commit_message)
expect(request.default_merge_commit_message)
.to match("Remove all technical debt\n\n")
end
 
Loading
Loading
@@ -943,34 +975,34 @@ describe MergeRequest do
allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
subject.cache_merge_request_closes_issues!
 
expect(subject.merge_commit_message)
expect(subject.default_merge_commit_message)
.to match("Closes #{issue.to_reference}")
end
 
it 'includes its reference in the body' do
request = build_stubbed(:merge_request)
 
expect(request.merge_commit_message)
expect(request.default_merge_commit_message)
.to match("See merge request #{request.to_reference(full: true)}")
end
 
it 'excludes multiple linebreak runs when description is blank' do
request = build(:merge_request, title: 'Title', description: nil)
 
expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
expect(request.default_merge_commit_message).not_to match("Title\n\n\n\n")
end
 
it 'includes its description in the body' do
request = build(:merge_request, description: 'By removing all code')
 
expect(request.merge_commit_message(include_description: true))
expect(request.default_merge_commit_message(include_description: true))
.to match("By removing all code\n\n")
end
 
it 'does not includes its description in the body' do
request = build(:merge_request, description: 'By removing all code')
 
expect(request.merge_commit_message)
expect(request.default_merge_commit_message)
.not_to match("By removing all code\n\n")
end
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment