summaryrefslogtreecommitdiff
path: root/lib/gitlab/quick_actions/merge_request_actions.rb
blob: 4efa29337d113c897653e0895eb3479a6915da42 (plain)
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# frozen_string_literal: true

module Gitlab
  module QuickActions
    module MergeRequestActions
      extend ActiveSupport::Concern
      include Gitlab::QuickActions::Dsl

      included do
        # MergeRequest only quick actions definitions
        desc do
          if preferred_strategy = preferred_auto_merge_strategy(quick_action_target)
            _("Merge automatically (%{strategy})") % { strategy: preferred_strategy.humanize }
          else
            _("Merge immediately")
          end
        end
        explanation do
          if preferred_strategy = preferred_auto_merge_strategy(quick_action_target)
            _("Schedules to merge this merge request (%{strategy}).") % { strategy: preferred_strategy.humanize }
          else
            _('Merges this merge request immediately.')
          end
        end
        execution_message do
          if params[:merge_request_diff_head_sha].blank?
            _("The `/merge` quick action requires the SHA of the head of the branch.")
          elsif params[:merge_request_diff_head_sha] != quick_action_target.diff_head_sha
            _("Branch has been updated since the merge was requested.")
          elsif preferred_strategy = preferred_auto_merge_strategy(quick_action_target)
            _("Scheduled to merge this merge request (%{strategy}).") % { strategy: preferred_strategy.humanize }
          else
            _('Merged this merge request.')
          end
        end
        types MergeRequest
        condition do
          quick_action_target.persisted? &&
            merge_orchestration_service.can_merge?(quick_action_target)
        end
        command :merge do
          next unless params[:merge_request_diff_head_sha].present?

          next unless params[:merge_request_diff_head_sha] == quick_action_target.diff_head_sha

          @updates[:merge] = params[:merge_request_diff_head_sha]
        end

        types MergeRequest
        desc do
          _('Rebase source branch')
        end
        explanation do
          _('Rebase source branch on the target branch.')
        end
        condition do
          merge_request = quick_action_target

          next false unless merge_request.open?
          next false unless merge_request.source_branch_exists?

          access_check = ::Gitlab::UserAccess
                           .new(current_user, container: merge_request.source_project)

          access_check.can_push_to_branch?(merge_request.source_branch)
        end
        command :rebase do
          unless quick_action_target.permits_force_push?
            @execution_message[:rebase] = _('This merge request branch is protected from force push.')
            next
          end

          if quick_action_target.cannot_be_merged?
            @execution_message[:rebase] = _('This merge request cannot be rebased while there are conflicts.')
            next
          end

          if quick_action_target.rebase_in_progress?
            @execution_message[:rebase] = _('A rebase is already in progress.')
            next
          end

          # This will be used to avoid simultaneous "/merge" and "/rebase" actions
          @updates[:rebase] = true

          branch = quick_action_target.source_branch

          @execution_message[:rebase] = _('Scheduled a rebase of branch %{branch}.') % { branch: branch }
        end

        desc 'Toggle the Draft status'
        explanation do
          noun = quick_action_target.to_ability_name.humanize(capitalize: false)
          if quick_action_target.work_in_progress?
            _("Unmarks this %{noun} as a draft.")
          else
            _("Marks this %{noun} as a draft.")
          end % { noun: noun }
        end
        execution_message do
          noun = quick_action_target.to_ability_name.humanize(capitalize: false)
          if quick_action_target.work_in_progress?
            _("Unmarked this %{noun} as a draft.")
          else
            _("Marked this %{noun} as a draft.")
          end % { noun: noun }
        end

        types MergeRequest
        condition do
          quick_action_target.respond_to?(:work_in_progress?) &&
            # Allow it to mark as WIP on MR creation page _or_ through MR notes.
            (quick_action_target.new_record? || current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target))
        end
        command :draft do
          @updates[:wip_event] = quick_action_target.work_in_progress? ? 'unwip' : 'wip'
        end

        desc _('Set target branch')
        explanation do |branch_name|
          _('Sets target branch to %{branch_name}.') % { branch_name: branch_name }
        end
        execution_message do |branch_name|
          _('Set target branch to %{branch_name}.') % { branch_name: branch_name }
        end
        params '<Local branch name>'
        types MergeRequest
        condition do
          quick_action_target.respond_to?(:target_branch) &&
            (current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target) ||
              quick_action_target.new_record?)
        end
        parse_params do |target_branch_param|
          target_branch_param.strip
        end
        command :target_branch do |branch_name|
          @updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)
        end

        desc _('Submit a review')
        explanation _('Submit the current review.')
        types MergeRequest
        condition do
          quick_action_target.persisted?
        end
        command :submit_review do
          next if params[:review_id]

          result = DraftNotes::PublishService.new(quick_action_target, current_user).execute
          @execution_message[:submit_review] = if result[:status] == :success
                                                 _('Submitted the current review.')
                                               else
                                                 result[:message]
                                               end
        end

        desc _('Approve a merge request')
        explanation _('Approve the current merge request.')
        types MergeRequest
        condition do
          quick_action_target.persisted? && quick_action_target.can_be_approved_by?(current_user)
        end
        command :approve do
          success = ::MergeRequests::ApprovalService.new(project: quick_action_target.project, current_user: current_user).execute(quick_action_target)

          next unless success

          @execution_message[:approve] = _('Approved the current merge request.')
        end

        desc _('Unapprove a merge request')
        explanation _('Unapprove the current merge request.')
        types MergeRequest
        condition do
          quick_action_target.persisted? && quick_action_target.can_be_unapproved_by?(current_user)
        end
        command :unapprove do
          success = ::MergeRequests::RemoveApprovalService.new(project: quick_action_target.project, current_user: current_user).execute(quick_action_target)

          next unless success

          @execution_message[:unapprove] = _('Unapproved the current merge request.')
        end

        desc do
          if quick_action_target.allows_multiple_reviewers?
            _('Assign reviewer(s)')
          else
            _('Assign reviewer')
          end
        end
        explanation do |users|
          reviewers = reviewers_to_add(users)
          _('Assigns %{reviewer_users_sentence} as %{reviewer_text}.') % { reviewer_users_sentence: reviewer_users_sentence(users),
                                                                           reviewer_text: 'reviewer'.pluralize(reviewers.size) }
        end
        execution_message do |users = nil|
          reviewers = reviewers_to_add(users)
          if reviewers.blank?
            _("Failed to assign a reviewer because no user was specified.")
          else
            _('Assigned %{reviewer_users_sentence} as %{reviewer_text}.') % { reviewer_users_sentence: reviewer_users_sentence(users),
                                                                              reviewer_text: 'reviewer'.pluralize(reviewers.size) }
          end
        end
        params do
          quick_action_target.allows_multiple_reviewers? ? '@user1 @user2' : '@user'
        end
        types MergeRequest
        condition do
          current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
        end
        parse_params do |reviewer_param|
          extract_users(reviewer_param)
        end
        command :assign_reviewer, :reviewer, :request_review do |users|
          next if users.empty?

          if quick_action_target.allows_multiple_reviewers?
            @updates[:reviewer_ids] ||= quick_action_target.reviewers.map(&:id)
            @updates[:reviewer_ids] |= users.map(&:id)
          else
            @updates[:reviewer_ids] = [users.first.id]
          end
        end

        desc do
          if quick_action_target.allows_multiple_reviewers?
            _('Remove all or specific reviewer(s)')
          else
            _('Remove reviewer')
          end
        end
        explanation do |users = nil|
          reviewers = reviewers_for_removal(users)
          _("Removes %{reviewer_text} %{reviewer_references}.") %
            { reviewer_text: 'reviewer'.pluralize(reviewers.size), reviewer_references: reviewers.map(&:to_reference).to_sentence }
        end
        execution_message do |users = nil|
          reviewers = reviewers_for_removal(users)
          _("Removed %{reviewer_text} %{reviewer_references}.") %
            { reviewer_text: 'reviewer'.pluralize(reviewers.size), reviewer_references: reviewers.map(&:to_reference).to_sentence }
        end
        params do
          quick_action_target.allows_multiple_reviewers? ? '@user1 @user2' : ''
        end
        types MergeRequest
        condition do
          quick_action_target.persisted? &&
            quick_action_target.reviewers.any? &&
            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
        end
        parse_params do |unassign_reviewer_param|
          # When multiple users are assigned, all will be unassigned if multiple reviewers are no longer allowed
          extract_users(unassign_reviewer_param) if quick_action_target.allows_multiple_reviewers?
        end
        command :unassign_reviewer, :remove_reviewer do |users = nil|
          if quick_action_target.allows_multiple_reviewers? && users&.any?
            @updates[:reviewer_ids] ||= quick_action_target.reviewers.map(&:id)
            @updates[:reviewer_ids] -= users.map(&:id)
          else
            @updates[:reviewer_ids] = []
          end
        end

        desc do
          if quick_action_target.allows_multiple_reviewers?
            _('Request attention from assignee(s) or reviewer(s)')
          else
            _('Request attention from assignee or reviewer')
          end
        end
        explanation do |users|
          _('Request attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) }
        end
        execution_message do |users = nil|
          if users.blank?
            _("Failed to request attention because no user was found.")
          else
            _('Requested attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) }
          end
        end
        params do
          quick_action_target.allows_multiple_reviewers? ? '@user1 @user2' : '@user'
        end
        types MergeRequest
        condition do
          Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml) &&
            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
        end
        parse_params do |attention_param|
          extract_users(attention_param)
        end
        command :attention, :attn do |users|
          next if users.empty?

          users.each do |user|
            ::MergeRequests::ToggleAttentionRequestedService.new(project: quick_action_target.project, merge_request: quick_action_target, current_user: current_user, user: user).execute
          end
        end

        desc do
          if quick_action_target.allows_multiple_reviewers?
            _('Remove attention request(s)')
          else
            _('Remove attention request')
          end
        end
        explanation do |users|
          _('Removes attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) }
        end
        execution_message do |users = nil|
          if users.blank?
            _("Failed to remove attention because no user was found.")
          else
            _('Removed attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) }
          end
        end
        params do
          quick_action_target.allows_multiple_reviewers? ? '@user1 @user2' : '@user'
        end
        types MergeRequest
        condition do
          Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml) &&
            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
        end
        parse_params do |attention_param|
          extract_users(attention_param)
        end
        command :remove_attention do |users|
          next if users.empty?

          ::MergeRequests::BulkRemoveAttentionRequestedService.new(project: quick_action_target.project, merge_request: quick_action_target, current_user: current_user, users: users).execute
        end
      end

      def reviewer_users_sentence(users)
        reviewers_to_add(users).map(&:to_reference).to_sentence
      end

      def reviewers_for_removal(users)
        reviewers = quick_action_target.reviewers
        if users.present? && quick_action_target.allows_multiple_reviewers?
          users
        else
          reviewers
        end
      end

      def reviewers_to_add(users)
        return if users.blank?

        if quick_action_target.allows_multiple_reviewers?
          users
        else
          [users.first]
        end
      end

      def merge_orchestration_service
        @merge_orchestration_service ||= ::MergeRequests::MergeOrchestrationService.new(project, current_user)
      end

      def preferred_auto_merge_strategy(merge_request)
        merge_orchestration_service.preferred_auto_merge_strategy(merge_request)
      end
    end
  end
end