gitlab-dangerfiles 0.2.0 → 0.6.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 679d12adff8fd532552d644bba3f1ae62395077310ffcc7aa62181b477f5453d
4
- data.tar.gz: f6460b3c349cf27d1a65246f41744cae15c95c0c949811aa1b972abe654b3e90
3
+ metadata.gz: 1376c48a26c7b3a10cc3e649d1cf28f49fdf58fd7b317713b9ac9a0f3771e5a5
4
+ data.tar.gz: 9ecd1d0db0f583740a73f96f6ffd14905f7a66157e427c6a11410a7bc7c70e55
5
5
  SHA512:
6
- metadata.gz: 8ebcb0cd08d49021346a0b74adb50b5ff10a8fe64227e86f960a72e6cc88260f0249927088bae3e949ddbf9a295c65d2d4c005d8d4b0ee157a907f700b72ce46
7
- data.tar.gz: e747d6b68ca54ee597e008332a5bfc7d4fa01a437e44af42fc68d5ab28aaf674b2e76995ab792559016fa381c0cd1e08b58071bb5c3d4b66f37e8c7891bbaa77
6
+ metadata.gz: e1684cb0a7b5367b37f099cad0dca796cdb5cf01fc71f36e4896268b4abc8c0ee59ed122459261f47ec1b873da8aa2355644c6074b92285ddd1b3f4215bbba86
7
+ data.tar.gz: 8e8644fa64222def924524b8545397615f7335198e88d35f47fc07c450793879c0c710c380b496eb0c80a6bf856c855ec4bb58266670aff6a526ff3b9c84bc5a
data/lib/danger/helper.rb CHANGED
@@ -4,21 +4,70 @@ require "net/http"
4
4
  require "json"
5
5
  require "danger"
6
6
  require_relative "../gitlab/dangerfiles/teammate"
7
+ require_relative "../gitlab/dangerfiles/title_linting"
7
8
 
8
9
  module Danger
9
10
  # Common helper functions for our danger scripts.
10
11
  class Helper < Danger::Plugin
11
12
  RELEASE_TOOLS_BOT = "gitlab-release-tools-bot"
13
+ DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
12
14
  CATEGORY_LABELS = {
13
15
  docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
14
16
  none: "",
15
17
  qa: "~QA",
16
18
  test: "~test ~Quality for `spec/features/*`",
17
19
  engineering_productivity: '~"Engineering Productivity" for CI, Danger',
20
+ ci_template: '~"ci::templates"',
18
21
  }.freeze
19
22
 
20
23
  HTTPError = Class.new(StandardError)
21
24
 
25
+ Change = Struct.new(:file, :change_type, :category)
26
+
27
+ class Changes < ::SimpleDelegator
28
+ def added
29
+ select_by_change_type(:added)
30
+ end
31
+
32
+ def modified
33
+ select_by_change_type(:modified)
34
+ end
35
+
36
+ def deleted
37
+ select_by_change_type(:deleted)
38
+ end
39
+
40
+ def renamed_before
41
+ select_by_change_type(:renamed_before)
42
+ end
43
+
44
+ def renamed_after
45
+ select_by_change_type(:renamed_after)
46
+ end
47
+
48
+ def has_category?(category)
49
+ any? { |change| change.category == category }
50
+ end
51
+
52
+ def by_category(category)
53
+ Changes.new(select { |change| change.category == category })
54
+ end
55
+
56
+ def categories
57
+ map(&:category).uniq
58
+ end
59
+
60
+ def files
61
+ map(&:file)
62
+ end
63
+
64
+ private
65
+
66
+ def select_by_change_type(change_type)
67
+ Changes.new(select { |change| change.change_type == change_type })
68
+ end
69
+ end
70
+
22
71
  def gitlab_helper
23
72
  # Unfortunately the following does not work:
24
73
  # - respond_to?(:gitlab)
@@ -103,14 +152,41 @@ module Danger
103
152
  end
104
153
  end
105
154
 
106
- # @return [Hash<String,Array<String>>]
155
+ # @return [Hash<Symbol,Array<String>>]
107
156
  def changes_by_category(categories)
108
157
  all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
109
158
  categories_for_file(file, categories).each { |category| hash[category] << file }
110
159
  end
111
160
  end
112
161
 
113
- # Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`.
162
+ # @return [Changes]
163
+ def changes(categories)
164
+ Changes.new([]).tap do |changes|
165
+ git.added_files.each do |file|
166
+ categories_for_file(file, categories).each { |category| changes << Change.new(file, :added, category) }
167
+ end
168
+
169
+ git.modified_files.each do |file|
170
+ categories_for_file(file, categories).each { |category| changes << Change.new(file, :modified, category) }
171
+ end
172
+
173
+ git.deleted_files.each do |file|
174
+ categories_for_file(file, categories).each { |category| changes << Change.new(file, :deleted, category) }
175
+ end
176
+
177
+ git.renamed_files.map { |x| x[:before] }.each do |file|
178
+ categories_for_file(file, categories).each { |category| changes << Change.new(file, :renamed_before, category) }
179
+ end
180
+
181
+ git.renamed_files.map { |x| x[:after] }.each do |file|
182
+ categories_for_file(file, categories).each { |category| changes << Change.new(file, :renamed_after, category) }
183
+ end
184
+ end
185
+ end
186
+
187
+ # Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`
188
+ # using filename regex and specific change regex if given.
189
+ #
114
190
  # @return Array<Symbol>
115
191
  def categories_for_file(file, categories)
116
192
  _, categories = categories.find do |key, _|
@@ -137,43 +213,64 @@ module Danger
137
213
  usernames.map { |u| Gitlab::Dangerfiles::Teammate.new("username" => u) }
138
214
  end
139
215
 
140
- def missing_database_labels(current_mr_labels)
141
- labels = if has_database_scoped_labels?(current_mr_labels)
142
- ["database"]
143
- else
144
- ["database", "database::review pending"]
145
- end
216
+ def mr_iid
217
+ return "" unless gitlab_helper
146
218
 
147
- labels - current_mr_labels
219
+ gitlab_helper.mr_json["iid"]
148
220
  end
149
221
 
150
- def sanitize_mr_title(title)
151
- title.gsub(/^WIP: */, "").gsub(/`/, '\\\`')
222
+ def mr_title
223
+ return "" unless gitlab_helper
224
+
225
+ gitlab_helper.mr_json["title"]
152
226
  end
153
227
 
154
- def security_mr?
155
- return false unless gitlab_helper
228
+ def mr_web_url
229
+ return "" unless gitlab_helper
230
+
231
+ gitlab_helper.mr_json["web_url"]
232
+ end
233
+
234
+ def mr_labels
235
+ return [] unless gitlab_helper
236
+
237
+ gitlab_helper.mr_labels
238
+ end
239
+
240
+ def mr_target_branch
241
+ return "" unless gitlab_helper
242
+
243
+ gitlab_helper.mr_json["target_branch"]
244
+ end
245
+
246
+ def draft_mr?
247
+ Gitlab::Dangerfiles::TitleLinting.has_draft_flag?(mr_title)
248
+ end
156
249
 
157
- gitlab_helper.mr_json["web_url"].include?("/gitlab-org/security/")
250
+ def security_mr?
251
+ mr_web_url.include?("/gitlab-org/security/")
158
252
  end
159
253
 
160
254
  def cherry_pick_mr?
161
- return false unless gitlab_helper
255
+ Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
256
+ end
162
257
 
163
- /cherry[\s-]*pick/i.match?(gitlab_helper.mr_json["title"])
258
+ def run_all_rspec_mr?
259
+ Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
164
260
  end
165
261
 
166
- def stable_branch?
167
- return false unless gitlab_helper
262
+ def run_as_if_foss_mr?
263
+ Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
264
+ end
168
265
 
169
- /\A\d+-\d+-stable-ee/i.match?(gitlab_helper.mr_json["target_branch"])
266
+ def stable_branch?
267
+ /\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
170
268
  end
171
269
 
172
270
  def mr_has_labels?(*labels)
173
- return false unless gitlab_helper
174
-
175
271
  labels = labels.flatten.uniq
176
- (labels & gitlab_helper.mr_labels) == labels
272
+
273
+ (labels & mr_labels) == labels
177
274
  end
178
275
 
179
276
  def labels_list(labels, sep: ", ")
@@ -190,10 +287,16 @@ module Danger
190
287
  all_changed_files.grep(regex)
191
288
  end
192
289
 
193
- private
194
-
195
290
  def has_database_scoped_labels?(current_mr_labels)
196
291
  current_mr_labels.any? { |label| label.start_with?("database::") }
197
292
  end
293
+
294
+ def has_ci_changes?
295
+ changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
296
+ end
297
+
298
+ def group_label(labels)
299
+ labels.find { |label| label.start_with?("group::") }
300
+ end
198
301
  end
199
302
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../gitlab/dangerfiles/teammate"
4
+ require_relative "../gitlab/dangerfiles/weightage/maintainers"
5
+ require_relative "../gitlab/dangerfiles/weightage/reviewers"
4
6
 
5
7
  module Danger
6
8
  # Common helper functions for our danger scripts. See Danger::Helper
@@ -24,7 +26,7 @@ module Danger
24
26
  #
25
27
  # @return [Array<Spin>]
26
28
  def spin(project, categories, timezone_experiment: false)
27
- spins = categories.map do |category|
29
+ spins = categories.sort.map do |category|
28
30
  including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)
29
31
 
30
32
  spin_for_category(project, category, timezone_experiment: including_timezone)
@@ -37,7 +39,7 @@ module Danger
37
39
  case spin.category
38
40
  when :qa
39
41
  # MR includes QA changes, but also other changes, and author isn't an SET
40
- if categories.size > 1 && !team_mr_author&.reviewer?(project, spin.category, [])
42
+ if categories.size > 1 && !team_mr_author&.any_capability?(project, spin.category)
41
43
  spin.optional_role = :maintainer
42
44
  end
43
45
  when :test
@@ -52,6 +54,11 @@ module Danger
52
54
  # Fetch an already picked backend maintainer, or pick one otherwise
53
55
  spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
54
56
  end
57
+ when :ci_template
58
+ if spin.maintainer.nil?
59
+ # Fetch an already picked backend maintainer, or pick one otherwise
60
+ spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
61
+ end
55
62
  end
56
63
  end
57
64
 
@@ -146,13 +153,13 @@ module Danger
146
153
  spin_role_for_category(team, role, project, category)
147
154
  end
148
155
 
149
- # TODO: take CODEOWNERS into account?
150
- # https://gitlab.com/gitlab-org/gitlab/issues/26723
151
-
152
- # Make traintainers have triple the chance to be picked as a reviewer
153
156
  random = new_random(mr_source_branch)
154
- reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
155
- maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
157
+
158
+ weighted_reviewers = Gitlab::Dangerfiles::Weightage::Reviewers.new(reviewers, traintainers).execute
159
+ weighted_maintainers = Gitlab::Dangerfiles::Weightage::Maintainers.new(maintainers).execute
160
+
161
+ reviewer = spin_for_person(weighted_reviewers, random: random, timezone_experiment: timezone_experiment)
162
+ maintainer = spin_for_person(weighted_maintainers, random: random, timezone_experiment: timezone_experiment)
156
163
 
157
164
  Spin.new(category, reviewer, maintainer, false, timezone_experiment)
158
165
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "title_linting"
4
+
5
+ module Gitlab
6
+ module Dangerfiles
7
+ class BaseLinter
8
+ MIN_SUBJECT_WORDS_COUNT = 3
9
+ MAX_LINE_LENGTH = 72
10
+
11
+ attr_reader :commit, :problems
12
+
13
+ def self.problems_mapping
14
+ {
15
+ subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
16
+ subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
17
+ subject_starts_with_lowercase: "The %s must start with a capital letter",
18
+ subject_ends_with_a_period: "The %s must not end with a period",
19
+ }
20
+ end
21
+
22
+ def self.subject_description
23
+ "commit subject"
24
+ end
25
+
26
+ def initialize(commit)
27
+ @commit = commit
28
+ @problems = {}
29
+ end
30
+
31
+ def failed?
32
+ problems.any?
33
+ end
34
+
35
+ def add_problem(problem_key, *args)
36
+ @problems[problem_key] = sprintf(self.class.problems_mapping[problem_key], *args)
37
+ end
38
+
39
+ def lint_subject
40
+ if subject_too_short?
41
+ add_problem(:subject_too_short, self.class.subject_description)
42
+ end
43
+
44
+ if subject_too_long?
45
+ add_problem(:subject_too_long, self.class.subject_description)
46
+ end
47
+
48
+ if subject_starts_with_lowercase?
49
+ add_problem(:subject_starts_with_lowercase, self.class.subject_description)
50
+ end
51
+
52
+ if subject_ends_with_a_period?
53
+ add_problem(:subject_ends_with_a_period, self.class.subject_description)
54
+ end
55
+
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ def subject
62
+ TitleLinting.remove_draft_flag(message_parts[0])
63
+ end
64
+
65
+ def subject_too_short?
66
+ subject.split(" ").length < MIN_SUBJECT_WORDS_COUNT
67
+ end
68
+
69
+ def subject_too_long?
70
+ line_too_long?(subject)
71
+ end
72
+
73
+ def line_too_long?(line)
74
+ line.length > MAX_LINE_LENGTH
75
+ end
76
+
77
+ def subject_starts_with_lowercase?
78
+ return false if ("A".."Z").cover?(subject[0])
79
+
80
+ first_char = subject.sub(/\A(\[.+\]|\w+:)\s/, "")[0]
81
+ first_char_downcased = first_char.downcase
82
+ return true unless ("a".."z").cover?(first_char_downcased)
83
+
84
+ first_char.downcase == first_char
85
+ end
86
+
87
+ def subject_ends_with_a_period?
88
+ subject.end_with?(".")
89
+ end
90
+
91
+ def message_parts
92
+ @message_parts ||= commit.message.split("\n", 3)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,40 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- emoji_checker_path = File.expand_path("emoji_checker", __dir__)
4
- defined?(Rails) ? require_dependency(emoji_checker_path) : require_relative(emoji_checker_path)
3
+ require_relative "base_linter"
4
+ require_relative "emoji_checker"
5
5
 
6
6
  module Gitlab
7
7
  module Dangerfiles
8
- class CommitLinter
9
- MIN_SUBJECT_WORDS_COUNT = 3
10
- MAX_LINE_LENGTH = 72
11
- MAX_CHANGED_FILES_IN_COMMIT = 3
8
+ class CommitLinter < BaseLinter
12
9
  MAX_CHANGED_LINES_IN_COMMIT = 30
13
- SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(#|!|&|%)\d+\b}.freeze
14
- DEFAULT_SUBJECT_DESCRIPTION = "commit subject"
15
- WIP_PREFIX = "WIP: "
16
- PROBLEMS = {
17
- subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
18
- subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
19
- subject_starts_with_lowercase: "The %s must start with a capital letter",
20
- subject_ends_with_a_period: "The %s must not end with a period",
21
- separator_missing: "The commit subject and body must be separated by a blank line",
22
- details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
23
- "at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
24
- details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
25
- message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
26
- "to the commit message, and are displayed as plain text outside of GitLab",
27
- message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
28
- "message, and may not be displayed properly everywhere",
29
- message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
30
- "`!123`), as short references are displayed as plain text outside of GitLab",
31
- }.freeze
32
-
33
- attr_reader :commit, :problems
10
+ SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&|%)\d+(?<!`)}.freeze
11
+
12
+ def self.problems_mapping
13
+ super.merge(
14
+ {
15
+ separator_missing: "The commit subject and body must be separated by a blank line",
16
+ details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
17
+ "at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
18
+ details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
19
+ message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
20
+ "to the commit message, and are displayed as plain text outside of GitLab",
21
+ message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
22
+ "message, and may not be displayed properly everywhere",
23
+ message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
24
+ "`!123`), as short references are displayed as plain text outside of GitLab",
25
+ }
26
+ )
27
+ end
34
28
 
35
29
  def initialize(commit)
36
- @commit = commit
37
- @problems = {}
30
+ super
31
+
38
32
  @linted = false
39
33
  end
40
34
 
@@ -58,19 +52,11 @@ module Gitlab
58
52
  !details.nil? && !details.empty?
59
53
  end
60
54
 
61
- def failed?
62
- problems.any?
63
- end
64
-
65
- def add_problem(problem_key, *args)
66
- @problems[problem_key] = sprintf(PROBLEMS[problem_key], *args)
67
- end
68
-
69
- def lint(subject_description = "commit subject")
55
+ def lint
70
56
  return self if @linted
71
57
 
72
58
  @linted = true
73
- lint_subject(subject_description)
59
+ lint_subject
74
60
  lint_separator
75
61
  lint_details
76
62
  lint_message
@@ -78,26 +64,6 @@ module Gitlab
78
64
  self
79
65
  end
80
66
 
81
- def lint_subject(subject_description)
82
- if subject_too_short?
83
- add_problem(:subject_too_short, subject_description)
84
- end
85
-
86
- if subject_too_long?
87
- add_problem(:subject_too_long, subject_description)
88
- end
89
-
90
- if subject_starts_with_lowercase?
91
- add_problem(:subject_starts_with_lowercase, subject_description)
92
- end
93
-
94
- if subject_ends_with_a_period?
95
- add_problem(:subject_ends_with_a_period, subject_description)
96
- end
97
-
98
- self
99
- end
100
-
101
67
  private
102
68
 
103
69
  def lint_separator
@@ -114,15 +80,11 @@ module Gitlab
114
80
  end
115
81
 
116
82
  details&.each_line do |line|
117
- line = line.strip
118
-
119
- next unless line_too_long?(line)
120
-
121
- url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length } # rubocop:disable CodeReuse/ActiveRecord
83
+ line_without_urls = line.strip.gsub(%r{https?://\S+}, "")
122
84
 
123
85
  # If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but
124
86
  # only if the line _without_ the URL does not exceed this limit.
125
- next unless line_too_long?(line.length - url_size)
87
+ next unless line_too_long?(line_without_urls)
126
88
 
127
89
  add_problem(:details_line_too_long)
128
90
  break
@@ -159,10 +121,6 @@ module Gitlab
159
121
  files_changed > MAX_CHANGED_FILES_IN_COMMIT && lines_changed > MAX_CHANGED_LINES_IN_COMMIT
160
122
  end
161
123
 
162
- def subject
163
- message_parts[0].delete_prefix(WIP_PREFIX)
164
- end
165
-
166
124
  def separator
167
125
  message_parts[1]
168
126
  end
@@ -171,37 +129,6 @@ module Gitlab
171
129
  message_parts[2]&.gsub(/^Signed-off-by.*$/, "")
172
130
  end
173
131
 
174
- def line_too_long?(line)
175
- case line
176
- when String
177
- line.length > MAX_LINE_LENGTH
178
- when Integer
179
- line > MAX_LINE_LENGTH
180
- else
181
- raise ArgumentError, "The line argument (#{line}) should be a String or an Integer! #{line.class} given."
182
- end
183
- end
184
-
185
- def subject_too_short?
186
- subject.split(" ").length < MIN_SUBJECT_WORDS_COUNT
187
- end
188
-
189
- def subject_too_long?
190
- line_too_long?(subject)
191
- end
192
-
193
- def subject_starts_with_lowercase?
194
- first_char = subject.sub(/\A\[.+\]\s/, "")[0]
195
- first_char_downcased = first_char.downcase
196
- return true unless ("a".."z").cover?(first_char_downcased)
197
-
198
- first_char.downcase == first_char
199
- end
200
-
201
- def subject_ends_with_a_period?
202
- subject.end_with?(".")
203
- end
204
-
205
132
  def message_contains_text_emoji?
206
133
  emoji_checker.includes_text_emoji?(commit.message)
207
134
  end
@@ -217,10 +144,6 @@ module Gitlab
217
144
  def emoji_checker
218
145
  @emoji_checker ||= Gitlab::Dangerfiles::EmojiChecker.new
219
146
  end
220
-
221
- def message_parts
222
- @message_parts ||= commit.message.split("\n", 3)
223
- end
224
147
  end
225
148
  end
226
149
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_linter"
4
+
5
+ module Gitlab
6
+ module Dangerfiles
7
+ class MergeRequestLinter < BaseLinter
8
+ alias_method :lint, :lint_subject
9
+
10
+ def self.subject_description
11
+ "merge request title"
12
+ end
13
+
14
+ def self.mr_run_options_regex
15
+ [
16
+ "RUN AS-IF-FOSS",
17
+ "UPDATE CACHE",
18
+ "RUN ALL RSPEC",
19
+ "SKIP RSPEC FAIL-FAST",
20
+ ].join("|")
21
+ end
22
+
23
+ private
24
+
25
+ def subject
26
+ super.gsub(/\[?(#{self.class.mr_run_options_regex})\]?/, "").strip
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,27 +3,42 @@
3
3
  module Gitlab
4
4
  module Dangerfiles
5
5
  class Teammate
6
- attr_reader :username, :name, :role, :projects, :available, :tz_offset_hours
6
+ attr_reader :options, :username, :name, :role, :projects, :available, :hungry, :reduced_capacity, :tz_offset_hours
7
7
 
8
8
  # The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb
9
9
  def initialize(options = {})
10
+ @options = options
10
11
  @username = options["username"]
11
12
  @name = options["name"]
12
13
  @markdown_name = options["markdown_name"]
13
14
  @role = options["role"]
14
15
  @projects = options["projects"]
15
16
  @available = options["available"]
17
+ @hungry = options["hungry"]
18
+ @reduced_capacity = options["reduced_capacity"]
16
19
  @tz_offset_hours = options["tz_offset_hours"]
17
20
  end
18
21
 
22
+ def to_h
23
+ options
24
+ end
25
+
26
+ def ==(other)
27
+ return false unless other.respond_to?(:username)
28
+
29
+ other.username == username
30
+ end
31
+
19
32
  def in_project?(name)
20
33
  projects&.has_key?(name)
21
34
  end
22
35
 
23
- # Traintainers also count as reviewers
36
+ def any_capability?(project, category)
37
+ capabilities(project).any? { |capability| capability.end_with?(category.to_s) }
38
+ end
39
+
24
40
  def reviewer?(project, category, labels)
25
- has_capability?(project, category, :reviewer, labels) ||
26
- traintainer?(project, category, labels)
41
+ has_capability?(project, category, :reviewer, labels)
27
42
  end
28
43
 
29
44
  def traintainer?(project, category, labels)
@@ -34,9 +49,7 @@ module Gitlab
34
49
  has_capability?(project, category, :maintainer, labels)
35
50
  end
36
51
 
37
- def markdown_name(timezone_experiment: false, author: nil)
38
- return @markdown_name unless timezone_experiment
39
-
52
+ def markdown_name(author: nil)
40
53
  "#{@markdown_name} (#{utc_offset_text(author)})"
41
54
  end
42
55
 
@@ -68,9 +81,9 @@ module Gitlab
68
81
 
69
82
  def offset_diff_compared_to_author(author)
70
83
  diff = floored_offset_hours - author.floored_offset_hours
71
- return "same timezone as `@#{author.username}`" if diff.zero?
84
+ return "same timezone as `@#{author.username}`" if diff == 0
72
85
 
73
- ahead_or_behind = diff < 0 ? "behind" : "ahead"
86
+ ahead_or_behind = diff < 0 ? "behind" : "ahead of"
74
87
  pluralized_hours = pluralize(diff.abs, "hour", "hours")
75
88
 
76
89
  "#{pluralized_hours} #{ahead_or_behind} `@#{author.username}`"
@@ -85,6 +98,7 @@ module Gitlab
85
98
  when :engineering_productivity
86
99
  return false unless role[/Engineering Productivity/]
87
100
  return true if kind == :reviewer
101
+ return true if capabilities(project).include?("#{kind} engineering_productivity")
88
102
 
89
103
  capabilities(project).include?("#{kind} backend")
90
104
  else
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Dangerfiles
5
+ module TitleLinting
6
+ DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
7
+ CHERRY_PICK_REGEX = /cherry[\s-]*pick/i.freeze
8
+ RUN_ALL_RSPEC_REGEX = /RUN ALL RSPEC/i.freeze
9
+ RUN_AS_IF_FOSS_REGEX = /RUN AS-IF-FOSS/i.freeze
10
+
11
+ module_function
12
+
13
+ def sanitize_mr_title(title)
14
+ remove_draft_flag(title).gsub(/`/, '\\\`')
15
+ end
16
+
17
+ def remove_draft_flag(title)
18
+ title.gsub(DRAFT_REGEX, "")
19
+ end
20
+
21
+ def has_draft_flag?(title)
22
+ DRAFT_REGEX.match?(title)
23
+ end
24
+
25
+ def has_cherry_pick_flag?(title)
26
+ CHERRY_PICK_REGEX.match?(title)
27
+ end
28
+
29
+ def has_run_all_rspec_flag?(title)
30
+ RUN_ALL_RSPEC_REGEX.match?(title)
31
+ end
32
+
33
+ def has_run_as_if_foss_flag?(title)
34
+ RUN_AS_IF_FOSS_REGEX.match?(title)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  module Gitlab
2
2
  module Dangerfiles
3
- VERSION = "0.2.0"
3
+ VERSION = "0.6.1"
4
4
  end
5
5
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Dangerfiles
5
+ module Weightage
6
+ CAPACITY_MULTIPLIER = 2 # change this number to change what it means to be a reduced capacity reviewer 1/this number
7
+ BASE_REVIEWER_WEIGHT = 1
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../weightage"
4
+
5
+ module Gitlab
6
+ module Dangerfiles
7
+ module Weightage
8
+ class Maintainers
9
+ def initialize(maintainers)
10
+ @maintainers = maintainers
11
+ end
12
+
13
+ def execute
14
+ maintainers.each_with_object([]) do |maintainer, weighted_maintainers|
15
+ add_weighted_reviewer(weighted_maintainers, maintainer, Gitlab::Dangerfiles::Weightage::BASE_REVIEWER_WEIGHT)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :maintainers
22
+
23
+ def add_weighted_reviewer(reviewers, reviewer, weight)
24
+ if reviewer.reduced_capacity
25
+ reviewers.fill(reviewer, reviewers.size, weight)
26
+ else
27
+ reviewers.fill(reviewer, reviewers.size, weight * Gitlab::Dangerfiles::Weightage::CAPACITY_MULTIPLIER)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../weightage"
4
+
5
+ module Gitlab
6
+ module Dangerfiles
7
+ module Weightage
8
+ # Weights after (current multiplier of 2)
9
+ #
10
+ # +------------------------------+--------------------------------+
11
+ # | reviewer type | weight(times in reviewer pool) |
12
+ # +------------------------------+--------------------------------+
13
+ # | reduced capacity reviewer | 1 |
14
+ # | reviewer | 2 |
15
+ # | hungry reviewer | 4 |
16
+ # | reduced capacity traintainer | 3 |
17
+ # | traintainer | 6 |
18
+ # | hungry traintainer | 8 |
19
+ # +------------------------------+--------------------------------+
20
+ #
21
+ class Reviewers
22
+ DEFAULT_REVIEWER_WEIGHT = Gitlab::Dangerfiles::Weightage::CAPACITY_MULTIPLIER * Gitlab::Dangerfiles::Weightage::BASE_REVIEWER_WEIGHT
23
+ TRAINTAINER_WEIGHT = 3
24
+
25
+ def initialize(reviewers, traintainers)
26
+ @reviewers = reviewers
27
+ @traintainers = traintainers
28
+ end
29
+
30
+ def execute
31
+ # TODO: take CODEOWNERS into account?
32
+ # https://gitlab.com/gitlab-org/gitlab/issues/26723
33
+
34
+ weighted_reviewers + weighted_traintainers
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :reviewers, :traintainers
40
+
41
+ def weighted_reviewers
42
+ reviewers.each_with_object([]) do |reviewer, total_reviewers|
43
+ add_weighted_reviewer(total_reviewers, reviewer, Gitlab::Dangerfiles::Weightage::BASE_REVIEWER_WEIGHT)
44
+ end
45
+ end
46
+
47
+ def weighted_traintainers
48
+ traintainers.each_with_object([]) do |reviewer, total_traintainers|
49
+ add_weighted_reviewer(total_traintainers, reviewer, TRAINTAINER_WEIGHT)
50
+ end
51
+ end
52
+
53
+ def add_weighted_reviewer(reviewers, reviewer, weight)
54
+ if reviewer.reduced_capacity
55
+ reviewers.fill(reviewer, reviewers.size, weight)
56
+ elsif reviewer.hungry
57
+ reviewers.fill(reviewer, reviewers.size, weight * Gitlab::Dangerfiles::Weightage::CAPACITY_MULTIPLIER + DEFAULT_REVIEWER_WEIGHT)
58
+ else
59
+ reviewers.fill(reviewer, reviewers.size, weight * Gitlab::Dangerfiles::Weightage::CAPACITY_MULTIPLIER)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-dangerfiles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rémy Coutable
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-23 00:00:00.000000000 Z
11
+ date: 2021-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: danger
@@ -135,10 +135,16 @@ files:
135
135
  - lib/gitlab-dangerfiles.rb
136
136
  - lib/gitlab/Dangerfile
137
137
  - lib/gitlab/dangerfiles.rb
138
+ - lib/gitlab/dangerfiles/base_linter.rb
138
139
  - lib/gitlab/dangerfiles/commit_linter.rb
139
140
  - lib/gitlab/dangerfiles/emoji_checker.rb
141
+ - lib/gitlab/dangerfiles/merge_request_linter.rb
140
142
  - lib/gitlab/dangerfiles/teammate.rb
143
+ - lib/gitlab/dangerfiles/title_linting.rb
141
144
  - lib/gitlab/dangerfiles/version.rb
145
+ - lib/gitlab/dangerfiles/weightage.rb
146
+ - lib/gitlab/dangerfiles/weightage/maintainers.rb
147
+ - lib/gitlab/dangerfiles/weightage/reviewers.rb
142
148
  homepage: https://gitlab.com/gitlab-org/gitlab-dangerfiles
143
149
  licenses:
144
150
  - MIT
@@ -162,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
168
  - !ruby/object:Gem::Version
163
169
  version: '0'
164
170
  requirements: []
165
- rubygems_version: 3.1.2
171
+ rubygems_version: 3.1.4
166
172
  signing_key:
167
173
  specification_version: 4
168
174
  summary: This gem provides common Dangerfile and plugins for GitLab projects.