gitlab-triage 1.10.0 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +6 -25
- data/.gitlab/merge_request_templates/Release.md +2 -2
- data/Guardfile +1 -1
- data/README.md +55 -7
- data/gitlab-triage.gemspec +2 -0
- data/lib/gitlab/triage/action/comment.rb +2 -2
- data/lib/gitlab/triage/api_query_builders/date_query_param_builder.rb +1 -1
- data/lib/gitlab/triage/command_builders/base_command_builder.rb +7 -3
- data/lib/gitlab/triage/command_builders/label_command_builder.rb +17 -0
- data/lib/gitlab/triage/command_builders/text_content_builder.rb +1 -5
- data/lib/gitlab/triage/engine.rb +45 -6
- data/lib/gitlab/triage/filters/discussions_conditions_filter.rb +60 -0
- data/lib/gitlab/triage/graphql_network.rb +54 -0
- data/lib/gitlab/triage/graphql_queries/query_builder.rb +41 -0
- data/lib/gitlab/triage/graphql_queries/threads_query.rb +31 -0
- data/lib/gitlab/triage/graphql_queries/user_notes_query.rb +23 -0
- data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +69 -0
- data/lib/gitlab/triage/policies_resources/rule_resources.rb +2 -11
- data/lib/gitlab/triage/policies_resources/summary_resources.rb +2 -11
- data/lib/gitlab/triage/resource/base.rb +5 -1
- data/lib/gitlab/triage/resource/label.rb +15 -0
- data/lib/gitlab/triage/url_builders/url_builder.rb +7 -3
- data/lib/gitlab/triage/version.rb +1 -1
- data/support/.gitlab-ci.example.yml +2 -2
- metadata +37 -4
- data/lib/gitlab/triage/filters/forbidden_labels_conditions_filter.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 817c2b22f9f62ba6ec09e4441aea29dd4f7ae639bd8b49888236ff11a27e33a9
|
4
|
+
data.tar.gz: fbd6f4fff1adc6e99e271d438fe7ced46cc40db7a83c59ae985a156d9f57a414
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bab557c9169e861a96fce7b471080194943da6753541860ec3bb01d8790b43bc1c604a59d8fa3e73a0e4dd8dac735dec98746ff2e8a6c97d78d3ee7d87681fa1
|
7
|
+
data.tar.gz: 76a6ce14b74488879f14ca43bb0dcbd1b8d2f22e55d1ce7ee7b4cca5efd42715f462eac219c9f5f40de2a81060aa0c4e58e87db8dd3f1e90788a74c1c61b8f2c
|
data/.gitlab-ci.yml
CHANGED
@@ -2,7 +2,7 @@ stages:
|
|
2
2
|
- prepare
|
3
3
|
- test
|
4
4
|
- triage
|
5
|
-
-
|
5
|
+
- deploy
|
6
6
|
|
7
7
|
default:
|
8
8
|
image: ruby:2.7
|
@@ -38,6 +38,7 @@ workflow:
|
|
38
38
|
services:
|
39
39
|
- docker:${DOCKER_VERSION}-dind
|
40
40
|
variables:
|
41
|
+
DOCKER_VERSION: "19.03.0"
|
41
42
|
DOCKER_DRIVER: overlay2
|
42
43
|
DOCKER_HOST: tcp://docker:2375
|
43
44
|
DOCKER_TLS_CERTDIR: ""
|
@@ -118,7 +119,7 @@ dry-run:gitlab-triage:
|
|
118
119
|
- gitlab-triage --version
|
119
120
|
- gitlab-triage --help
|
120
121
|
- gitlab-triage --init
|
121
|
-
- gitlab-triage --dry-run --debug --token $
|
122
|
+
- gitlab-triage --dry-run --debug --token $GITLAB_API_TOKEN --source-id $CI_PROJECT_PATH
|
122
123
|
|
123
124
|
# This job requires allows to override the `CI_PROJECT_PATH` variable when triggered.
|
124
125
|
dry-run:custom:
|
@@ -127,26 +128,6 @@ dry-run:custom:
|
|
127
128
|
- when: manual
|
128
129
|
allow_failure: true
|
129
130
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
release:
|
134
|
-
stage: release
|
135
|
-
rules:
|
136
|
-
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
|
137
|
-
changes: ["lib/gitlab/triage/version.rb"]
|
138
|
-
- if: '$CI_MERGE_REQUEST_TITLE =~ /RELEASE/'
|
139
|
-
when: manual
|
140
|
-
before_script: []
|
141
|
-
script:
|
142
|
-
- version=$(ruby -r ./lib/gitlab/triage/version -e 'puts Gitlab::Triage::VERSION' | tr -d "\n")
|
143
|
-
- tag="v${version}"
|
144
|
-
- message="Version ${version}."
|
145
|
-
# TODO: Add release notes from the Release MR?
|
146
|
-
- 'curl --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" -d "tag_name=${tag}" -d "ref=${CI_COMMIT_SHA}" -d "message=${message}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/repository/tags"'
|
147
|
-
- gem build gitlab-triage.gemspec
|
148
|
-
- gem push "gitlab-triage-${version}.gem"
|
149
|
-
artifacts:
|
150
|
-
paths:
|
151
|
-
- gitlab-triage*.gem
|
152
|
-
expire_in: 30 days
|
131
|
+
include:
|
132
|
+
- project: 'gitlab-org/quality/pipeline-common'
|
133
|
+
file: '/ci/gem-release.yml'
|
@@ -7,7 +7,7 @@ with the latest commit from https://gitlab.com/gitlab-org/gitlab-triage/commits/
|
|
7
7
|
<!-- Keep the sections order but remove the empty sections -->
|
8
8
|
|
9
9
|
```markdown
|
10
|
-
### New
|
10
|
+
### New features and features updates
|
11
11
|
|
12
12
|
- !aaa <Title of the aaa MR>.
|
13
13
|
|
@@ -19,7 +19,7 @@ with the latest commit from https://gitlab.com/gitlab-org/gitlab-triage/commits/
|
|
19
19
|
|
20
20
|
- !ccc <Title of the ccc MR>.
|
21
21
|
|
22
|
-
### Other changes (
|
22
|
+
### Other changes (tooling, technical debt)
|
23
23
|
|
24
24
|
- !ddd <Title of the ddd MR>.
|
25
25
|
```
|
data/Guardfile
CHANGED
@@ -24,7 +24,7 @@
|
|
24
24
|
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
25
|
# * 'just' rspec: 'rspec'
|
26
26
|
|
27
|
-
guard :rspec, cmd: "bundle exec rspec" do
|
27
|
+
guard :rspec, cmd: "bundle exec rspec -f doc" do
|
28
28
|
require "guard/rspec/dsl"
|
29
29
|
dsl = Guard::RSpec::Dsl.new(self)
|
30
30
|
|
data/README.md
CHANGED
@@ -141,6 +141,8 @@ Available condition types:
|
|
141
141
|
- [`assignee_member` condition](#assignee-member-condition)
|
142
142
|
- [`source_branch` condition](#source-branch-condition)
|
143
143
|
- [`target_branch` condition](#target-branch-condition)
|
144
|
+
- [`weight` condition](#weight-condition)
|
145
|
+
- [`discussions` condition](#discussions-condition)
|
144
146
|
- [`ruby` condition](#ruby-condition)
|
145
147
|
|
146
148
|
##### Date condition
|
@@ -467,6 +469,44 @@ conditions:
|
|
467
469
|
target_branch: 'master'
|
468
470
|
```
|
469
471
|
|
472
|
+
##### Weight condition
|
473
|
+
|
474
|
+
Accepts a string per the [API documentation](https://docs.gitlab.com/ee/api/issues.html#list-issues).
|
475
|
+
This condition is only applicable for issues (not merge requests).
|
476
|
+
|
477
|
+
| State | Type | Value |
|
478
|
+
| --------- | ---- | ------ |
|
479
|
+
| Any weight | string | `Any` |
|
480
|
+
| No weight | string | `None` |
|
481
|
+
| Specific weight | integer | integer |
|
482
|
+
|
483
|
+
Example:
|
484
|
+
|
485
|
+
```yml
|
486
|
+
conditions:
|
487
|
+
weight: Any
|
488
|
+
```
|
489
|
+
|
490
|
+
##### Discussions condition
|
491
|
+
|
492
|
+
Accepts a hash of fields.
|
493
|
+
|
494
|
+
| Field | Type | Values | Required |
|
495
|
+
| --------- | ---- | ---- | -------- |
|
496
|
+
| `attribute` | string | `threads`, `notes` | yes |
|
497
|
+
| `condition` | string | `less_than`, `greater_than` | yes |
|
498
|
+
| `threshold` | integer | integer | yes |
|
499
|
+
|
500
|
+
Example:
|
501
|
+
|
502
|
+
```yml
|
503
|
+
conditions:
|
504
|
+
discussions:
|
505
|
+
attribute: threads
|
506
|
+
condition: greater_than
|
507
|
+
threshold: 15
|
508
|
+
```
|
509
|
+
|
470
510
|
##### Ruby condition
|
471
511
|
|
472
512
|
This condition allows users to write a Ruby expression to be evaluated for
|
@@ -552,6 +592,10 @@ Adds a number of labels to the resource.
|
|
552
592
|
|
553
593
|
Accepts an array of strings. Each element is the name of a label to add.
|
554
594
|
|
595
|
+
If any of the labels doesn't exist, the automation will stop immediately so
|
596
|
+
that if a label is renamed or deleted, you'll have to explicitly update or remove
|
597
|
+
it in your policy file.
|
598
|
+
|
555
599
|
Example:
|
556
600
|
|
557
601
|
```yml
|
@@ -567,6 +611,10 @@ Removes a number of labels from the resource.
|
|
567
611
|
|
568
612
|
Accepts an array of strings. Each element is the name of a label to remove.
|
569
613
|
|
614
|
+
If any of the labels doesn't exist, the automation will stop immediately so
|
615
|
+
that if a label is renamed or deleted, you'll have to explicitly update or remove
|
616
|
+
it in your policy file.
|
617
|
+
|
570
618
|
Example:
|
571
619
|
|
572
620
|
```yml
|
@@ -1016,22 +1064,22 @@ Usage: gitlab-triage [options]
|
|
1016
1064
|
Triaging against a specific project:
|
1017
1065
|
|
1018
1066
|
```
|
1019
|
-
gitlab-triage --dry-run --token $
|
1067
|
+
gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org/triage
|
1020
1068
|
```
|
1021
1069
|
|
1022
1070
|
Triaging against a whole group:
|
1023
1071
|
|
1024
1072
|
```
|
1025
|
-
gitlab-triage --dry-run --token $
|
1073
|
+
gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org --source groups
|
1026
1074
|
```
|
1027
1075
|
|
1028
1076
|
Triaging against an entire instance:
|
1029
1077
|
|
1030
1078
|
```
|
1031
|
-
gitlab-triage --dry-run --token $
|
1079
|
+
gitlab-triage --dry-run --token $GITLAB_API_TOKEN --all-projects
|
1032
1080
|
```
|
1033
1081
|
|
1034
|
-
> **Note:** The `--all-projects` option will process all resources for all projects visible to the specified `$
|
1082
|
+
> **Note:** The `--all-projects` option will process all resources for all projects visible to the specified `$GITLAB_API_TOKEN`
|
1035
1083
|
|
1036
1084
|
#### Running on GitLab CI pipeline
|
1037
1085
|
|
@@ -1042,7 +1090,7 @@ run:triage:triage:
|
|
1042
1090
|
stage: triage
|
1043
1091
|
script:
|
1044
1092
|
- gem install gitlab-triage
|
1045
|
-
- gitlab-triage --token $
|
1093
|
+
- gitlab-triage --token $GITLAB_API_TOKEN --source-id $CI_PROJECT_PATH
|
1046
1094
|
only:
|
1047
1095
|
- schedules
|
1048
1096
|
```
|
@@ -1056,7 +1104,7 @@ Yes, you can override the host url using the following options:
|
|
1056
1104
|
##### CLI
|
1057
1105
|
|
1058
1106
|
```
|
1059
|
-
gitlab-triage --dry-run --token $
|
1107
|
+
gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org/triage --host-url https://gitlab.host.com
|
1060
1108
|
```
|
1061
1109
|
|
1062
1110
|
##### Policy file
|
@@ -1093,7 +1141,7 @@ Gitlab::Triage::Resource::Context.include MyPlugin
|
|
1093
1141
|
And then run it with:
|
1094
1142
|
|
1095
1143
|
```shell
|
1096
|
-
gitlab-triage -r ./my_plugin.rb --token $
|
1144
|
+
gitlab-triage -r ./my_plugin.rb --token $GITLAB_API_TOKEN --source-id gitlab-org/triage
|
1097
1145
|
```
|
1098
1146
|
|
1099
1147
|
This allows you to use `has_severity_label?` in the Ruby condition:
|
data/gitlab-triage.gemspec
CHANGED
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.require_paths = ['lib']
|
21
21
|
|
22
22
|
spec.add_dependency 'activesupport', '~> 5.1'
|
23
|
+
spec.add_dependency 'globalid', '~> 0.4'
|
24
|
+
spec.add_dependency 'graphql-client', '~> 0.16'
|
23
25
|
spec.add_dependency 'httparty', '~> 0.17'
|
24
26
|
|
25
27
|
spec.add_development_dependency 'bundler'
|
@@ -41,8 +41,8 @@ module Gitlab
|
|
41
41
|
CommandBuilders::CommentCommandBuilder.new(
|
42
42
|
[
|
43
43
|
CommandBuilders::TextContentBuilder.new(policy.actions[:comment], resource: resource, network: network).build_command,
|
44
|
-
CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels]).build_command,
|
45
|
-
CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels]).build_command,
|
44
|
+
CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels], resource: resource, network: network).build_command,
|
45
|
+
CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels], resource: resource, network: network).build_command,
|
46
46
|
CommandBuilders::CcCommandBuilder.new(policy.actions[:mention]).build_command,
|
47
47
|
CommandBuilders::MoveCommandBuilder.new(policy.actions[:move]).build_command,
|
48
48
|
CommandBuilders::StatusCommandBuilder.new(policy.actions[:status]).build_command
|
@@ -2,13 +2,15 @@ module Gitlab
|
|
2
2
|
module Triage
|
3
3
|
module CommandBuilders
|
4
4
|
class BaseCommandBuilder
|
5
|
-
def initialize(items)
|
5
|
+
def initialize(items, resource: nil, network: nil)
|
6
6
|
@items = Array.wrap(items)
|
7
7
|
@items.delete('')
|
8
|
+
@resource = resource&.with_indifferent_access
|
9
|
+
@network = network
|
8
10
|
end
|
9
11
|
|
10
12
|
def build_command
|
11
|
-
if
|
13
|
+
if items.any?
|
12
14
|
[slash_command_string, content_string].compact.join(separator)
|
13
15
|
else
|
14
16
|
""
|
@@ -17,6 +19,8 @@ module Gitlab
|
|
17
19
|
|
18
20
|
private
|
19
21
|
|
22
|
+
attr_reader :items, :resource, :network
|
23
|
+
|
20
24
|
def separator
|
21
25
|
' '
|
22
26
|
end
|
@@ -26,7 +30,7 @@ module Gitlab
|
|
26
30
|
end
|
27
31
|
|
28
32
|
def content_string
|
29
|
-
|
33
|
+
items.map do |item|
|
30
34
|
format_item(item)
|
31
35
|
end.join(separator)
|
32
36
|
end
|
@@ -4,8 +4,25 @@ module Gitlab
|
|
4
4
|
module Triage
|
5
5
|
module CommandBuilders
|
6
6
|
class LabelCommandBuilder < BaseCommandBuilder
|
7
|
+
def build_command
|
8
|
+
ensure_labels_exist!
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
7
13
|
private
|
8
14
|
|
15
|
+
def ensure_labels_exist!
|
16
|
+
items.each do |label|
|
17
|
+
label_opts = { project_id: resource[:project_id], name: label }
|
18
|
+
|
19
|
+
unless Resource::Label.new(label_opts, network: network).exist?
|
20
|
+
raise Resource::Label::LabelDoesntExistError,
|
21
|
+
"Label `#{label}` doesn't exist!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
9
26
|
def slash_command_string
|
10
27
|
"/label"
|
11
28
|
end
|
@@ -34,13 +34,9 @@ module Gitlab
|
|
34
34
|
}.freeze
|
35
35
|
PLACEHOLDER_REGEX = /{{([\w\.]+)}}/.freeze
|
36
36
|
|
37
|
-
attr_reader :resource, :network
|
38
|
-
|
39
37
|
def initialize(
|
40
38
|
items, resource: nil, network: nil, redact_confidentials: true)
|
41
|
-
super(items)
|
42
|
-
@resource = resource&.with_indifferent_access
|
43
|
-
@network = network
|
39
|
+
super(items, resource: resource, network: network)
|
44
40
|
@redact_confidentials = redact_confidentials
|
45
41
|
end
|
46
42
|
|
data/lib/gitlab/triage/engine.rb
CHANGED
@@ -4,10 +4,10 @@ require 'active_support/inflector'
|
|
4
4
|
require_relative 'expand_condition'
|
5
5
|
require_relative 'filters/merge_request_date_conditions_filter'
|
6
6
|
require_relative 'filters/votes_conditions_filter'
|
7
|
-
require_relative 'filters/forbidden_labels_conditions_filter'
|
8
7
|
require_relative 'filters/no_additional_labels_conditions_filter'
|
9
8
|
require_relative 'filters/author_member_conditions_filter'
|
10
9
|
require_relative 'filters/assignee_member_conditions_filter'
|
10
|
+
require_relative 'filters/discussions_conditions_filter'
|
11
11
|
require_relative 'filters/ruby_conditions_filter'
|
12
12
|
require_relative 'limiters/date_field_limiter'
|
13
13
|
require_relative 'action'
|
@@ -20,7 +20,10 @@ 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'
|
22
22
|
require_relative 'network'
|
23
|
+
require_relative 'graphql_network'
|
23
24
|
require_relative 'network_adapters/httparty_adapter'
|
25
|
+
require_relative 'network_adapters/graphql_adapter'
|
26
|
+
require_relative 'graphql_queries/query_builder'
|
24
27
|
require_relative 'ui'
|
25
28
|
|
26
29
|
module Gitlab
|
@@ -28,7 +31,10 @@ module Gitlab
|
|
28
31
|
class Engine
|
29
32
|
attr_reader :per_page, :policies, :options
|
30
33
|
|
31
|
-
|
34
|
+
DEFAULT_NETWORK_ADAPTER = Gitlab::Triage::NetworkAdapters::HttpartyAdapter
|
35
|
+
DEFAULT_GRAPHQL_ADAPTER = Gitlab::Triage::NetworkAdapters::GraphqlAdapter
|
36
|
+
|
37
|
+
def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
|
32
38
|
options.host_url = policies.delete(:host_url) { options.host_url }
|
33
39
|
options.api_version = policies.delete(:api_version) { 'v4' }
|
34
40
|
options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?
|
@@ -37,6 +43,7 @@ module Gitlab
|
|
37
43
|
@policies = policies
|
38
44
|
@options = options
|
39
45
|
@network_adapter_class = network_adapter_class
|
46
|
+
@graphql_network_adapter_class = graphql_network_adapter_class
|
40
47
|
|
41
48
|
assert_all!
|
42
49
|
assert_project_id!
|
@@ -63,6 +70,10 @@ module Gitlab
|
|
63
70
|
@network ||= Network.new(network_adapter)
|
64
71
|
end
|
65
72
|
|
73
|
+
def graphql_network
|
74
|
+
@graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
|
75
|
+
end
|
76
|
+
|
66
77
|
private
|
67
78
|
|
68
79
|
def assert_project_id!
|
@@ -95,6 +106,10 @@ module Gitlab
|
|
95
106
|
@network_adapter ||= @network_adapter_class.new(options)
|
96
107
|
end
|
97
108
|
|
109
|
+
def graphql_network_adapter
|
110
|
+
@graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
|
111
|
+
end
|
112
|
+
|
98
113
|
def rule_conditions(rule)
|
99
114
|
rule.fetch(:conditions) { {} }
|
100
115
|
end
|
@@ -159,8 +174,11 @@ module Gitlab
|
|
159
174
|
# retrieving the resources for every rule is inefficient
|
160
175
|
# however, previous rules may affect those upcoming
|
161
176
|
resources = network.query_api(build_get_url(resource_type, conditions))
|
177
|
+
graphql_query = build_graphql_query(resource_type, conditions)
|
178
|
+
graphql_resources = graphql_network.query(graphql_query, source: options.source_id) if graphql_query.present?
|
162
179
|
# In some filters/actions we want to know which resource type it is
|
163
180
|
attach_resource_type(resources, resource_type)
|
181
|
+
decorate_resources_with_graphql_data(resources, graphql_resources)
|
164
182
|
|
165
183
|
puts "\n\n* Found #{resources.count} resources..."
|
166
184
|
print "* Filtering resources..."
|
@@ -181,6 +199,13 @@ module Gitlab
|
|
181
199
|
resources.each { |resource| resource[:type] ||= resource_type }
|
182
200
|
end
|
183
201
|
|
202
|
+
def decorate_resources_with_graphql_data(resources, graphql_resources)
|
203
|
+
return if graphql_resources.nil?
|
204
|
+
|
205
|
+
graphql_resources_by_id = graphql_resources.to_h { |resource| [resource[:id], resource] }
|
206
|
+
resources.each { |resource| resource.merge!(graphql_resources_by_id[resource[:id]].to_h) }
|
207
|
+
end
|
208
|
+
|
184
209
|
def process_action(policy)
|
185
210
|
Action.process(
|
186
211
|
policy: policy,
|
@@ -202,10 +227,6 @@ module Gitlab
|
|
202
227
|
results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate
|
203
228
|
end
|
204
229
|
|
205
|
-
if conditions[:forbidden_labels]
|
206
|
-
results << Filters::ForbiddenLabelsConditionsFilter.new(resource, conditions[:forbidden_labels]).calculate
|
207
|
-
end
|
208
|
-
|
209
230
|
if conditions[:no_additional_labels]
|
210
231
|
results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate
|
211
232
|
end
|
@@ -218,6 +239,10 @@ module Gitlab
|
|
218
239
|
results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
|
219
240
|
end
|
220
241
|
|
242
|
+
if conditions[:discussions]
|
243
|
+
results << Filters::DiscussionsConditionsFilter.new(resource, conditions[:discussions]).calculate
|
244
|
+
end
|
245
|
+
|
221
246
|
if conditions[:ruby]
|
222
247
|
results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
|
223
248
|
end
|
@@ -244,6 +269,11 @@ module Gitlab
|
|
244
269
|
|
245
270
|
condition_builders = []
|
246
271
|
condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('labels', conditions[:labels], ',') if conditions[:labels]
|
272
|
+
|
273
|
+
if conditions[:forbidden_labels]
|
274
|
+
condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('not[labels]', conditions[:forbidden_labels], ',')
|
275
|
+
end
|
276
|
+
|
247
277
|
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('state', conditions[:state]) if conditions[:state]
|
248
278
|
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('milestone', Array(conditions[:milestone])[0]) if conditions[:milestone]
|
249
279
|
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
|
@@ -253,6 +283,10 @@ module Gitlab
|
|
253
283
|
condition_builders << APIQueryBuilders::DateQueryParamBuilder.new(conditions.delete(:date))
|
254
284
|
end
|
255
285
|
|
286
|
+
if conditions[:weight] && resource_type.to_sym == :issues
|
287
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight])
|
288
|
+
end
|
289
|
+
|
256
290
|
condition_builders.each do |condition_builder|
|
257
291
|
params[condition_builder.param_name] = condition_builder.param_content
|
258
292
|
end
|
@@ -266,6 +300,11 @@ module Gitlab
|
|
266
300
|
params: params
|
267
301
|
).build
|
268
302
|
end
|
303
|
+
|
304
|
+
def build_graphql_query(resource_type, conditions)
|
305
|
+
Gitlab::Triage::GraphqlQueries::QueryBuilder
|
306
|
+
.new(options.source, resource_type, conditions)
|
307
|
+
end
|
269
308
|
end
|
270
309
|
end
|
271
310
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative 'base_conditions_filter'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Triage
|
5
|
+
module Filters
|
6
|
+
class DiscussionsConditionsFilter < BaseConditionsFilter
|
7
|
+
ATTRIBUTES = %w[notes threads].freeze
|
8
|
+
CONDITIONS = %w[greater_than less_than].freeze
|
9
|
+
|
10
|
+
def self.filter_parameters
|
11
|
+
[
|
12
|
+
{
|
13
|
+
name: :attribute,
|
14
|
+
type: String,
|
15
|
+
values: ATTRIBUTES
|
16
|
+
},
|
17
|
+
{
|
18
|
+
name: :condition,
|
19
|
+
type: String,
|
20
|
+
values: CONDITIONS
|
21
|
+
},
|
22
|
+
{
|
23
|
+
name: :threshold,
|
24
|
+
type: Numeric
|
25
|
+
}
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize_variables(condition)
|
30
|
+
@attribute = condition[:attribute].to_sym
|
31
|
+
@condition = condition[:condition].to_sym
|
32
|
+
@threshold = condition[:threshold]
|
33
|
+
end
|
34
|
+
|
35
|
+
def resource_value
|
36
|
+
if @attribute == :notes
|
37
|
+
@resource[:user_notes_count]
|
38
|
+
else
|
39
|
+
@resource.dig(:discussions, :nodes)&.count do |node|
|
40
|
+
!node&.dig(:notes, :nodes, 0, :system)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def condition_value
|
46
|
+
@threshold
|
47
|
+
end
|
48
|
+
|
49
|
+
def calculate
|
50
|
+
case @condition
|
51
|
+
when :greater_than
|
52
|
+
resource_value.to_i > condition_value
|
53
|
+
when :less_than
|
54
|
+
resource_value.to_i < condition_value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'net/protocol'
|
3
|
+
require 'globalid'
|
4
|
+
|
5
|
+
require_relative 'retryable'
|
6
|
+
require_relative 'ui'
|
7
|
+
require_relative 'errors'
|
8
|
+
|
9
|
+
module Gitlab
|
10
|
+
module Triage
|
11
|
+
class GraphqlNetwork
|
12
|
+
attr_reader :options, :adapter
|
13
|
+
|
14
|
+
def initialize(adapter)
|
15
|
+
@adapter = adapter
|
16
|
+
@options = adapter.options
|
17
|
+
end
|
18
|
+
|
19
|
+
def query(graphql_query, variables = {})
|
20
|
+
return if graphql_query.blank?
|
21
|
+
|
22
|
+
response = {}
|
23
|
+
resources = []
|
24
|
+
|
25
|
+
parsed_graphql_query = adapter.parse(graphql_query.query)
|
26
|
+
|
27
|
+
begin
|
28
|
+
print '.'
|
29
|
+
|
30
|
+
response = adapter.query(
|
31
|
+
parsed_graphql_query,
|
32
|
+
resource_path: graphql_query.resource_path,
|
33
|
+
variables: variables.merge(after: response.delete(:end_cursor))
|
34
|
+
)
|
35
|
+
|
36
|
+
resources.concat(Array.wrap(response.delete(:results)))
|
37
|
+
end while response.delete(:more_pages)
|
38
|
+
|
39
|
+
resources
|
40
|
+
.map { |resource| resource.deep_transform_keys(&:underscore) }
|
41
|
+
.map(&:with_indifferent_access)
|
42
|
+
.map { |resource| resource.merge(id: extract_id_from_global_id(resource[:id])) }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def extract_id_from_global_id(global_id)
|
48
|
+
return if global_id.blank?
|
49
|
+
|
50
|
+
GlobalID.parse(global_id).model_id.to_i
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'user_notes_query'
|
2
|
+
require_relative 'threads_query'
|
3
|
+
|
4
|
+
module Gitlab
|
5
|
+
module Triage
|
6
|
+
module GraphqlQueries
|
7
|
+
class QueryBuilder
|
8
|
+
def initialize(source_type, resource_type, conditions)
|
9
|
+
@source_type = source_type.to_s.singularize
|
10
|
+
@resource_type = resource_type
|
11
|
+
@conditions = conditions
|
12
|
+
end
|
13
|
+
|
14
|
+
def resource_path
|
15
|
+
[source_type, resource_type]
|
16
|
+
end
|
17
|
+
|
18
|
+
def query
|
19
|
+
return if query_template.nil?
|
20
|
+
|
21
|
+
format(query_template, source_type: source_type, resource_type: resource_type.to_s.camelize(:lower))
|
22
|
+
end
|
23
|
+
|
24
|
+
delegate :present?, to: :query_template
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :source_type, :resource_type, :conditions
|
29
|
+
|
30
|
+
def query_template
|
31
|
+
case conditions.dig(:discussions, :attribute).to_s
|
32
|
+
when 'notes'
|
33
|
+
UserNotesQuery
|
34
|
+
when 'threads'
|
35
|
+
ThreadsQuery
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Gitlab
|
2
|
+
module Triage
|
3
|
+
module GraphqlQueries
|
4
|
+
ThreadsQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
|
5
|
+
query($source: ID!, $after: String) {
|
6
|
+
%{source_type}(fullPath: $source) {
|
7
|
+
id
|
8
|
+
%{resource_type}(after: $after) {
|
9
|
+
pageInfo {
|
10
|
+
hasNextPage
|
11
|
+
endCursor
|
12
|
+
}
|
13
|
+
nodes {
|
14
|
+
id
|
15
|
+
discussions {
|
16
|
+
nodes {
|
17
|
+
notes {
|
18
|
+
nodes {
|
19
|
+
system
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
GRAPHQL
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gitlab
|
2
|
+
module Triage
|
3
|
+
module GraphqlQueries
|
4
|
+
UserNotesQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
|
5
|
+
query($source: ID!, $after: String) {
|
6
|
+
%{source_type}(fullPath: $source) {
|
7
|
+
id
|
8
|
+
%{resource_type}(after: $after) {
|
9
|
+
pageInfo {
|
10
|
+
hasNextPage
|
11
|
+
endCursor
|
12
|
+
}
|
13
|
+
nodes {
|
14
|
+
id
|
15
|
+
userNotesCount
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
GRAPHQL
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'graphql/client'
|
2
|
+
require 'graphql/client/http'
|
3
|
+
|
4
|
+
require_relative 'base_adapter'
|
5
|
+
require_relative '../ui'
|
6
|
+
require_relative '../errors'
|
7
|
+
|
8
|
+
module Gitlab
|
9
|
+
module Triage
|
10
|
+
module NetworkAdapters
|
11
|
+
class GraphqlAdapter < BaseAdapter
|
12
|
+
Client = GraphQL::Client
|
13
|
+
|
14
|
+
def query(graphql_query, resource_path: [], variables: {})
|
15
|
+
response = client.query(graphql_query, variables: variables, context: { token: options.token })
|
16
|
+
|
17
|
+
raise_on_error!(response)
|
18
|
+
|
19
|
+
parsed_response = parse_response(response, resource_path)
|
20
|
+
|
21
|
+
return { results: {} } if parsed_response.nil?
|
22
|
+
return { results: parsed_response.map(&:to_h) } if parsed_response.is_a?(Client::List)
|
23
|
+
return { results: parsed_response.to_h } unless parsed_response.nodes?
|
24
|
+
|
25
|
+
{
|
26
|
+
more_pages: parsed_response.page_info.has_next_page,
|
27
|
+
end_cursor: parsed_response.page_info.end_cursor,
|
28
|
+
results: parsed_response.nodes.map(&:to_h)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
delegate :parse, to: :client
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parse_response(response, resource_path)
|
37
|
+
resource_path.reduce(response.data) { |data, resource| data&.send(resource) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def raise_on_error!(response)
|
41
|
+
return if response.errors.blank?
|
42
|
+
|
43
|
+
puts Gitlab::Triage::UI.debug response.inspect if options.debug
|
44
|
+
|
45
|
+
raise "There was an error: #{response.errors.messages.to_json}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def http_client
|
49
|
+
Client::HTTP.new("#{options.host_url}/api/graphql") do
|
50
|
+
def headers(context) # rubocop:disable Lint/NestedMethodDefinition
|
51
|
+
{
|
52
|
+
'Content-type' => 'application/json',
|
53
|
+
'PRIVATE-TOKEN' => context[:token]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def schema
|
60
|
+
@schema ||= Client.load_schema(http_client)
|
61
|
+
end
|
62
|
+
|
63
|
+
def client
|
64
|
+
@client ||= Client.new(schema: schema, execute: http_client).tap { |client| client.allow_dynamic_queries = true }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -1,20 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'delegate'
|
4
4
|
|
5
5
|
module Gitlab
|
6
6
|
module Triage
|
7
7
|
module PoliciesResources
|
8
|
-
|
9
|
-
include Enumerable
|
10
|
-
extend Forwardable
|
11
|
-
|
12
|
-
def initialize(new_resources)
|
13
|
-
@resources = new_resources
|
14
|
-
end
|
15
|
-
|
16
|
-
def_delegator :@resources, :each
|
17
|
-
end
|
8
|
+
RuleResources = Class.new(SimpleDelegator)
|
18
9
|
end
|
19
10
|
end
|
20
11
|
end
|
@@ -1,20 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'delegate'
|
4
4
|
|
5
5
|
module Gitlab
|
6
6
|
module Triage
|
7
7
|
module PoliciesResources
|
8
|
-
|
9
|
-
include Enumerable
|
10
|
-
extend Forwardable
|
11
|
-
|
12
|
-
def initialize(new_rule_to_resources)
|
13
|
-
@rule_to_resources = new_rule_to_resources
|
14
|
-
end
|
15
|
-
|
16
|
-
def_delegator :@rule_to_resources, :each
|
17
|
-
end
|
8
|
+
SummaryResources = Class.new(SimpleDelegator)
|
18
9
|
end
|
19
10
|
end
|
20
11
|
end
|
@@ -58,11 +58,15 @@ module Gitlab
|
|
58
58
|
build_url(params: params)
|
59
59
|
end
|
60
60
|
|
61
|
+
def resource_id
|
62
|
+
resource[:iid]
|
63
|
+
end
|
64
|
+
|
61
65
|
def resource_url(params: {}, sub_resource_type: nil)
|
62
66
|
build_url(
|
63
67
|
params: params,
|
64
68
|
options: {
|
65
|
-
resource_id:
|
69
|
+
resource_id: resource_id,
|
66
70
|
sub_resource_type: sub_resource_type
|
67
71
|
}
|
68
72
|
)
|
@@ -8,6 +8,8 @@ module Gitlab
|
|
8
8
|
module Triage
|
9
9
|
module Resource
|
10
10
|
class Label < Base
|
11
|
+
LabelDoesntExistError = Class.new(StandardError)
|
12
|
+
|
11
13
|
FIELDS = %i[
|
12
14
|
id
|
13
15
|
project_id
|
@@ -35,6 +37,19 @@ module Gitlab
|
|
35
37
|
Time.parse(value) if value
|
36
38
|
end
|
37
39
|
end
|
40
|
+
|
41
|
+
def exist?
|
42
|
+
label = network.query_api_cached(resource_url).first
|
43
|
+
return false unless label
|
44
|
+
|
45
|
+
label[:name] == name
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def resource_id
|
51
|
+
name
|
52
|
+
end
|
38
53
|
end
|
39
54
|
end
|
40
55
|
end
|
@@ -17,7 +17,7 @@ module Gitlab
|
|
17
17
|
|
18
18
|
def build
|
19
19
|
url = base_url
|
20
|
-
url << "/#{@resource_id}" if @resource_id
|
20
|
+
url << "/#{percent_encode(@resource_id.to_s)}" if @resource_id
|
21
21
|
url << "/#{@sub_resource_type}" if @sub_resource_type
|
22
22
|
url << params_string if @params
|
23
23
|
url
|
@@ -31,16 +31,20 @@ module Gitlab
|
|
31
31
|
|
32
32
|
def base_url
|
33
33
|
url = host_with_api_url
|
34
|
-
url << "/#{@source}/#{
|
34
|
+
url << "/#{@source}/#{percent_encode(@source_id.to_s)}" unless @all
|
35
35
|
url << "/#{@resource_type}" if @resource_type
|
36
36
|
url
|
37
37
|
end
|
38
38
|
|
39
39
|
def params_string
|
40
40
|
"?" << @params.map do |k, v|
|
41
|
-
"#{k}=#{v}"
|
41
|
+
"#{percent_encode(k.to_s)}=#{percent_encode(v.to_s)}"
|
42
42
|
end.join("&")
|
43
43
|
end
|
44
|
+
|
45
|
+
def percent_encode(str)
|
46
|
+
CGI.escape(str).gsub('+', '%20')
|
47
|
+
end
|
44
48
|
end
|
45
49
|
end
|
46
50
|
end
|
@@ -8,7 +8,7 @@ dry-run:triage:
|
|
8
8
|
script:
|
9
9
|
- gem install gitlab-triage
|
10
10
|
- gitlab-triage --help
|
11
|
-
- gitlab-triage --dry-run --token $
|
11
|
+
- gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source projects --source-id $CI_PROJECT_PATH
|
12
12
|
when: manual
|
13
13
|
except:
|
14
14
|
- schedules
|
@@ -17,6 +17,6 @@ run:triage:
|
|
17
17
|
stage: triage
|
18
18
|
script:
|
19
19
|
- gem install gitlab-triage
|
20
|
-
- gitlab-triage --token $
|
20
|
+
- gitlab-triage --token $GITLAB_API_TOKEN --source projects --source-id $CI_PROJECT_PATH
|
21
21
|
only:
|
22
22
|
- schedules
|
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: 1.
|
4
|
+
version: 1.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: globalid
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: graphql-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.16'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.16'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: httparty
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -158,17 +186,22 @@ files:
|
|
158
186
|
- lib/gitlab/triage/filters/assignee_member_conditions_filter.rb
|
159
187
|
- lib/gitlab/triage/filters/author_member_conditions_filter.rb
|
160
188
|
- lib/gitlab/triage/filters/base_conditions_filter.rb
|
161
|
-
- lib/gitlab/triage/filters/
|
189
|
+
- lib/gitlab/triage/filters/discussions_conditions_filter.rb
|
162
190
|
- lib/gitlab/triage/filters/member_conditions_filter.rb
|
163
191
|
- lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb
|
164
192
|
- lib/gitlab/triage/filters/name_conditions_filter.rb
|
165
193
|
- lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb
|
166
194
|
- lib/gitlab/triage/filters/ruby_conditions_filter.rb
|
167
195
|
- lib/gitlab/triage/filters/votes_conditions_filter.rb
|
196
|
+
- lib/gitlab/triage/graphql_network.rb
|
197
|
+
- lib/gitlab/triage/graphql_queries/query_builder.rb
|
198
|
+
- lib/gitlab/triage/graphql_queries/threads_query.rb
|
199
|
+
- lib/gitlab/triage/graphql_queries/user_notes_query.rb
|
168
200
|
- lib/gitlab/triage/limiters/base_limiter.rb
|
169
201
|
- lib/gitlab/triage/limiters/date_field_limiter.rb
|
170
202
|
- lib/gitlab/triage/network.rb
|
171
203
|
- lib/gitlab/triage/network_adapters/base_adapter.rb
|
204
|
+
- lib/gitlab/triage/network_adapters/graphql_adapter.rb
|
172
205
|
- lib/gitlab/triage/network_adapters/httparty_adapter.rb
|
173
206
|
- lib/gitlab/triage/network_adapters/test_adapter.rb
|
174
207
|
- lib/gitlab/triage/option_parser.rb
|
@@ -214,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
214
247
|
- !ruby/object:Gem::Version
|
215
248
|
version: '0'
|
216
249
|
requirements: []
|
217
|
-
rubygems_version: 3.1.
|
250
|
+
rubygems_version: 3.1.4
|
218
251
|
signing_key:
|
219
252
|
specification_version: 4
|
220
253
|
summary: GitLab triage automation project.
|
@@ -1,32 +0,0 @@
|
|
1
|
-
require_relative 'base_conditions_filter'
|
2
|
-
|
3
|
-
module Gitlab
|
4
|
-
module Triage
|
5
|
-
module Filters
|
6
|
-
class ForbiddenLabelsConditionsFilter < BaseConditionsFilter
|
7
|
-
def validate_condition(condition)
|
8
|
-
raise ArgumentError, 'condition must be an array containing forbidden label values' unless condition.is_a?(Array)
|
9
|
-
end
|
10
|
-
|
11
|
-
def initialize_variables(forbidden_labels)
|
12
|
-
@attribute = :labels
|
13
|
-
@forbidden_labels = forbidden_labels
|
14
|
-
end
|
15
|
-
|
16
|
-
def resource_value
|
17
|
-
@resource[@attribute]
|
18
|
-
end
|
19
|
-
|
20
|
-
def calculate
|
21
|
-
label_intersection.empty?
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def label_intersection
|
27
|
-
resource_value & @forbidden_labels
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|