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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
|
# frozen_string_literal: true
module MergeRequests
# MergeService class
#
# Do git merge and in case of success
# mark merge request as merged and execute all hooks and notifications
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::MergeBaseService
include Gitlab::Utils::StrongMemoize
GENERIC_ERROR_MESSAGE = 'An error occurred while merging'
LEASE_TIMEOUT = 15.minutes.to_i
delegate :merge_jid, :state, to: :@merge_request
def execute(merge_request, options = {})
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project: project, current_user: current_user, params: params).execute(merge_request)
return
end
return if merge_request.merged?
return unless exclusive_lease(merge_request.id).try_obtain
@merge_request = merge_request
@options = options
jid = merge_jid
validate!
merge_request.in_locked_state do
if commit
after_merge
clean_merge_jid
success
end
end
log_info("Merge process finished on JID #{jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
ensure
exclusive_lease(merge_request.id).cancel
end
private
def validate!
authorization_check!
error_check!
updated_check!
end
def authorization_check!
unless @merge_request.can_be_merged_by?(current_user)
raise_error('You are not allowed to merge this merge request')
end
end
def error_check!
super
check_source
error =
if @merge_request.should_be_rebased?
'Only fast-forward merge is allowed for your project. Please update your source branch'
elsif !@merge_request.mergeable?(skip_discussions_check: @options[:skip_discussions_check])
'Merge request is not mergeable'
elsif !@merge_request.squash && project.squash_always?
'This project requires squashing commits when merge requests are accepted.'
end
raise_error(error) if error
end
def updated_check!
unless source_matches?
raise_error('Branch has been updated since the merge was requested. '\
'Please review the changes.')
end
end
def commit
log_info("Git merge started on JID #{merge_jid}")
commit_id = try_merge
if commit_id
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
else
raise_error(GENERIC_ERROR_MESSAGE)
end
update_merge_sha_metadata(commit_id)
commit_id
ensure
merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
end
def update_merge_sha_metadata(commit_id)
data_to_update = merge_success_data(commit_id)
data_to_update[:squash_commit_sha] = source if merge_request.squash_on_merge?
merge_request.update!(**data_to_update) if data_to_update.present?
end
def merge_success_data(commit_id)
{ merge_commit_sha: commit_id }
end
def try_merge
execute_git_merge
rescue Gitlab::Git::PreReceiveError => e
raise MergeError,
"Something went wrong during merge pre-receive hook. #{e.message}".strip
rescue StandardError => e
handle_merge_error(log_message: e.message)
raise_error(GENERIC_ERROR_MESSAGE)
end
def execute_git_merge
repository.merge(current_user, source, merge_request, commit_message)
end
def after_merge
log_info("Post merge started on JID #{merge_jid} with state #{state}")
MergeRequests::PostMergeService.new(project: project, current_user: current_user).execute(merge_request)
log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch?
MergeRequests::DeleteSourceBranchWorker.perform_async(@merge_request.id, @merge_request.source_branch_sha, branch_deletion_user.id)
end
merge_request_merge_param
end
def clean_merge_jid
merge_request.update_column(:merge_jid, nil)
end
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
# Verify again that the source branch can be removed, since branch may be protected,
# or the source branch may have been updated, or the user may not have permission
#
def delete_source_branch?
params.fetch('should_remove_source_branch', @merge_request.force_remove_source_branch?) &&
@merge_request.can_remove_source_branch?(branch_deletion_user)
end
def merge_request_merge_param
if @merge_request.can_remove_source_branch?(branch_deletion_user) && !params.fetch('should_remove_source_branch', nil).nil?
@merge_request.update(merge_params: @merge_request.merge_params.merge('should_remove_source_branch' => params['should_remove_source_branch']))
end
end
def handle_merge_error(log_message:, save_message_on_model: false)
log_error("MergeService ERROR: #{merge_request_info} - #{log_message}")
@merge_request.update(merge_error: log_message) if save_message_on_model
end
def log_info(message)
payload = log_payload("#{merge_request_info} - #{message}")
logger.info(**payload)
end
def log_error(message)
payload = log_payload(message)
logger.error(**payload)
end
def logger
@logger ||= Gitlab::AppLogger
end
def log_payload(message)
Gitlab::ApplicationContext.current
.merge(merge_request_info: merge_request_info,
message: message)
end
def merge_request_info
@merge_request_info ||= merge_request.to_reference(full: true)
end
def source_matches?
# params-keys are symbols coming from the controller, but when they get
# loaded from the database they're strings
params.with_indifferent_access[:sha] == merge_request.diff_head_sha
end
def exclusive_lease(merge_request_id)
strong_memoize(:"exclusive_lease_#{merge_request_id}") do
lease_key = ['merge_requests_merge_service', merge_request_id].join(':')
Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
end
end
end
end
|