gitlab-triage 1.7.1 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.gitlab-ci.yml +57 -41
  4. data/.gitlab/merge_request_templates/Release.md +35 -0
  5. data/.rubocop.yml +3 -0
  6. data/Gemfile +1 -1
  7. data/Guardfile +1 -1
  8. data/README.md +105 -8
  9. data/gitlab-triage.gemspec +1 -1
  10. data/lib/gitlab/triage/action/comment.rb +16 -3
  11. data/lib/gitlab/triage/api_query_builders/date_query_param_builder.rb +78 -0
  12. data/lib/gitlab/triage/command_builders/base_command_builder.rb +7 -3
  13. data/lib/gitlab/triage/command_builders/label_command_builder.rb +17 -0
  14. data/lib/gitlab/triage/command_builders/move_command_builder.rb +19 -0
  15. data/lib/gitlab/triage/command_builders/text_content_builder.rb +18 -6
  16. data/lib/gitlab/triage/engine.rb +42 -17
  17. data/lib/gitlab/triage/errors.rb +1 -1
  18. data/lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb +51 -5
  19. data/lib/gitlab/triage/option_parser.rb +10 -0
  20. data/lib/gitlab/triage/options.rb +2 -0
  21. data/lib/gitlab/triage/policies/rule_policy.rb +1 -1
  22. data/lib/gitlab/triage/policies/summary_policy.rb +1 -1
  23. data/lib/gitlab/triage/policies_resources/rule_resources.rb +5 -6
  24. data/lib/gitlab/triage/policies_resources/summary_resources.rb +5 -6
  25. data/lib/gitlab/triage/resource/base.rb +10 -1
  26. data/lib/gitlab/triage/resource/label.rb +15 -0
  27. data/lib/gitlab/triage/resource/merge_request.rb +9 -0
  28. data/lib/gitlab/triage/url_builders/url_builder.rb +10 -9
  29. data/lib/gitlab/triage/validators/limiter_validator.rb +3 -1
  30. data/lib/gitlab/triage/validators/params_validator.rb +5 -3
  31. data/lib/gitlab/triage/version.rb +3 -1
  32. data/support/.gitlab-ci.example.yml +2 -2
  33. data/support/.triage-policies.example.yml +2 -2
  34. metadata +6 -5
  35. data/lib/gitlab/triage/filters/forbidden_labels_conditions_filter.rb +0 -32
  36. data/lib/gitlab/triage/filters/issuable_date_conditions_filter.rb +0 -65
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.license = 'MIT'
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
- f.match(%r{^(test|spec|features)/})
16
+ f.match(%r{^(docs|test|spec|features)/})
17
17
  end
18
18
  spec.bindir = 'bin'
19
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
@@ -7,6 +7,7 @@ require_relative '../command_builders/label_command_builder'
7
7
  require_relative '../command_builders/remove_label_command_builder'
8
8
  require_relative '../command_builders/cc_command_builder'
9
9
  require_relative '../command_builders/status_command_builder'
10
+ require_relative '../command_builders/move_command_builder'
10
11
 
11
12
  module Gitlab
12
13
  module Triage
@@ -40,9 +41,10 @@ module Gitlab
40
41
  CommandBuilders::CommentCommandBuilder.new(
41
42
  [
42
43
  CommandBuilders::TextContentBuilder.new(policy.actions[:comment], resource: resource, network: network).build_command,
43
- CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels]).build_command,
44
- CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels]).build_command,
44
+ CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels], resource: resource, network: network).build_command,
45
+ CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels], resource: resource, network: network).build_command,
45
46
  CommandBuilders::CcCommandBuilder.new(policy.actions[:mention]).build_command,
47
+ CommandBuilders::MoveCommandBuilder.new(policy.actions[:move]).build_command,
46
48
  CommandBuilders::StatusCommandBuilder.new(policy.actions[:status]).build_command
47
49
  ]
48
50
  ).build_command
@@ -61,13 +63,24 @@ module Gitlab
61
63
  source_id: resource[:project_id],
62
64
  resource_type: policy.type,
63
65
  resource_id: resource['iid'],
64
- sub_resource_type: 'notes'
66
+ sub_resource_type: sub_resource_type
65
67
  ).build
66
68
 
67
69
  puts Gitlab::Triage::UI.debug "post_url: #{post_url}" if network.options.debug
68
70
 
69
71
  post_url
70
72
  end
73
+
74
+ def sub_resource_type
75
+ case type = policy.actions[:comment_type]
76
+ when 'comment', nil # nil is default
77
+ 'notes'
78
+ when 'thread'
79
+ 'discussions'
80
+ else
81
+ raise ArgumentError, "Unknown comment type: #{type}"
82
+ end
83
+ end
71
84
  end
72
85
  end
73
86
  end
@@ -0,0 +1,78 @@
1
+ require_relative '../validators/params_validator'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module APIQueryBuilders
6
+ class DateQueryParamBuilder
7
+ ATTRIBUTES = %w[updated_at created_at].freeze
8
+ CONDITIONS = %w[older_than newer_than].freeze
9
+ INTERVAL_TYPES = %w[days weeks months years].freeze
10
+
11
+ def self.filter_parameters
12
+ [
13
+ {
14
+ name: :attribute,
15
+ type: String,
16
+ values: ATTRIBUTES
17
+ },
18
+ {
19
+ name: :condition,
20
+ type: String,
21
+ values: CONDITIONS
22
+ },
23
+ {
24
+ name: :interval_type,
25
+ type: String,
26
+ values: INTERVAL_TYPES
27
+ },
28
+ {
29
+ name: :interval,
30
+ type: Numeric
31
+ }
32
+ ]
33
+ end
34
+
35
+ def self.applicable?(condition)
36
+ ATTRIBUTES.include?(condition[:attribute].to_s)
37
+ end
38
+
39
+ def initialize(condition_hash)
40
+ @attribute = condition_hash[:attribute].to_s
41
+ @interval_condition = condition_hash[:condition].to_sym
42
+ @interval_type = condition_hash[:interval_type]
43
+ @interval = condition_hash[:interval]
44
+ validate_condition(condition_hash)
45
+ end
46
+
47
+ def validate_condition(condition)
48
+ ParamsValidator.new(self.class.filter_parameters, condition).validate!
49
+ end
50
+
51
+ def param_name
52
+ prefix = attribute.sub(/_at\z/, '')
53
+ suffix =
54
+ case interval_condition
55
+ when :older_than
56
+ 'before'
57
+ when :newer_than
58
+ 'after'
59
+ end
60
+
61
+ "#{prefix}_#{suffix}"
62
+ end
63
+
64
+ def param_content
65
+ interval.public_send(interval_type).ago.to_date # rubocop:disable GitlabSecurity/PublicSend
66
+ end
67
+
68
+ def build_param
69
+ "&#{param_name}=#{param_content.strip}"
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :condition_hash, :attribute, :interval_condition, :interval_type, :interval
75
+ end
76
+ end
77
+ end
78
+ end
@@ -2,13 +2,15 @@ module Gitlab
2
2
  module Triage
3
3
  module CommandBuilders
4
4
  class BaseCommandBuilder
5
- def initialize(items)
5
+ def initialize(items, resource: nil, network: nil)
6
6
  @items = Array.wrap(items)
7
7
  @items.delete('')
8
+ @resource = resource&.with_indifferent_access
9
+ @network = network
8
10
  end
9
11
 
10
12
  def build_command
11
- if @items.any?
13
+ if items.any?
12
14
  [slash_command_string, content_string].compact.join(separator)
13
15
  else
14
16
  ""
@@ -17,6 +19,8 @@ module Gitlab
17
19
 
18
20
  private
19
21
 
22
+ attr_reader :items, :resource, :network
23
+
20
24
  def separator
21
25
  ' '
22
26
  end
@@ -26,7 +30,7 @@ module Gitlab
26
30
  end
27
31
 
28
32
  def content_string
29
- @items.map do |item|
33
+ items.map do |item|
30
34
  format_item(item)
31
35
  end.join(separator)
32
36
  end
@@ -4,8 +4,25 @@ module Gitlab
4
4
  module Triage
5
5
  module CommandBuilders
6
6
  class LabelCommandBuilder < BaseCommandBuilder
7
+ def build_command
8
+ ensure_labels_exist!
9
+
10
+ super
11
+ end
12
+
7
13
  private
8
14
 
15
+ def ensure_labels_exist!
16
+ items.each do |label|
17
+ label_opts = { project_id: resource[:project_id], name: label }
18
+
19
+ unless Resource::Label.new(label_opts, network: network).exist?
20
+ raise Resource::Label::LabelDoesntExistError,
21
+ "Label `#{label}` doesn't exist!"
22
+ end
23
+ end
24
+ end
25
+
9
26
  def slash_command_string
10
27
  "/label"
11
28
  end
@@ -0,0 +1,19 @@
1
+ require_relative 'base_command_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module CommandBuilders
6
+ class MoveCommandBuilder < BaseCommandBuilder
7
+ private
8
+
9
+ def slash_command_string
10
+ "/move"
11
+ end
12
+
13
+ def format_item(item)
14
+ item
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/core_ext/array/wrap'
4
+ require 'cgi'
4
5
 
5
6
  require_relative 'base_command_builder'
6
7
  require_relative '../resource/context'
@@ -33,13 +34,9 @@ module Gitlab
33
34
  }.freeze
34
35
  PLACEHOLDER_REGEX = /{{([\w\.]+)}}/.freeze
35
36
 
36
- attr_reader :resource, :network
37
-
38
37
  def initialize(
39
38
  items, resource: nil, network: nil, redact_confidentials: true)
40
- super(items)
41
- @resource = resource&.with_indifferent_access
42
- @network = network
39
+ super(items, resource: resource, network: network)
43
40
  @redact_confidentials = redact_confidentials
44
41
  end
45
42
 
@@ -76,7 +73,22 @@ module Gitlab
76
73
  template.sub(PLACEHOLDER_REGEX, attribute.to_s)
77
74
  end.join(', ')
78
75
 
79
- comment.gsub("{{#{placeholder}}}", formatted_text)
76
+ escaped_text =
77
+ case placeholder
78
+ when :items
79
+ # We don't need to escape it because it's recursive,
80
+ # which the contents should all be escaped already.
81
+ # Or put it another way, items isn't an attribute
82
+ # retrieved externally. It's a generated value which
83
+ # should be safe to begin with. At some point we
84
+ # may want to make this more distinguishable,
85
+ # separating values from API and values generated.
86
+ formatted_text
87
+ else
88
+ CGI.escape_html(formatted_text)
89
+ end
90
+
91
+ comment.gsub("{{#{placeholder}}}", escaped_text)
80
92
  end
81
93
  end
82
94
 
@@ -2,10 +2,8 @@ require 'active_support/all'
2
2
  require 'active_support/inflector'
3
3
 
4
4
  require_relative 'expand_condition'
5
- require_relative 'filters/issuable_date_conditions_filter'
6
5
  require_relative 'filters/merge_request_date_conditions_filter'
7
6
  require_relative 'filters/votes_conditions_filter'
8
- require_relative 'filters/forbidden_labels_conditions_filter'
9
7
  require_relative 'filters/no_additional_labels_conditions_filter'
10
8
  require_relative 'filters/author_member_conditions_filter'
11
9
  require_relative 'filters/assignee_member_conditions_filter'
@@ -16,6 +14,7 @@ require_relative 'policies/rule_policy'
16
14
  require_relative 'policies/summary_policy'
17
15
  require_relative 'policies_resources/rule_resources'
18
16
  require_relative 'policies_resources/summary_resources'
17
+ require_relative 'api_query_builders/date_query_param_builder'
19
18
  require_relative 'api_query_builders/single_query_param_builder'
20
19
  require_relative 'api_query_builders/multi_query_param_builder'
21
20
  require_relative 'url_builders/url_builder'
@@ -38,6 +37,7 @@ module Gitlab
38
37
  @options = options
39
38
  @network_adapter_class = network_adapter_class
40
39
 
40
+ assert_all!
41
41
  assert_project_id!
42
42
  assert_token!
43
43
  require_ruby_files
@@ -66,6 +66,7 @@ module Gitlab
66
66
 
67
67
  def assert_project_id!
68
68
  return if options.source_id
69
+ return if options.all
69
70
 
70
71
  raise ArgumentError, 'A project_id is needed (pass it with the `--source-id` option)!'
71
72
  end
@@ -76,6 +77,11 @@ module Gitlab
76
77
  raise ArgumentError, 'A token is needed (pass it with the `--token` option)!'
77
78
  end
78
79
 
80
+ def assert_all!
81
+ raise ArgumentError, '--all-projects option cannot be used in conjunction with --source and --source-id option!' if
82
+ options.all && (options.source || options.source_id)
83
+ end
84
+
79
85
  def require_ruby_files
80
86
  options.require_files.each(&method(:require))
81
87
  end
@@ -146,7 +152,7 @@ module Gitlab
146
152
  end
147
153
 
148
154
  def resources_for_rule(resource_type, rule)
149
- puts Gitlab::Triage::UI.header("Processing rule: **#{rule[:name]}**", char: '-')
155
+ puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule[:name]}**", char: '-')
150
156
 
151
157
  ExpandCondition.perform(rule_conditions(rule)) do |conditions|
152
158
  # retrieving the resources for every rule is inefficient
@@ -186,22 +192,31 @@ module Gitlab
186
192
  resources.select do |resource|
187
193
  results = []
188
194
 
195
+ # rubocop:disable Style/IfUnlessModifier
189
196
  if conditions[:date]
190
- results << case resource[:type]
191
- when 'issues'
192
- Filters::IssuableDateConditionsFilter.new(resource, conditions[:date]).calculate
193
- when 'merge_requests'
194
- Filters::MergeRequestDateConditionsFilter.new(resource, conditions[:date]).calculate
195
- else
196
- raise "Unknown resource type: #{resource[:type]}"
197
- end
197
+ results << Filters::MergeRequestDateConditionsFilter.new(resource, conditions[:date]).calculate
198
198
  end
199
- results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate if conditions[:upvotes]
200
- results << Filters::ForbiddenLabelsConditionsFilter.new(resource, conditions[:forbidden_labels]).calculate if conditions[:forbidden_labels]
201
- results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate if conditions[:no_additional_labels]
202
- results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member], network).calculate if conditions[:author_member]
203
- results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate if conditions[:assignee_member]
204
- results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate if conditions[:ruby]
199
+
200
+ if conditions[:upvotes]
201
+ results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate
202
+ end
203
+
204
+ if conditions[:no_additional_labels]
205
+ results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate
206
+ end
207
+
208
+ if conditions[:author_member]
209
+ results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member], network).calculate
210
+ end
211
+
212
+ if conditions[:assignee_member]
213
+ results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
214
+ end
215
+
216
+ if conditions[:ruby]
217
+ results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
218
+ end
219
+ # rubocop:enable Style/IfUnlessModifier
205
220
 
206
221
  results.all?
207
222
  end
@@ -224,17 +239,27 @@ module Gitlab
224
239
 
225
240
  condition_builders = []
226
241
  condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('labels', conditions[:labels], ',') if conditions[:labels]
242
+
243
+ if conditions[:forbidden_labels]
244
+ condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('not[labels]', conditions[:forbidden_labels], ',')
245
+ end
246
+
227
247
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('state', conditions[:state]) if conditions[:state]
228
248
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('milestone', Array(conditions[:milestone])[0]) if conditions[:milestone]
229
249
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
230
250
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('target_branch', conditions[:target_branch]) if conditions[:target_branch]
231
251
 
252
+ if conditions[:date] && APIQueryBuilders::DateQueryParamBuilder.applicable?(conditions[:date])
253
+ condition_builders << APIQueryBuilders::DateQueryParamBuilder.new(conditions.delete(:date))
254
+ end
255
+
232
256
  condition_builders.each do |condition_builder|
233
257
  params[condition_builder.param_name] = condition_builder.param_content
234
258
  end
235
259
 
236
260
  UrlBuilders::UrlBuilder.new(
237
261
  network_options: options,
262
+ all: options.all,
238
263
  source: options.source,
239
264
  source_id: options.source_id,
240
265
  resource_type: resource_type,
@@ -1 +1 @@
1
- require 'gitlab/triage/errors/network'
1
+ require_relative 'errors/network'
@@ -1,21 +1,67 @@
1
- require_relative 'issuable_date_conditions_filter'
1
+ require_relative 'base_conditions_filter'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
5
  module Filters
6
- class MergeRequestDateConditionsFilter < IssuableDateConditionsFilter
7
- ATTRIBUTES = %w[updated_at created_at merged_at].freeze
6
+ class MergeRequestDateConditionsFilter < BaseConditionsFilter
7
+ ATTRIBUTES = %w[merged_at].freeze
8
+ CONDITIONS = %w[older_than newer_than].freeze
9
+ INTERVAL_TYPES = %w[days weeks months years].freeze
10
+
11
+ def self.allowed_attributes
12
+ self::ATTRIBUTES
13
+ end
14
+
15
+ def self.filter_parameters
16
+ [
17
+ {
18
+ name: :attribute,
19
+ type: String,
20
+ values: allowed_attributes
21
+ },
22
+ {
23
+ name: :condition,
24
+ type: String,
25
+ values: CONDITIONS
26
+ },
27
+ {
28
+ name: :interval_type,
29
+ type: String,
30
+ values: INTERVAL_TYPES
31
+ },
32
+ {
33
+ name: :interval,
34
+ type: Numeric
35
+ }
36
+ ]
37
+ end
38
+
39
+ def initialize_variables(condition)
40
+ @attribute = condition[:attribute].to_sym
41
+ @condition = condition[:condition].to_sym
42
+ @interval_type = condition[:interval_type].to_sym
43
+ @interval = condition[:interval]
44
+ end
8
45
 
9
46
  # Guard against merge requests with no merged_at values
10
47
  def resource_value
11
- super if @resource[@attribute]
48
+ @resource[@attribute]&.to_date
49
+ end
50
+
51
+ def condition_value
52
+ @interval.public_send(@interval_type).ago.to_date # rubocop:disable GitlabSecurity/PublicSend
12
53
  end
13
54
 
14
55
  # Guard against merge requests with no merged_at values
15
56
  def calculate
16
57
  return false unless resource_value
17
58
 
18
- super
59
+ case @condition
60
+ when :older_than
61
+ resource_value < condition_value
62
+ when :newer_than
63
+ resource_value > condition_value
64
+ end
19
65
  end
20
66
  end
21
67
  end