gitlab-triage 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +131 -1
  3. data/gitlab-triage.gemspec +1 -0
  4. data/lib/gitlab/triage/api_query_builders/base_query_param_builder.rb +18 -0
  5. data/lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb +20 -0
  6. data/lib/gitlab/triage/api_query_builders/single_query_param_builder.rb +13 -0
  7. data/lib/gitlab/triage/engine.rb +45 -34
  8. data/lib/gitlab/triage/expand_condition.rb +21 -0
  9. data/lib/gitlab/triage/expand_condition/sequence.rb +19 -0
  10. data/lib/gitlab/triage/expand_condition/sequence/expansion.rb +187 -0
  11. data/lib/gitlab/triage/filters/assignee_member_conditions_filter.rb +13 -0
  12. data/lib/gitlab/triage/filters/author_member_conditions_filter.rb +13 -0
  13. data/lib/gitlab/triage/{limiters/base_conditions_limiter.rb → filters/base_conditions_filter.rb} +24 -12
  14. data/lib/gitlab/triage/{limiters/date_conditions_limiter.rb → filters/date_conditions_filter.rb} +4 -4
  15. data/lib/gitlab/triage/{limiters/forbidden_labels_conditions_limiter.rb → filters/forbidden_labels_conditions_filter.rb} +3 -7
  16. data/lib/gitlab/triage/{limiters/member_conditions_limiter.rb → filters/member_conditions_filter.rb} +5 -5
  17. data/lib/gitlab/triage/{limiters/name_conditions_limiter.rb → filters/name_conditions_filter.rb} +3 -7
  18. data/lib/gitlab/triage/{limiters/no_additional_labels_conditions_limiter.rb → filters/no_additional_labels_conditions_filter.rb} +4 -4
  19. data/lib/gitlab/triage/filters/ruby_conditions_filter.rb +31 -0
  20. data/lib/gitlab/triage/{limiters/votes_conditions_limiter.rb → filters/votes_conditions_filter.rb} +4 -4
  21. data/lib/gitlab/triage/network.rb +5 -0
  22. data/lib/gitlab/triage/resource/base.rb +46 -0
  23. data/lib/gitlab/triage/resource/context.rb +34 -0
  24. data/lib/gitlab/triage/resource/milestone.rb +82 -0
  25. data/lib/gitlab/triage/version.rb +1 -1
  26. metadata +35 -14
  27. data/lib/gitlab/triage/filter_builders/base_filter_builder.rb +0 -18
  28. data/lib/gitlab/triage/filter_builders/multi_filter_builder.rb +0 -20
  29. data/lib/gitlab/triage/filter_builders/single_filter_builder.rb +0 -13
  30. data/lib/gitlab/triage/limiters/assignee_member_conditions_limiter.rb +0 -13
  31. data/lib/gitlab/triage/limiters/author_member_conditions_limiter.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d82746964d894d23cf152a4a5fb996e23216355bba6aa639cb6ea339dcf6fd13
4
- data.tar.gz: cc931bd1c1e8f4097e3463b4351f426e39b69cbdbb9dc2e4ac7dafd426659de7
3
+ metadata.gz: 4ab21a6dffa12d92f9321ac3c6d2bc0c92627fad703fceb7ebf68494d8f5b259
4
+ data.tar.gz: e5868058935e55d70ccadaada8030a8bec92dee6ceeeea2f9996cd60f3823cdf
5
5
  SHA512:
6
- metadata.gz: 25effb45c3c03772ade06f3c8fee5b69598917a541aaece754fe053fc77f4bf92d8e74739d9cc3fe605ec275df73cc1c2110615b43d8ee0c57cf2fd3db2e8b3e
7
- data.tar.gz: 7c40513edecbed3ab28f9fd116fc554bd6a4fc9dfc828d461a2bccfa4d166aa0960385a1865c487c88172f348a62c19e9a0a197b626aba5e8c8ef1ba5bf6109c
6
+ metadata.gz: a875bebf70cd7dcc2a6c5f064d692535cdf98b664323fb640b22fed7f52ac4d5f49e8b21adb5b4942f626e4f4638d23f3042109ae13c495ae9c732d177e9298f
7
+ data.tar.gz: 01a627062e23ba73d40be5cc7260a92aa7be9ec30b9fb694585305ae919435174099a701d6045998c03738ee7fd7dcf0cac9ebf821abfea9b4697fd6f8f55e06
data/README.md CHANGED
@@ -44,7 +44,7 @@ resource_rules:
44
44
  interval_type: days
45
45
  interval: 5
46
46
  state: opened
47
- label:
47
+ labels:
48
48
  - No Label
49
49
  actions:
50
50
  labels:
@@ -89,6 +89,7 @@ Available condition types:
89
89
  - [`no_additional_labels` condition](#no-additional-labels-condition)
90
90
  - [`author_member` condition](#author-member-condition)
91
91
  - [`assignee_member` condition](#assignee-member-condition)
92
+ - [`ruby` condition](#ruby-condition)
92
93
 
93
94
  ##### Date condition
94
95
 
@@ -177,6 +178,71 @@ conditions:
177
178
  - feature proposal
178
179
  ```
179
180
 
181
+ ###### Labels over sequences
182
+
183
+ The name of a label can contain one or more sequence conditions, written
184
+ like `{0..9}`, which means `0`, `1`, `2`, and so on up to `9`. For each
185
+ number, the rule will be duplicated with the new label name.
186
+
187
+ Example:
188
+
189
+ ```yml
190
+ resource_rules:
191
+ issues:
192
+ rules:
193
+ - name: Add missing ~"missed\-deliverable" label
194
+ conditions:
195
+ labels:
196
+ - missed:{10..11}.{0..1}
197
+ - deliverable
198
+ actions:
199
+ labels:
200
+ - missed deliverable
201
+ ```
202
+
203
+ Which will be expanded into:
204
+
205
+ ```yml
206
+ resource_rules:
207
+ issues:
208
+ rules:
209
+ - name: Add missing ~"missed\-deliverable" label
210
+ conditions:
211
+ labels:
212
+ - missed:10.0
213
+ - deliverable
214
+ actions:
215
+ labels:
216
+ - missed deliverable
217
+
218
+ - name: Add missing ~"missed\-deliverable" label
219
+ conditions:
220
+ labels:
221
+ - missed:10.1
222
+ - deliverable
223
+ actions:
224
+ labels:
225
+ - missed deliverable
226
+
227
+ - name: Add missing ~"missed\-deliverable" label
228
+ conditions:
229
+ labels:
230
+ - missed:11.0
231
+ - deliverable
232
+ actions:
233
+ labels:
234
+ - missed deliverable
235
+
236
+ - name: Add missing ~"missed\-deliverable" label
237
+ conditions:
238
+ labels:
239
+ - missed:11.1
240
+ - deliverable
241
+ actions:
242
+ labels:
243
+ - missed deliverable
244
+ ```
245
+
180
246
  ##### Forbidden labels condition
181
247
 
182
248
  Accepts an array of strings. Each element in the array represents the name of a label to filter on.
@@ -251,6 +317,70 @@ conditions:
251
317
  source_id: 9970
252
318
  ```
253
319
 
320
+ ##### Ruby condition
321
+
322
+ This condition allows users to write a Ruby expression to be evaluated for
323
+ each resource. If it evaluates to a truthy value, it satisfies the condition.
324
+ If it evaluates to a falsey value, it does not satisfy the condition.
325
+
326
+ Accepts a string as the Ruby expression.
327
+
328
+ Example:
329
+
330
+ ```yml
331
+ conditions:
332
+ ruby: Date.today > milestone.succ.start_date
333
+ ```
334
+
335
+ In the above example, this describes that we want to act on the resources
336
+ which passed the next active milestone's starting date.
337
+
338
+ Here `milestone` will return a `Gitlab::Triage::Resource::Milestone` object,
339
+ representing the milestone of the questioning resource. `Milestone#succ` would
340
+ return the next active milestone, based on the `start_date` of all milestones
341
+ along with the representing milestone. If the milestone was coming from a
342
+ project, then it's based on all active milestones in that project. If the
343
+ milestone was coming from a group, then it's based on all active milestones
344
+ in the group.
345
+
346
+ If we also want to handle some edge cases, for example, a resource might not
347
+ have a milestone, and a milestone might not be active, and there might not
348
+ have a next milestone. We could instead write something like:
349
+
350
+ ```yml
351
+ conditions:
352
+ ruby: milestone&.active? && milestone&.succ && Date.today > milestone.succ.start_date
353
+ ```
354
+
355
+ This will make it only act on resources which have active milestones and
356
+ there exists next milestone which has already started.
357
+
358
+ Here's a list of currently available API:
359
+
360
+ ##### API
361
+
362
+ | Name | Return type | Description |
363
+ | ---- | ---- | ---- |
364
+ | milestone | Milestone | The milestone attached to the resource |
365
+
366
+ ##### Methods for `Milestone`
367
+
368
+ | Method | Return type | Description |
369
+ | ---- | ---- | ---- |
370
+ | id | Integer | The id of the milestone |
371
+ | iid | Integer | The iid of the milestone |
372
+ | project_id | Integer | The project id of the milestone if available |
373
+ | group_id | Integer | The group id of the milestone if available |
374
+ | title | String | The title of the milestone |
375
+ | description | String | The description of the milestone |
376
+ | state | String | The state of the milestone. Could be `active` or `closed` |
377
+ | due_date | Date | The due date of the milestone. Could be `nil` |
378
+ | start_date | Date | The start date of the milestone. Could be `nil` |
379
+ | updated_at | Time | The updated timestamp of the milestone |
380
+ | created_at | Time | The created timestamp of the milestone |
381
+ | succ | Milestone | The next active milestone beside this milestone |
382
+ | active? | Boolean | `true` if `state` is `active`; `false` otherwise |
383
+
254
384
  #### Actions field
255
385
 
256
386
  Used to declare an action to be carried out on a resource if **all** conditions are satisfied.
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency 'gitlab-styles', '~> 2.1'
27
27
  spec.add_development_dependency 'rake', '~> 10.0'
28
28
  spec.add_development_dependency 'rspec', '~> 3.0'
29
+ spec.add_development_dependency 'webmock', '~> 3.4'
29
30
  end
@@ -0,0 +1,18 @@
1
+ module Gitlab
2
+ module Triage
3
+ module APIQueryBuilders
4
+ class BaseQueryParamBuilder
5
+ attr_reader :param_name, :param_contents
6
+
7
+ def initialize(param_name, param_contents)
8
+ @param_name = param_name
9
+ @param_contents = param_contents
10
+ end
11
+
12
+ def build_param
13
+ "&#{param_name}=#{param_content}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'base_query_param_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module APIQueryBuilders
6
+ class MultiQueryParamBuilder < BaseQueryParamBuilder
7
+ attr_reader :separator
8
+
9
+ def initialize(param_name, param_contents, separator)
10
+ @separator = separator
11
+ super(param_name, param_contents)
12
+ end
13
+
14
+ def param_content
15
+ param_contents.join(separator)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'base_query_param_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module APIQueryBuilders
6
+ class SingleQueryParamBuilder < BaseQueryParamBuilder
7
+ def param_content
8
+ param_contents
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,19 +1,21 @@
1
1
  require 'active_support/all'
2
2
 
3
- require_relative 'limiters/date_conditions_limiter'
4
- require_relative 'limiters/votes_conditions_limiter'
5
- require_relative 'limiters/forbidden_labels_conditions_limiter'
6
- require_relative 'limiters/no_additional_labels_conditions_limiter'
7
- require_relative 'limiters/author_member_conditions_limiter'
8
- require_relative 'limiters/assignee_member_conditions_limiter'
3
+ require_relative 'expand_condition'
4
+ require_relative 'filters/date_conditions_filter'
5
+ require_relative 'filters/votes_conditions_filter'
6
+ require_relative 'filters/forbidden_labels_conditions_filter'
7
+ require_relative 'filters/no_additional_labels_conditions_filter'
8
+ require_relative 'filters/author_member_conditions_filter'
9
+ require_relative 'filters/assignee_member_conditions_filter'
10
+ require_relative 'filters/ruby_conditions_filter'
9
11
  require_relative 'command_builders/comment_body_builder'
10
12
  require_relative 'command_builders/comment_command_builder'
11
13
  require_relative 'command_builders/label_command_builder'
12
14
  require_relative 'command_builders/remove_label_command_builder'
13
15
  require_relative 'command_builders/cc_command_builder'
14
16
  require_relative 'command_builders/status_command_builder'
15
- require_relative 'filter_builders/single_filter_builder'
16
- require_relative 'filter_builders/multi_filter_builder'
17
+ require_relative 'api_query_builders/single_query_param_builder'
18
+ require_relative 'api_query_builders/multi_query_param_builder'
17
19
  require_relative 'url_builders/url_builder'
18
20
  require_relative 'network'
19
21
  require_relative 'network_adapters/httparty_adapter'
@@ -42,10 +44,10 @@ module Gitlab
42
44
  puts "Performing a dry run.\n\n" if options.dry_run
43
45
 
44
46
  resource_rules.each do |type, resource|
45
- puts Gitlab::Triage::UI.header("Processing rules for #{type}")
47
+ puts Gitlab::Triage::UI.header("Processing rules for #{type}", char: '-')
46
48
  puts
47
49
  resource[:rules].each do |rule|
48
- puts Gitlab::Triage::UI.header("Processing rule: #{rule[:name]}", char: '~')
50
+ puts Gitlab::Triage::UI.header("Processing rule: **#{rule[:name]}**", char: '-')
49
51
  puts
50
52
  process_rule(type, rule)
51
53
  end
@@ -87,40 +89,49 @@ module Gitlab
87
89
  end
88
90
 
89
91
  def process_rule(resource_type, rule)
90
- # retrieving the resources for every rule is inefficient
91
- # however, previous rules may affect those upcoming
92
- resources = network.query_api(options.token, build_get_url(resource_type, rule_conditions(rule)))
93
- puts "\n\nFound #{resources.count} resources..."
94
- puts "Limiting resources..."
95
- resources = limit_resources(resources, rule_conditions(rule))
96
- puts "Total resource after limiting: #{resources.count} resources"
97
- process_resources(resource_type, resources, rule)
92
+ ExpandCondition.perform(rule_conditions(rule)) do |conditions|
93
+ # retrieving the resources for every rule is inefficient
94
+ # however, previous rules may affect those upcoming
95
+ resources = network.query_api(options.token, build_get_url(resource_type, conditions))
96
+ puts "\n\n* Found #{resources.count} resources..."
97
+ print "* Limiting resources..."
98
+ resources = limit_resources(resources, conditions)
99
+ puts "\n* Total resource after limiting: #{resources.count} resources"
100
+ process_resources(resource_type, resources, rule)
101
+ end
98
102
  end
99
103
 
100
104
  def limit_resources(resources, conditions)
105
+ net = { host_url: host_url, api_version: api_version, token: options.token, network: network }
106
+
101
107
  resources.select do |resource|
102
108
  results = []
103
- results << Limiters::DateConditionsLimiter.new(resource, conditions[:date]).calculate if conditions[:date]
104
- results << Limiters::VotesConditionsLimiter.new(resource, conditions[:upvotes]).calculate if conditions[:upvotes]
105
- results << Limiters::ForbiddenLabelsConditionsLimiter.new(resource, conditions[:forbidden_labels]).calculate if conditions[:forbidden_labels]
106
- results << Limiters::NoAdditionalLabelsConditionsLimiter.new(resource, conditions.fetch(:labels) { [] }).calculate if conditions[:no_additional_labels]
107
- results << Limiters::AuthorMemberConditionsLimiter.new(resource, conditions[:author_member], { host_url: host_url, api_version: api_version, token: options.token, network: network }).calculate if conditions[:author_member]
108
- results << Limiters::AssigneeMemberConditionsLimiter.new(resource, conditions[:assignee_member], { host_url: host_url, api_version: api_version, token: options.token, network: network }).calculate if conditions[:assignee_member]
109
- !results.uniq.include?(false)
109
+
110
+ results << Filters::DateConditionsFilter.new(resource, conditions[:date]).calculate if conditions[:date]
111
+ results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate if conditions[:upvotes]
112
+ results << Filters::ForbiddenLabelsConditionsFilter.new(resource, conditions[:forbidden_labels]).calculate if conditions[:forbidden_labels]
113
+ results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate if conditions[:no_additional_labels]
114
+ results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member], net).calculate if conditions[:author_member]
115
+ results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], net).calculate if conditions[:assignee_member]
116
+ results << Filters::RubyConditionsFilter.new(resource, conditions, net).calculate if conditions[:ruby]
117
+
118
+ results.all?
110
119
  end
111
120
  end
112
121
 
113
122
  def process_resources(resource_type, resources, rule)
123
+ if options.dry_run
124
+ puts "\nThe following comments would be posted for the rule **#{rule[:name]}**:\n\n"
125
+ end
126
+
114
127
  resources.each do |resource|
115
128
  comment = build_comment(rule_actions(rule), resource: resource)
116
129
 
117
130
  if options.dry_run
118
- puts "\nThe following comment would be posted for the rule '#{rule[:name]}':\n\n"
119
- puts ">>>\n#{comment}\n>>>"
120
- break
131
+ puts "# #{resource[:web_url]}\n```\n#{comment}\n```\n"
132
+ else
133
+ network.post_api(options.token, build_post_url(resource_type, resource), comment)
121
134
  end
122
-
123
- network.post_api(options.token, build_post_url(resource_type, resource), comment)
124
135
  end
125
136
  end
126
137
 
@@ -144,12 +155,12 @@ module Gitlab
144
155
  }
145
156
 
146
157
  condition_builders = []
147
- condition_builders << FilterBuilders::MultiFilterBuilder.new('labels', conditions[:labels], ',') if conditions[:labels]
148
- condition_builders << FilterBuilders::SingleFilterBuilder.new('state', conditions[:state]) if conditions[:state]
149
- condition_builders << FilterBuilders::MultiFilterBuilder.new('milestone', conditions[:milestone], ',') if conditions[:milestone]
158
+ condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('labels', conditions[:labels], ',') if conditions[:labels]
159
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('state', conditions[:state]) if conditions[:state]
160
+ condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('milestone', conditions[:milestone], ',') if conditions[:milestone]
150
161
 
151
162
  condition_builders.each do |condition_builder|
152
- params[condition_builder.filter_name] = condition_builder.filter_content
163
+ params[condition_builder.param_name] = condition_builder.param_content
153
164
  end
154
165
 
155
166
  get_url = UrlBuilders::UrlBuilder.new(
@@ -0,0 +1,21 @@
1
+ require_relative 'expand_condition/sequence'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module ExpandCondition
6
+ PIPELINE = [
7
+ Sequence
8
+ ].freeze
9
+
10
+ def self.perform(conditions, pipeline = PIPELINE, &block)
11
+ expand([conditions], pipeline).each(&block)
12
+ end
13
+
14
+ def self.expand(conditions, pipeline = PIPELINE)
15
+ pipeline.inject(conditions) do |result, job|
16
+ result.flat_map(&job.method(:expand))
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'sequence/expansion'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module ExpandCondition
6
+ module Sequence
7
+ def self.expand(conditions)
8
+ labels = conditions[:labels]
9
+
10
+ return conditions unless labels
11
+
12
+ Expansion.perform(labels).map do |new_labels|
13
+ conditions.merge(labels: new_labels)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,187 @@
1
+ module Gitlab
2
+ module Triage
3
+ module ExpandCondition
4
+ module Sequence
5
+ module Expansion
6
+ PATTERN = /\{(\d+)\.\.(\d+)\}/
7
+
8
+ # This method will take a list of strings, which contains the
9
+ # sequence pattern, and expand them into each possible matches.
10
+ # For example, suppose the input is:
11
+ #
12
+ # * a:{0..1}
13
+ # * b:{2..3}
14
+ # * c
15
+ #
16
+ # The result would be:
17
+ #
18
+ # * * a:0
19
+ # * b:2
20
+ # * c
21
+ # * * a:0
22
+ # * b:3
23
+ # * c
24
+ # * * a:1
25
+ # * b:2
26
+ # * c
27
+ # * * a:1
28
+ # * b:3
29
+ # * c
30
+ #
31
+ # We get this by picking the 1st number from the 1st string,
32
+ # which is 0 from "a:{0..1}", and the 1st number from the
33
+ # 2nd string, which is 2 from "b:{2..3}", and since the 3rd
34
+ # string is just a fixed string, we just pick it.
35
+ #
36
+ # This way we have the first possible match, that is:
37
+ #
38
+ # * a:0
39
+ # * b:2
40
+ # * c
41
+ #
42
+ # Then we repeat the process by picking the next number for the
43
+ # next possible match, starting from the least significant
44
+ # string, which is c, but there's nothing more to pick. Then we
45
+ # go to the next one, which will be the 2nd string: "b:{2..3}",
46
+ # and we pick the 2nd number for it: 3. Since we have a new pick,
47
+ # we have a new possible match:
48
+ #
49
+ # * a:0
50
+ # * b:3
51
+ # * c
52
+ #
53
+ # Again we repeat the process, and 2nd string doesn't have more
54
+ # choices therefore we need to go to the 1st string now. When
55
+ # this happens, we'll need to reset the picks from the previous
56
+ # string, thus 2nd string will go back to 2. The next number for
57
+ # the 1st string is 1, and then we form the new match:
58
+ #
59
+ # * a:1
60
+ # * b:2
61
+ # * c
62
+ #
63
+ # The next step will be the last match by picking the next number
64
+ # from the 2nd string again: 3, and we get:
65
+ #
66
+ # * a:1
67
+ # * b:3
68
+ # * c
69
+ #
70
+ # The method will stop here because it had walked through all the
71
+ # possible combinations. The total number of results is the product
72
+ # of numbers of sequences.
73
+ #
74
+ # Note that a string can contain multiple sequences, and it will
75
+ # also walk through them one by one. For example, given:
76
+ #
77
+ # * a:{0..1}:{2..3}
78
+ # * c
79
+ #
80
+ # We'll get:
81
+ #
82
+ # * * a:0:2
83
+ # * c
84
+ # * * a:0:3
85
+ # * c
86
+ # * * a:1:2
87
+ # * c
88
+ # * * a:1:3
89
+ # * c
90
+ def self.perform(strings)
91
+ expanded_strings = strings.map(&method(:expand_sequences))
92
+
93
+ product_of_all(expanded_strings)
94
+ end
95
+
96
+ # This method returns the product of list of lists. For example,
97
+ # giving it [%w[a:0 a:1], %w[b:2 b:3], %w[c]] will return:
98
+ #
99
+ # [%w[a:0 b:2 c], %w[a:0 b:3 c], %w[a:1 b:2 c], %w[a:1 b:3 c]]
100
+ def self.product_of_all(expanded_strings)
101
+ expanded_strings.first.product(*expanded_strings.drop(1))
102
+ end
103
+
104
+ # This method expands the string from the sequences. For example,
105
+ # giving it "a:{0..1}:{2..3}" will return:
106
+ #
107
+ # %w[
108
+ # a:0:2
109
+ # a:0:3
110
+ # a:1:2
111
+ # a:1:3
112
+ # ]
113
+ def self.expand_sequences(string)
114
+ expand(string, scan_sequences(string))
115
+ end
116
+
117
+ # This method extracts the sequences from the string. For example,
118
+ # giving it "a:{0..1}:{2..3}" will return:
119
+ #
120
+ # [0..1, 2..3]
121
+ def self.scan_sequences(string)
122
+ string.scan(PATTERN).map do |(lower, upper)|
123
+ Integer(lower)..Integer(upper)
124
+ end
125
+ end
126
+
127
+ # This recursive method does the heavy lifting. It substitutes the
128
+ # sequence patterns in a string with a picked number from the
129
+ # sequence, and collect all the results. Here's an example:
130
+ #
131
+ # expand("a:{0..1}:{2..3}", [0..1, 2..3])
132
+ #
133
+ # This means that we want to pick the numbers from the sequences,
134
+ # and fill them back to the string containing the pattern in the
135
+ # respective order. We don't care which pattern it is because
136
+ # the order should have spoken for it. The result will be:
137
+ #
138
+ # %w[
139
+ # a:0:2
140
+ # a:0:3
141
+ # a:1:2
142
+ # a:1:3
143
+ # ]
144
+ #
145
+ # We start by picking the first sequence, which is 0..1 here. We
146
+ # want all the possible picks, thus we flat_map on it, substituting
147
+ # the first pattern with the picked number. This means we get:
148
+ #
149
+ # "a:0:{2..3}"
150
+ #
151
+ # For the first iteration. Before we jump to the next pick from the
152
+ # sequence, we recursively do this again on the current string,
153
+ # which only has one sequence pattern left. It will be called like:
154
+ #
155
+ # expand("a:0:{2..3}", [2..3])
156
+ #
157
+ # Because we also dropped the first sequence we have already used.
158
+ # On the next recursive call, we don't have any sequences left,
159
+ # therefore we just return the current string: "a:0:2".
160
+ #
161
+ # Flattening the recursion, it might look like this:
162
+ #
163
+ # (0..1).flat_map do |x|
164
+ # (2..3).flat_map do |y|
165
+ # "a:{0..1}:{2..3}".sub(PATTERN, x.to_s).sub(PATTERN, y.to_s)
166
+ # end
167
+ # end
168
+ #
169
+ # So here we could clearly see that we go deep first, substituting
170
+ # the least significant pattern first, and then go back to the
171
+ # previous one, until there's nothing more to pick.
172
+ def self.expand(string, sequences)
173
+ if sequences.empty?
174
+ [string]
175
+ else
176
+ remainings = sequences.drop(1)
177
+
178
+ sequences.first.flat_map do |number|
179
+ expand(string.sub(PATTERN, number.to_s), remainings)
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'member_conditions_filter'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Filters
6
+ class AssigneeMemberConditionsFilter < MemberConditionsFilter
7
+ def member_field
8
+ :assignee
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'member_conditions_filter'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Filters
6
+ class AuthorMemberConditionsFilter < MemberConditionsFilter
7
+ def member_field
8
+ :author
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,61 +2,73 @@ require 'active_support/all'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- module Limiters
6
- class BaseConditionsLimiter
5
+ module Filters
6
+ class BaseConditionsFilter
7
7
  def initialize(resource, condition)
8
8
  @resource = resource
9
9
  validate_condition(condition)
10
10
  initialize_variables(condition)
11
11
  end
12
12
 
13
- def self.params_limiter_names(params = nil)
14
- params ||= limiter_parameters
13
+ def calculate
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def self.filter_parameters
18
+ []
19
+ end
20
+
21
+ def self.params_filter_names(params = nil)
22
+ params ||= filter_parameters
15
23
 
16
24
  params.map do |param|
17
25
  param[:name]
18
26
  end
19
27
  end
20
28
 
21
- def self.all_params_limiter_names
22
- params_limiter_names
29
+ def self.all_params_filter_names
30
+ params_filter_names
23
31
  end
24
32
 
25
33
  def self.params_checking_condition_value
26
- params_limiter_names params_check_for_field(:values)
34
+ params_filter_names params_check_for_field(:values)
27
35
  end
28
36
 
29
37
  def self.params_checking_condition_type
30
- params_limiter_names params_check_for_field(:type)
38
+ params_filter_names params_check_for_field(:type)
31
39
  end
32
40
 
33
41
  def self.params_check_for_field(field)
34
- limiter_parameters.select do |param|
42
+ filter_parameters.select do |param|
35
43
  param[field].present?
36
44
  end
37
45
  end
38
46
 
47
+ private
48
+
39
49
  def validate_condition(condition)
40
50
  validate_required_parameters(condition)
41
51
  validate_parameter_types(condition)
42
52
  validate_parameter_content(condition)
43
53
  end
44
54
 
55
+ def initialize_variables(condition); end
56
+
45
57
  def validate_required_parameters(condition)
46
- self.class.limiter_parameters.each do |param|
58
+ self.class.filter_parameters.each do |param|
47
59
  raise ArgumentError, "#{param[:name]} is a required parameter" unless condition[param[:name]]
48
60
  end
49
61
  end
50
62
 
51
63
  def validate_parameter_types(condition)
52
- self.class.limiter_parameters.each do |param|
64
+ self.class.filter_parameters.each do |param|
53
65
  param_types = Array(param[:type]).flatten
54
66
  raise ArgumentError, "#{param[:name]} must be of type #{param[:type]}" unless param_types.any? { |type| condition[param[:name]].is_a?(type) }
55
67
  end
56
68
  end
57
69
 
58
70
  def validate_parameter_content(condition)
59
- self.class.limiter_parameters.each do |param|
71
+ self.class.filter_parameters.each do |param|
60
72
  if param[:values]
61
73
  raise ArgumentError, "#{param[:name]} must be of one of #{param[:values].join(',')}" unless param[:values].include?(condition[param[:name]])
62
74
  end
@@ -1,14 +1,14 @@
1
- require_relative 'base_conditions_limiter'
1
+ require_relative 'base_conditions_filter'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- module Limiters
6
- class DateConditionsLimiter < BaseConditionsLimiter
5
+ module Filters
6
+ class DateConditionsFilter < BaseConditionsFilter
7
7
  ATTRIBUTES = %w[updated_at created_at].freeze
8
8
  CONDITIONS = %w[older_than newer_than].freeze
9
9
  INTERVAL_TYPES = %w[days weeks months years].freeze
10
10
 
11
- def self.limiter_parameters
11
+ def self.filter_parameters
12
12
  [
13
13
  {
14
14
  name: :attribute,
@@ -1,13 +1,9 @@
1
- require_relative 'base_conditions_limiter'
1
+ require_relative 'base_conditions_filter'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- module Limiters
6
- class ForbiddenLabelsConditionsLimiter < BaseConditionsLimiter
7
- def self.limiter_parameters
8
- []
9
- end
10
-
5
+ module Filters
6
+ class ForbiddenLabelsConditionsFilter < BaseConditionsFilter
11
7
  def validate_condition(condition)
12
8
  raise ArgumentError, 'condition must be an array containing forbidden label values' unless condition.is_a?(Array)
13
9
  end
@@ -1,10 +1,10 @@
1
- require_relative 'base_conditions_limiter'
1
+ require_relative 'base_conditions_filter'
2
2
  require_relative '../url_builders/url_builder'
3
3
 
4
4
  module Gitlab
5
5
  module Triage
6
- module Limiters
7
- class MemberConditionsLimiter < BaseConditionsLimiter
6
+ module Filters
7
+ class MemberConditionsFilter < BaseConditionsFilter
8
8
  SOURCES = %w[project group].freeze
9
9
  CONDITIONS = %w[member_of not_member_of].freeze
10
10
 
@@ -14,7 +14,7 @@ module Gitlab
14
14
  super(resource, condition)
15
15
  end
16
16
 
17
- def self.limiter_parameters
17
+ def self.filter_parameters
18
18
  [
19
19
  {
20
20
  name: :source,
@@ -61,7 +61,7 @@ module Gitlab
61
61
  end
62
62
 
63
63
  def members
64
- @members ||= @network.query_api(@net[:token], member_url)
64
+ @members ||= @network.query_api_cached(@net[:token], member_url)
65
65
  end
66
66
 
67
67
  def member_url
@@ -1,13 +1,9 @@
1
- require_relative 'base_conditions_limiter'
1
+ require_relative 'base_conditions_filter'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- module Limiters
6
- class NameConditionsLimiter < BaseConditionsLimiter
7
- def self.limiter_parameters
8
- []
9
- end
10
-
5
+ module Filters
6
+ class NameConditionsFilter < BaseConditionsFilter
11
7
  def initialize_variables(matching_name)
12
8
  @attribute = :name
13
9
  @matching_name = matching_name
@@ -1,10 +1,10 @@
1
- require_relative 'base_conditions_limiter'
1
+ require_relative 'base_conditions_filter'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- module Limiters
6
- class NoAdditionalLabelsConditionsLimiter < BaseConditionsLimiter
7
- def self.limiter_parameters
5
+ module Filters
6
+ class NoAdditionalLabelsConditionsFilter < BaseConditionsFilter
7
+ def self.filter_parameters
8
8
  []
9
9
  end
10
10
 
@@ -0,0 +1,31 @@
1
+ require_relative 'base_conditions_filter'
2
+ require_relative '../resource/context'
3
+ require 'date'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module Filters
8
+ class RubyConditionsFilter < BaseConditionsFilter
9
+ def self.limiter_parameters
10
+ [{ name: :ruby, type: String }]
11
+ end
12
+
13
+ def initialize(resource, condition, net = {})
14
+ super(resource, condition)
15
+
16
+ @net = net
17
+ end
18
+
19
+ def calculate
20
+ !!Resource::Context.new(@resource, @net).eval(@expression)
21
+ end
22
+
23
+ private
24
+
25
+ def initialize_variables(condition)
26
+ @expression = condition[:ruby]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,13 +1,13 @@
1
- require_relative 'base_conditions_limiter'
1
+ require_relative 'base_conditions_filter'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- module Limiters
6
- class VotesConditionsLimiter < BaseConditionsLimiter
5
+ module Filters
6
+ class VotesConditionsFilter < BaseConditionsFilter
7
7
  ATTRIBUTES = %w[upvotes downvotes].freeze
8
8
  CONDITIONS = %w[greater_than less_than].freeze
9
9
 
10
- def self.limiter_parameters
10
+ def self.filter_parameters
11
11
  [
12
12
  {
13
13
  name: :attribute,
@@ -14,6 +14,11 @@ module Gitlab
14
14
  def initialize(adapter, options = {})
15
15
  @adapter = adapter
16
16
  @options = options
17
+ @cache = Hash.new { |hash, key| hash[key] = {} }
18
+ end
19
+
20
+ def query_api_cached(token, url)
21
+ @cache.dig(token, url) || @cache[token][url] = query_api(token, url)
17
22
  end
18
23
 
19
24
  def query_api(token, url)
@@ -0,0 +1,46 @@
1
+ require_relative '../url_builders/url_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Resource
6
+ class Base
7
+ attr_reader :resource, :net
8
+
9
+ def initialize(new_resource, new_net)
10
+ @resource = new_resource
11
+ @net = new_net
12
+ end
13
+
14
+ private
15
+
16
+ def network
17
+ net[:network]
18
+ end
19
+
20
+ def url(params = {})
21
+ UrlBuilders::UrlBuilder.new(
22
+ net_opts.merge(params: { per_page: 100 }.merge(params))
23
+ ).build
24
+ end
25
+
26
+ def net_opts
27
+ {
28
+ host_url: net[:host_url],
29
+ api_version: net[:api_version],
30
+ resource_type: self.class.name.demodulize.underscore.pluralize,
31
+ source: source,
32
+ source_id: resource[:"#{source.singularize}_id"]
33
+ }
34
+ end
35
+
36
+ def source
37
+ if resource[:project_id]
38
+ 'projects'
39
+ elsif resource[:group_id]
40
+ 'groups'
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'base'
2
+ require_relative 'milestone'
3
+
4
+ module Gitlab
5
+ module Triage
6
+ module Resource
7
+ class Context < Base
8
+ EvaluationError = Class.new(RuntimeError)
9
+
10
+ def eval(ruby)
11
+ instance_eval <<~RUBY
12
+ begin
13
+ #{ruby}
14
+ rescue StandardError, ScriptError => e
15
+ raise EvaluationError.new(e.message)
16
+ end
17
+ RUBY
18
+ rescue EvaluationError => e
19
+ # This way we could obtain the original backtrace and error
20
+ # If we just let instance_eval raise an error, the backtrace
21
+ # won't contain the actual line where it's giving an error.
22
+ raise e.cause
23
+ end
24
+
25
+ private
26
+
27
+ def milestone
28
+ @milestone ||=
29
+ resource[:milestone] && Milestone.new(resource[:milestone], net)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,82 @@
1
+ require_relative 'base'
2
+ require 'date'
3
+ require 'time'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module Resource
8
+ class Milestone < Base
9
+ FIELDS = %i[
10
+ id
11
+ iid
12
+ project_id
13
+ group_id
14
+ title
15
+ description
16
+ state
17
+ ].freeze
18
+
19
+ DATE_FIELDS = %i[
20
+ due_date
21
+ start_date
22
+ ].freeze
23
+
24
+ TIME_FIELDS = %i[
25
+ updated_at
26
+ created_at
27
+ ].freeze
28
+
29
+ FIELDS.each do |field|
30
+ define_method(field) do
31
+ resource[field]
32
+ end
33
+ end
34
+
35
+ DATE_FIELDS.each do |field|
36
+ define_method(field) do
37
+ value = resource[field]
38
+
39
+ Date.parse(value) if value
40
+ end
41
+ end
42
+
43
+ TIME_FIELDS.each do |field|
44
+ define_method(field) do
45
+ value = resource[field]
46
+
47
+ Time.parse(value) if value
48
+ end
49
+ end
50
+
51
+ def succ
52
+ index = current_index
53
+
54
+ all_active_with_start_date[index.succ] if index
55
+ end
56
+
57
+ def active?
58
+ state == 'active'
59
+ end
60
+
61
+ private
62
+
63
+ def current_index
64
+ all_active_with_start_date
65
+ .index { |milestone| milestone.id == id }
66
+ end
67
+
68
+ def all_active_with_start_date
69
+ @all_active_with_start_date ||=
70
+ all_active.select(&:start_date).sort_by(&:start_date)
71
+ end
72
+
73
+ def all_active
74
+ @all_active ||=
75
+ network
76
+ .query_api_cached(net[:token], url(state: 'active'))
77
+ .map { |milestone| self.class.new(milestone, net) }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,5 +1,5 @@
1
1
  module Gitlab
2
2
  module Triage
3
- VERSION = '0.6.0'.freeze
3
+ VERSION = '0.7.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-triage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-02 00:00:00.000000000 Z
11
+ date: 2018-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.4'
97
111
  description:
98
112
  email:
99
113
  - remy@rymai.me
@@ -117,6 +131,9 @@ files:
117
131
  - bin/gitlab-triage
118
132
  - gitlab-triage.gemspec
119
133
  - lib/gitlab/triage.rb
134
+ - lib/gitlab/triage/api_query_builders/base_query_param_builder.rb
135
+ - lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb
136
+ - lib/gitlab/triage/api_query_builders/single_query_param_builder.rb
120
137
  - lib/gitlab/triage/command_builders/base_command_builder.rb
121
138
  - lib/gitlab/triage/command_builders/cc_command_builder.rb
122
139
  - lib/gitlab/triage/command_builders/comment_body_builder.rb
@@ -125,22 +142,26 @@ files:
125
142
  - lib/gitlab/triage/command_builders/remove_label_command_builder.rb
126
143
  - lib/gitlab/triage/command_builders/status_command_builder.rb
127
144
  - lib/gitlab/triage/engine.rb
128
- - lib/gitlab/triage/filter_builders/base_filter_builder.rb
129
- - lib/gitlab/triage/filter_builders/multi_filter_builder.rb
130
- - lib/gitlab/triage/filter_builders/single_filter_builder.rb
131
- - lib/gitlab/triage/limiters/assignee_member_conditions_limiter.rb
132
- - lib/gitlab/triage/limiters/author_member_conditions_limiter.rb
133
- - lib/gitlab/triage/limiters/base_conditions_limiter.rb
134
- - lib/gitlab/triage/limiters/date_conditions_limiter.rb
135
- - lib/gitlab/triage/limiters/forbidden_labels_conditions_limiter.rb
136
- - lib/gitlab/triage/limiters/member_conditions_limiter.rb
137
- - lib/gitlab/triage/limiters/name_conditions_limiter.rb
138
- - lib/gitlab/triage/limiters/no_additional_labels_conditions_limiter.rb
139
- - lib/gitlab/triage/limiters/votes_conditions_limiter.rb
145
+ - lib/gitlab/triage/expand_condition.rb
146
+ - lib/gitlab/triage/expand_condition/sequence.rb
147
+ - lib/gitlab/triage/expand_condition/sequence/expansion.rb
148
+ - lib/gitlab/triage/filters/assignee_member_conditions_filter.rb
149
+ - lib/gitlab/triage/filters/author_member_conditions_filter.rb
150
+ - lib/gitlab/triage/filters/base_conditions_filter.rb
151
+ - lib/gitlab/triage/filters/date_conditions_filter.rb
152
+ - lib/gitlab/triage/filters/forbidden_labels_conditions_filter.rb
153
+ - lib/gitlab/triage/filters/member_conditions_filter.rb
154
+ - lib/gitlab/triage/filters/name_conditions_filter.rb
155
+ - lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb
156
+ - lib/gitlab/triage/filters/ruby_conditions_filter.rb
157
+ - lib/gitlab/triage/filters/votes_conditions_filter.rb
140
158
  - lib/gitlab/triage/network.rb
141
159
  - lib/gitlab/triage/network_adapters/base_adapter.rb
142
160
  - lib/gitlab/triage/network_adapters/httparty_adapter.rb
143
161
  - lib/gitlab/triage/network_adapters/test_adapter.rb
162
+ - lib/gitlab/triage/resource/base.rb
163
+ - lib/gitlab/triage/resource/context.rb
164
+ - lib/gitlab/triage/resource/milestone.rb
144
165
  - lib/gitlab/triage/retryable.rb
145
166
  - lib/gitlab/triage/ui.rb
146
167
  - lib/gitlab/triage/url_builders/url_builder.rb
@@ -1,18 +0,0 @@
1
- module Gitlab
2
- module Triage
3
- module FilterBuilders
4
- class BaseFilterBuilder
5
- attr_reader :filter_name, :filter_contents
6
-
7
- def initialize(filter_name, filter_contents)
8
- @filter_name = filter_name
9
- @filter_contents = filter_contents
10
- end
11
-
12
- def build_filter
13
- "&#{filter_name}=#{filter_content}"
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,20 +0,0 @@
1
- require_relative 'base_filter_builder'
2
-
3
- module Gitlab
4
- module Triage
5
- module FilterBuilders
6
- class MultiFilterBuilder < BaseFilterBuilder
7
- attr_reader :separator
8
-
9
- def initialize(filter_name, filter_contents, separator)
10
- @separator = separator
11
- super(filter_name, filter_contents)
12
- end
13
-
14
- def filter_content
15
- filter_contents.join(separator)
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,13 +0,0 @@
1
- require_relative 'base_filter_builder'
2
-
3
- module Gitlab
4
- module Triage
5
- module FilterBuilders
6
- class SingleFilterBuilder < BaseFilterBuilder
7
- def filter_content
8
- filter_contents
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- require_relative 'member_conditions_limiter'
2
-
3
- module Gitlab
4
- module Triage
5
- module Limiters
6
- class AssigneeMemberConditionsLimiter < MemberConditionsLimiter
7
- def member_field
8
- :assignee
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- require_relative 'member_conditions_limiter'
2
-
3
- module Gitlab
4
- module Triage
5
- module Limiters
6
- class AuthorMemberConditionsLimiter < MemberConditionsLimiter
7
- def member_field
8
- :author
9
- end
10
- end
11
- end
12
- end
13
- end