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
206
207
208
209
|
# frozen_string_literal: true
module MergeRequests
class PushOptionsHandlerService < ::BaseProjectService
LIMIT = 10
attr_reader :errors, :changes,
:push_options, :target_project
def initialize(project:, current_user:, changes:, push_options:, params: {})
super(project: project, current_user: current_user, params: params)
@target_project = @project.default_merge_request_target
@changes = Gitlab::ChangesList.new(changes)
@push_options = push_options
@errors = []
end
def execute
validate_service
return self if errors.present?
branches.each do |branch|
execute_for_branch(branch)
rescue Gitlab::Access::AccessDeniedError
errors << 'User access was denied'
rescue StandardError => e
Gitlab::AppLogger.error(e)
errors << 'An unknown error occurred'
end
self
end
private
def branches
changes_by_branch.keys
end
def changes_by_branch
@changes_by_branch ||= changes.each_with_object({}) do |changes, result|
next unless Gitlab::Git.branch_ref?(changes[:ref])
# Deleted branch
next if Gitlab::Git.blank_ref?(changes[:newrev])
# Default branch
branch_name = Gitlab::Git.branch_name(changes[:ref])
next if branch_name == target_project.default_branch
result[branch_name] = changes
end
end
def validate_service
if current_user.nil?
errors << 'User is required'
return
end
unless current_user&.can?(:read_code, target_project)
errors << 'User access was denied'
return
end
unless target_project.merge_requests_enabled?
errors << "Merge requests are not enabled for project #{target_project.full_path}"
end
if branches.size > LIMIT
errors << "Too many branches pushed (#{branches.size} were pushed, limit is #{LIMIT})"
end
if push_options[:target] && !target_project.repository.branch_exists?(push_options[:target])
errors << "Target branch #{target_project.full_path}:#{push_options[:target]} does not exist"
end
end
# Returns a Hash of branch => MergeRequest
def merge_requests
@merge_requests ||= MergeRequest.from_project(target_project)
.opened
.from_source_branches(branches)
.index_by(&:source_branch)
end
def execute_for_branch(branch)
merge_request = merge_requests[branch]
if merge_request
update!(merge_request)
else
create!(branch)
end
end
def create!(branch)
unless push_options[:create]
errors << "A merge_request.create push option is required to create a merge request for branch #{branch}"
return
end
# Use BuildService to assign the standard attributes of a merge request
merge_request = ::MergeRequests::BuildService.new(
project: project,
current_user: current_user,
params: create_params(branch)
).execute
unless merge_request.errors.present?
merge_request = ::MergeRequests::CreateService.new(
project: project,
current_user: current_user,
params: merge_request.attributes.merge(assignee_ids: merge_request.assignee_ids,
label_ids: merge_request.label_ids)
).execute
end
collect_errors_from_merge_request(merge_request) unless merge_request.persisted?
end
def update!(merge_request)
merge_request = ::MergeRequests::UpdateService.new(
project: target_project,
current_user: current_user,
params: update_params(merge_request)
).execute(merge_request)
collect_errors_from_merge_request(merge_request) unless merge_request.valid?
end
def base_params
params = {
title: push_options[:title],
description: push_options[:description],
draft: push_options[:draft],
target_branch: push_options[:target],
force_remove_source_branch: push_options[:remove_source_branch],
label: push_options[:label],
unlabel: push_options[:unlabel],
assign: push_options[:assign],
unassign: push_options[:unassign]
}
params.compact!
params[:add_labels] = params.delete(:label).keys if params.has_key?(:label)
params[:remove_labels] = params.delete(:unlabel).keys if params.has_key?(:unlabel)
params[:add_assignee_ids] = convert_to_user_ids(params.delete(:assign).keys) if params.has_key?(:assign)
params[:remove_assignee_ids] = convert_to_user_ids(params.delete(:unassign).keys) if params.has_key?(:unassign)
if push_options[:milestone]
milestone = Milestone.for_projects_and_groups(@project, @project.ancestors_upto)&.find_by_name(push_options[:milestone])
params[:milestone_id] = milestone.id if milestone
end
if params.key?(:description)
params[:description] = params[:description].gsub('\n', "\n")
end
params
end
def merge_params(branch)
return {} unless push_options.key?(:merge_when_pipeline_succeeds)
{
merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds],
merge_user: current_user,
sha: changes_by_branch.dig(branch, :newrev)
}
end
def create_params(branch)
params = base_params
params.merge!(
assignee_ids: [current_user.id],
source_branch: branch,
source_project: project,
target_project: target_project
)
params.merge!(merge_params(branch))
params[:target_branch] ||= target_project.default_branch
params
end
def update_params(merge_request)
base_params.merge(merge_params(merge_request.source_branch))
end
def convert_to_user_ids(ids_or_usernames)
ids, usernames = ids_or_usernames.partition { |id_or_username| id_or_username.is_a?(Numeric) || id_or_username.match?(/\A\d+\z/) }
ids += User.by_username(usernames).pluck(:id) unless usernames.empty? # rubocop:disable CodeReuse/ActiveRecord
ids
end
def collect_errors_from_merge_request(merge_request)
merge_request.errors.full_messages.each do |error|
errors << error
end
end
end
end
|