interpret_service.rb 9.6 KB
Newer Older
1 2 3 4
module SlashCommands
  class InterpretService < BaseService
    include Gitlab::SlashCommands::Dsl

5
    attr_reader :issuable, :options
6

7 8
    # Takes a text and interprets the commands that are extracted from it.
    # Returns the content without commands, and hash of changes to be applied to a record.
9 10
    def execute(content, issuable)
      @issuable = issuable
11 12
      @updates = {}

D
Douwe Maan 已提交
13
      opts = {
14
        issuable:     issuable,
D
Douwe Maan 已提交
15
        current_user: current_user,
16 17
        project:      project,
        params:       params
D
Douwe Maan 已提交
18 19 20 21
      }

      content, commands = extractor.extract_commands(content, opts)

22
      commands.each do |name, arg|
D
Douwe Maan 已提交
23 24 25
        definition = self.class.command_definitions_by_name[name.to_sym]
        next unless definition

26
        definition.execute(self, opts, arg)
27 28
      end

D
Douwe Maan 已提交
29
      [content, @updates]
30 31 32 33
    end

    private

D
Douwe Maan 已提交
34 35
    def extractor
      Gitlab::SlashCommands::Extractor.new(self.class.command_definitions)
36 37
    end

38
    desc do
39
      "Close this #{issuable.to_ability_name.humanize(capitalize: false)}"
40 41
    end
    condition do
42 43 44
      issuable.persisted? &&
        issuable.open? &&
        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
R
Rémy Coutable 已提交
45
    end
46 47 48 49
    command :close do
      @updates[:state_event] = 'close'
    end

50
    desc do
51
      "Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}"
52 53
    end
    condition do
54 55 56
      issuable.persisted? &&
        issuable.closed? &&
        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
R
Rémy Coutable 已提交
57
    end
D
Douwe Maan 已提交
58
    command :reopen do
59 60 61
      @updates[:state_event] = 'reopen'
    end

62 63
    desc 'Merge (when build succeeds)'
    condition do
J
Jarka Kadlecova 已提交
64
      last_diff_sha = params && params[:merge_request_diff_head_sha]
65
      issuable.is_a?(MergeRequest) &&
J
Jarka Kadlecova 已提交
66 67
        issuable.persisted? &&
        issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
68 69 70 71 72
    end
    command :merge do
      @updates[:merge] = params[:merge_request_diff_head_sha]
    end

R
Rémy Coutable 已提交
73 74
    desc 'Change title'
    params '<New title>'
75
    condition do
76 77
      issuable.persisted? &&
        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
R
Rémy Coutable 已提交
78
    end
R
Rémy Coutable 已提交
79 80 81 82
    command :title do |title_param|
      @updates[:title] = title_param
    end

83
    desc 'Assign'
84
    params '@user'
85
    condition do
86
      current_user.can?(:"admin_#{issuable.to_ability_name}", project)
R
Rémy Coutable 已提交
87
    end
88
    command :assign do |assignee_param|
89
      user = extract_references(assignee_param, :user).first
90
      user ||= User.find_by(username: assignee_param)
91

92
      @updates[:assignee_id] = user.id if user
93 94 95
    end

    desc 'Remove assignee'
96
    condition do
97 98 99
      issuable.persisted? &&
        issuable.assignee_id? &&
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
R
Rémy Coutable 已提交
100
    end
D
Douwe Maan 已提交
101
    command :unassign do
102 103 104
      @updates[:assignee_id] = nil
    end

105
    desc 'Set milestone'
106
    params '%"milestone"'
107
    condition do
108
      current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
109
        project.milestones.active.any?
R
Rémy Coutable 已提交
110
    end
111 112
    command :milestone do |milestone_param|
      milestone = extract_references(milestone_param, :milestone).first
113
      milestone ||= project.milestones.find_by(title: milestone_param.strip)
114

115
      @updates[:milestone_id] = milestone.id if milestone
116 117 118
    end

    desc 'Remove milestone'
119
    condition do
120 121 122
      issuable.persisted? &&
        issuable.milestone_id? &&
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
R
Rémy Coutable 已提交
123
    end
D
Douwe Maan 已提交
124
    command :remove_milestone do
125 126 127 128 129
      @updates[:milestone_id] = nil
    end

    desc 'Add label(s)'
    params '~label1 ~"label 2"'
130
    condition do
131 132
      available_labels = LabelsFinder.new(current_user, project_id: project.id).execute

133
      current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
134
        available_labels.any?
R
Rémy Coutable 已提交
135
    end
D
Douwe Maan 已提交
136
    command :label do |labels_param|
137 138
      label_ids = find_label_ids(labels_param)

B
barthc 已提交
139 140 141 142 143 144
      if label_ids.any?
        @updates[:add_label_ids] ||= []
        @updates[:add_label_ids] += label_ids

        @updates[:add_label_ids].uniq!
      end
145 146
    end

D
Douwe Maan 已提交
147
    desc 'Remove all or specific label(s)'
148
    params '~label1 ~"label 2"'
149
    condition do
150 151 152
      issuable.persisted? &&
        issuable.labels.any? &&
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
R
Rémy Coutable 已提交
153
    end
D
Douwe Maan 已提交
154 155 156
    command :unlabel do |labels_param = nil|
      if labels_param.present?
        label_ids = find_label_ids(labels_param)
157

B
barthc 已提交
158 159 160 161 162 163
        if label_ids.any?
          @updates[:remove_label_ids] ||= []
          @updates[:remove_label_ids] += label_ids

          @updates[:remove_label_ids].uniq!
        end
D
Douwe Maan 已提交
164 165 166
      else
        @updates[:label_ids] = []
      end
167 168
    end

D
Douwe Maan 已提交
169 170
    desc 'Replace all label(s)'
    params '~label1 ~"label 2"'
171
    condition do
172 173 174
      issuable.persisted? &&
        issuable.labels.any? &&
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
R
Rémy Coutable 已提交
175
    end
D
Douwe Maan 已提交
176 177 178
    command :relabel do |labels_param|
      label_ids = find_label_ids(labels_param)

B
barthc 已提交
179 180 181 182 183 184
      if label_ids.any?
        @updates[:label_ids] ||= []
        @updates[:label_ids] += label_ids

        @updates[:label_ids].uniq!
      end
185 186 187
    end

    desc 'Add a todo'
188
    condition do
189 190
      issuable.persisted? &&
        !TodoService.new.todo_exist?(issuable, current_user)
R
Rémy Coutable 已提交
191
    end
192
    command :todo do
193
      @updates[:todo_event] = 'add'
194 195 196
    end

    desc 'Mark todo as done'
197
    condition do
198 199
      issuable.persisted? &&
        TodoService.new.todo_exist?(issuable, current_user)
R
Rémy Coutable 已提交
200
    end
201 202 203 204 205
    command :done do
      @updates[:todo_event] = 'done'
    end

    desc 'Subscribe'
206
    condition do
207
      issuable.persisted? &&
208
        !issuable.subscribed?(current_user, project)
R
Rémy Coutable 已提交
209
    end
210 211 212 213 214
    command :subscribe do
      @updates[:subscription_event] = 'subscribe'
    end

    desc 'Unsubscribe'
215
    condition do
216
      issuable.persisted? &&
217
        issuable.subscribed?(current_user, project)
R
Rémy Coutable 已提交
218
    end
219 220 221 222
    command :unsubscribe do
      @updates[:subscription_event] = 'unsubscribe'
    end

R
Rémy Coutable 已提交
223
    desc 'Set due date'
D
Douwe Maan 已提交
224
    params '<in 2 days | this Friday | December 31st>'
225
    condition do
226
      issuable.respond_to?(:due_date) &&
227
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
R
Rémy Coutable 已提交
228
    end
D
Douwe Maan 已提交
229
    command :due do |due_date_param|
R
Rémy Coutable 已提交
230
      due_date = Chronic.parse(due_date_param).try(:to_date)
231 232 233 234 235

      @updates[:due_date] = due_date if due_date
    end

    desc 'Remove due date'
236
    condition do
237 238 239
      issuable.persisted? &&
        issuable.respond_to?(:due_date) &&
        issuable.due_date? &&
240
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
R
Rémy Coutable 已提交
241
    end
D
Douwe Maan 已提交
242
    command :remove_due_date do
243 244 245
      @updates[:due_date] = nil
    end

T
Thomas Balthazar 已提交
246 247 248 249 250 251 252 253 254 255 256 257
    desc do
      "Toggle the Work In Progress status"
    end
    condition do
      issuable.persisted? &&
        issuable.respond_to?(:work_in_progress?) &&
        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
    end
    command :wip do
      @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
    end

M
mhasbini 已提交
258 259 260 261 262 263 264 265 266 267 268 269
    desc 'Toggle emoji reward'
    params ':emoji:'
    condition do
      issuable.persisted?
    end
    command :award do |emoji|
      name = award_emoji_name(emoji)
      if name && issuable.user_can_award?(current_user, name)
        @updates[:emoji_award] = name
      end
    end

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
    desc 'Set time estimate'
    params '<1w 3d 2h 14m>'
    condition do
      current_user.can?(:"admin_#{issuable.to_ability_name}", project)
    end
    command :estimate do |raw_duration|
      time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration)

      if time_estimate
        @updates[:time_estimate] = time_estimate
      end
    end

    desc 'Add or substract spent time'
    params '<1h 30m | -1h 30m>'
    condition do
      current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
    end
    command :spend do |raw_duration|
      time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration)

      if time_spent
292
        @updates[:spend_time] = { duration: time_spent, user: current_user }
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
      end
    end

    desc 'Remove time estimate'
    condition do
      issuable.persisted? &&
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
    end
    command :remove_estimate do
      @updates[:time_estimate] = 0
    end

    desc 'Remove spent time'
    condition do
      issuable.persisted? &&
        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
    end
    command :remove_time_spent do
311
      @updates[:spend_time] = { duration: :reset, user: current_user }
312 313
    end

314 315 316
    # This is a dummy command, so that it appears in the autocomplete commands
    desc 'CC'
    params '@user'
317
    command :cc
318

319 320 321 322 323 324 325 326 327 328 329 330
    desc 'Defines target branch for MR'
    params '<Local branch name>'
    condition do
      issuable.respond_to?(:target_branch) &&
        (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) ||
          issuable.new_record?)
    end
    command :target_branch do |target_branch_param|
      branch_name = target_branch_param.strip
      @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
    end

331
    def find_label_ids(labels_param)
332
      label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
333
      labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
334 335

      label_ids_by_reference | labels_ids_by_name
336 337
    end

338
    def extract_references(arg, type)
339
      ext = Gitlab::ReferenceExtractor.new(project, current_user)
340
      ext.analyze(arg, author: current_user)
341 342 343

      ext.references(type)
    end
M
mhasbini 已提交
344 345 346 347 348

    def award_emoji_name(emoji)
      match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern)
      match[1] if match
    end
349 350
  end
end