gitlab-dangerfiles 0.2.0 → 0.6.1

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