gitlab-triage 1.6.1 → 1.10.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.gitlab-ci.yml +75 -40
  4. data/.gitlab/merge_request_templates/Release.md +35 -0
  5. data/.rubocop.yml +3 -0
  6. data/Gemfile +1 -1
  7. data/README.md +95 -4
  8. data/gitlab-triage.gemspec +1 -1
  9. data/lib/gitlab/triage/action/comment.rb +14 -1
  10. data/lib/gitlab/triage/api_query_builders/date_query_param_builder.rb +78 -0
  11. data/lib/gitlab/triage/command_builders/move_command_builder.rb +19 -0
  12. data/lib/gitlab/triage/command_builders/text_content_builder.rb +17 -1
  13. data/lib/gitlab/triage/engine.rb +41 -16
  14. data/lib/gitlab/triage/errors.rb +1 -1
  15. data/lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb +51 -5
  16. data/lib/gitlab/triage/network.rb +2 -1
  17. data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +13 -1
  18. data/lib/gitlab/triage/option_parser.rb +10 -0
  19. data/lib/gitlab/triage/options.rb +2 -0
  20. data/lib/gitlab/triage/policies/rule_policy.rb +1 -1
  21. data/lib/gitlab/triage/policies/summary_policy.rb +1 -1
  22. data/lib/gitlab/triage/policies_resources/rule_resources.rb +5 -6
  23. data/lib/gitlab/triage/policies_resources/summary_resources.rb +5 -6
  24. data/lib/gitlab/triage/resource/base.rb +5 -0
  25. data/lib/gitlab/triage/resource/issue.rb +4 -0
  26. data/lib/gitlab/triage/resource/merge_request.rb +13 -0
  27. data/lib/gitlab/triage/resource/shared/issuable.rb +26 -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/.triage-policies.example.yml +2 -2
  33. metadata +6 -4
  34. data/lib/gitlab/triage/filters/issuable_date_conditions_filter.rb +0 -65
@@ -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.delete_suffix('_at')
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
@@ -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'
@@ -76,7 +77,22 @@ module Gitlab
76
77
  template.sub(PLACEHOLDER_REGEX, attribute.to_s)
77
78
  end.join(', ')
78
79
 
79
- comment.gsub("{{#{placeholder}}}", formatted_text)
80
+ escaped_text =
81
+ case placeholder
82
+ when :items
83
+ # We don't need to escape it because it's recursive,
84
+ # which the contents should all be escaped already.
85
+ # Or put it another way, items isn't an attribute
86
+ # retrieved externally. It's a generated value which
87
+ # should be safe to begin with. At some point we
88
+ # may want to make this more distinguishable,
89
+ # separating values from API and values generated.
90
+ formatted_text
91
+ else
92
+ CGI.escape_html(formatted_text)
93
+ end
94
+
95
+ comment.gsub("{{#{placeholder}}}", escaped_text)
80
96
  end
81
97
  end
82
98
 
@@ -2,7 +2,6 @@ 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
7
  require_relative 'filters/forbidden_labels_conditions_filter'
@@ -16,6 +15,7 @@ require_relative 'policies/rule_policy'
16
15
  require_relative 'policies/summary_policy'
17
16
  require_relative 'policies_resources/rule_resources'
18
17
  require_relative 'policies_resources/summary_resources'
18
+ require_relative 'api_query_builders/date_query_param_builder'
19
19
  require_relative 'api_query_builders/single_query_param_builder'
20
20
  require_relative 'api_query_builders/multi_query_param_builder'
21
21
  require_relative 'url_builders/url_builder'
@@ -38,6 +38,7 @@ module Gitlab
38
38
  @options = options
39
39
  @network_adapter_class = network_adapter_class
40
40
 
41
+ assert_all!
41
42
  assert_project_id!
42
43
  assert_token!
43
44
  require_ruby_files
@@ -66,6 +67,7 @@ module Gitlab
66
67
 
67
68
  def assert_project_id!
68
69
  return if options.source_id
70
+ return if options.all
69
71
 
70
72
  raise ArgumentError, 'A project_id is needed (pass it with the `--source-id` option)!'
71
73
  end
@@ -76,6 +78,11 @@ module Gitlab
76
78
  raise ArgumentError, 'A token is needed (pass it with the `--token` option)!'
77
79
  end
78
80
 
81
+ def assert_all!
82
+ raise ArgumentError, '--all-projects option cannot be used in conjunction with --source and --source-id option!' if
83
+ options.all && (options.source || options.source_id)
84
+ end
85
+
79
86
  def require_ruby_files
80
87
  options.require_files.each(&method(:require))
81
88
  end
@@ -146,7 +153,7 @@ module Gitlab
146
153
  end
147
154
 
148
155
  def resources_for_rule(resource_type, rule)
149
- puts Gitlab::Triage::UI.header("Processing rule: **#{rule[:name]}**", char: '-')
156
+ puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule[:name]}**", char: '-')
150
157
 
151
158
  ExpandCondition.perform(rule_conditions(rule)) do |conditions|
152
159
  # retrieving the resources for every rule is inefficient
@@ -186,22 +193,35 @@ module Gitlab
186
193
  resources.select do |resource|
187
194
  results = []
188
195
 
196
+ # rubocop:disable Style/IfUnlessModifier
189
197
  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
198
+ results << Filters::MergeRequestDateConditionsFilter.new(resource, conditions[:date]).calculate
199
+ end
200
+
201
+ if conditions[:upvotes]
202
+ results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate
203
+ end
204
+
205
+ if conditions[:forbidden_labels]
206
+ results << Filters::ForbiddenLabelsConditionsFilter.new(resource, conditions[:forbidden_labels]).calculate
207
+ end
208
+
209
+ if conditions[:no_additional_labels]
210
+ results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate
198
211
  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]
212
+
213
+ if conditions[:author_member]
214
+ results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member], network).calculate
215
+ end
216
+
217
+ if conditions[:assignee_member]
218
+ results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
219
+ end
220
+
221
+ if conditions[:ruby]
222
+ results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
223
+ end
224
+ # rubocop:enable Style/IfUnlessModifier
205
225
 
206
226
  results.all?
207
227
  end
@@ -229,12 +249,17 @@ module Gitlab
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
@@ -31,11 +31,12 @@ module Gitlab
31
31
 
32
32
  begin
33
33
  print '.'
34
+ url = response.fetch(:next_page_url) { url }
34
35
 
35
36
  response = execute_with_retry([Net::ReadTimeout, Errors::Network::InternalServerError]) do
36
37
  puts Gitlab::Triage::UI.debug "query_api: #{url}" if options.debug
37
38
 
38
- @adapter.get(token, response.fetch(:next_page_url) { url })
39
+ @adapter.get(token, url)
39
40
  end
40
41
 
41
42
  results = response.delete(:results)
@@ -22,7 +22,7 @@ module Gitlab
22
22
 
23
23
  {
24
24
  more_pages: (response.headers["x-next-page"].to_s != ""),
25
- next_page_url: url + "&page=#{response.headers['x-next-page']}",
25
+ next_page_url: next_page_url(url, response),
26
26
  results: response.parsed_response,
27
27
  ratelimit_remaining: response.headers["ratelimit-remaining"].to_i,
28
28
  ratelimit_reset_at: Time.at(response.headers["ratelimit-reset"].to_i)
@@ -66,6 +66,18 @@ module Gitlab
66
66
 
67
67
  raise Errors::Network::InternalServerError, 'Internal server error encountered!'
68
68
  end
69
+
70
+ def next_page_url(url, response)
71
+ return unless response.headers['x-next-page'].present?
72
+
73
+ next_page = "&page=#{response.headers['x-next-page']}"
74
+
75
+ if url.include?('&page')
76
+ url.gsub(/&page=\d+/, next_page)
77
+ else
78
+ url + next_page
79
+ end
80
+ end
69
81
  end
70
82
  end
71
83
  end
@@ -23,6 +23,10 @@ module Gitlab
23
23
  options.policies_files << value
24
24
  end
25
25
 
26
+ opts.on('--all-projects', 'Process all projects the token has access to') do |value|
27
+ options.all = value
28
+ end
29
+
26
30
  opts.on('-s', '--source [type]', [:projects, :groups], 'The source type between [ projects or groups ], default value: projects') do |value|
27
31
  options.source = value
28
32
  end
@@ -59,6 +63,12 @@ module Gitlab
59
63
  exit # rubocop:disable Rails/Exit
60
64
  end
61
65
 
66
+ opts.on('-v', '--version', 'Print version') do
67
+ require_relative 'version'
68
+ $stdout.puts Gitlab::Triage::VERSION
69
+ exit # rubocop:disable Rails/Exit
70
+ end
71
+
62
72
  opts.on('--init', 'Initialize the project with a policy file') do
63
73
  example_path =
64
74
  File.expand_path('../../../support/.triage-policies.example.yml', __dir__)
@@ -3,6 +3,7 @@ module Gitlab
3
3
  Options = Struct.new(
4
4
  :dry_run,
5
5
  :policies_files,
6
+ :all,
6
7
  :source,
7
8
  :source_id,
8
9
  :token,
@@ -17,6 +18,7 @@ module Gitlab
17
18
  # Defaults
18
19
  self.host_url ||= 'https://gitlab.com'
19
20
  self.api_version ||= 'v4'
21
+ self.all ||= false
20
22
  self.source ||= 'projects'
21
23
  self.require_files ||= []
22
24
  self.policies_files ||= Set.new
@@ -15,7 +15,7 @@ module Gitlab
15
15
  type: type,
16
16
  policy_spec: policy_spec,
17
17
  action: action,
18
- resources: resources.resources,
18
+ resources: resources,
19
19
  network: network)
20
20
  end
21
21
  end
@@ -10,7 +10,7 @@ module Gitlab
10
10
  # Build an issue from several rules policies
11
11
  def build_issue
12
12
  action = actions[:summarize]
13
- issues = resources.build_issues do |inner_policy_spec, inner_resources|
13
+ issues = resources.map do |inner_policy_spec, inner_resources|
14
14
  Policies::RulePolicy.new(
15
15
  type, inner_policy_spec, inner_resources, network)
16
16
  .build_issue
@@ -1,20 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
4
+
3
5
  module Gitlab
4
6
  module Triage
5
7
  module PoliciesResources
6
8
  class RuleResources
7
- attr_reader :resources
9
+ include Enumerable
10
+ extend Forwardable
8
11
 
9
12
  def initialize(new_resources)
10
13
  @resources = new_resources
11
14
  end
12
15
 
13
- def each
14
- resources.each do |resource|
15
- yield(resource)
16
- end
17
- end
16
+ def_delegator :@resources, :each
18
17
  end
19
18
  end
20
19
  end