gitlab-triage 1.7.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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