gitlab-triage 1.6.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
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