gitlab-triage 0.6.0 → 0.7.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 (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