1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
# frozen_string_literal: true
module Gitlab
module MergeRequests
class MessageGenerator
def initialize(merge_request:, current_user:)
@merge_request = merge_request
@current_user = @merge_request.metrics&.merged_by || @merge_request.merge_user || current_user
end
def merge_commit_message
return unless @merge_request.target_project.merge_commit_template.present?
replace_placeholders(@merge_request.target_project.merge_commit_template, allowed_placeholders: PLACEHOLDERS)
end
def squash_commit_message
return unless @merge_request.target_project.squash_commit_template.present?
replace_placeholders(
@merge_request.target_project.squash_commit_template,
allowed_placeholders: PLACEHOLDERS,
squash: true
)
end
def new_mr_description
return unless @merge_request.description.present?
replace_placeholders(
@merge_request.description,
allowed_placeholders: ALLOWED_NEW_MR_PLACEHOLDERS,
keep_carriage_return: true
)
end
private
attr_reader :merge_request, :current_user
PLACEHOLDERS = {
'source_branch' => ->(merge_request, _, _) { merge_request.source_branch.to_s },
'target_branch' => ->(merge_request, _, _) { merge_request.target_branch.to_s },
'title' => ->(merge_request, _, _) { merge_request.title },
'issues' => ->(merge_request, _, _) do
return if merge_request.visible_closing_issues_for.blank?
closes_issues_references = merge_request.visible_closing_issues_for.map do |issue|
issue.to_reference(merge_request.target_project)
end
"Closes #{closes_issues_references.to_sentence}"
end,
'description' => ->(merge_request, _, _) { merge_request.description },
'reference' => ->(merge_request, _, _) { merge_request.to_reference(full: true) },
'local_reference' => ->(merge_request, _, _) { merge_request.to_reference(full: false) },
'first_commit' => -> (merge_request, _, _) {
return unless merge_request.persisted? || merge_request.compare_commits.present?
merge_request.first_commit&.safe_message&.strip
},
'first_multiline_commit' => -> (merge_request, _, _) {
merge_request.first_multiline_commit&.safe_message&.strip.presence || merge_request.title
},
'url' => ->(merge_request, _, _) { Gitlab::UrlBuilder.build(merge_request) },
'reviewed_by' => ->(merge_request, _, _) {
merge_request.reviewed_by_users
.map { |user| "Reviewed-by: #{user.name} <#{user.commit_email_or_default}>" }
.join("\n")
},
'approved_by' => ->(merge_request, _, _) {
merge_request.approved_by_users
.map { |user| "Approved-by: #{user.name} <#{user.commit_email_or_default}>" }
.join("\n")
},
'merged_by' => ->(_, user, _) { "#{user&.name} <#{user&.commit_email_or_default}>" },
'co_authored_by' => ->(merge_request, merged_by, squash) do
commit_author = squash ? merge_request.author : merged_by
merge_request.recent_commits
.to_h { |commit| [commit.author_email, commit.author_name] }
.except(commit_author&.commit_email_or_default)
.map { |author_email, author_name| "Co-authored-by: #{author_name} <#{author_email}>" }
.join("\n")
end,
'all_commits' => -> (merge_request, _, _) do
merge_request
.recent_commits
.without_merge_commits
.map do |commit|
if commit.safe_message&.bytesize&.>(100.kilobytes)
"* #{commit.title}\n\n-- Skipped commit body exceeding 100KiB in size."
else
"* #{commit.safe_message&.strip}"
end
end
.join("\n\n")
end
}.freeze
# A new merge request that is in the process of being created and hasn't
# been persisted to the database.
#
# Limit the placeholders to a subset of the available ones where the
# placeholders wouldn't make sense in context. Disallowed placeholders
# will be replaced with an empty string.
ALLOWED_NEW_MR_PLACEHOLDERS = %w[
source_branch
target_branch
first_commit
first_multiline_commit
co_authored_by
all_commits
].freeze
PLACEHOLDERS_COMBINED_REGEX = /%{(#{Regexp.union(PLACEHOLDERS.keys)})}/.freeze
def replace_placeholders(message, allowed_placeholders: [], squash: false, keep_carriage_return: false)
# Convert CRLF to LF.
message = message.delete("\r") unless keep_carriage_return
used_variables = message.scan(PLACEHOLDERS_COMBINED_REGEX).map { |value| value[0] }.uniq
values = used_variables.to_h do |variable_name|
replacement = if allowed_placeholders.include?(variable_name)
PLACEHOLDERS[variable_name].call(merge_request, current_user, squash)
end
["%{#{variable_name}}", replacement]
end
names_of_empty_variables = values.filter_map { |name, value| name if value.blank? }
# Remove lines that contain empty variable placeholder and nothing else.
if names_of_empty_variables.present?
# If there is blank line or EOF after it, remove blank line before it as well.
message = message.gsub(/\n\n#{Regexp.union(names_of_empty_variables)}(\n\n|\Z)/, '\1')
# Otherwise, remove only the line it is in.
message = message.gsub(/^#{Regexp.union(names_of_empty_variables)}\n/, '')
end
# Substitute all variables with their values.
message = message.gsub(Regexp.union(values.keys), values) if values.present?
message
end
end
end
end
|