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 +4 -4
- data/lib/danger/helper.rb +127 -24
- data/lib/danger/roulette.rb +15 -8
- data/lib/gitlab/dangerfiles/base_linter.rb +96 -0
- data/lib/gitlab/dangerfiles/commit_linter.rb +27 -104
- data/lib/gitlab/dangerfiles/merge_request_linter.rb +30 -0
- data/lib/gitlab/dangerfiles/teammate.rb +23 -9
- data/lib/gitlab/dangerfiles/title_linting.rb +38 -0
- data/lib/gitlab/dangerfiles/version.rb +1 -1
- data/lib/gitlab/dangerfiles/weightage.rb +10 -0
- data/lib/gitlab/dangerfiles/weightage/maintainers.rb +33 -0
- data/lib/gitlab/dangerfiles/weightage/reviewers.rb +65 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1376c48a26c7b3a10cc3e649d1cf28f49fdf58fd7b317713b9ac9a0f3771e5a5
|
4
|
+
data.tar.gz: 9ecd1d0db0f583740a73f96f6ffd14905f7a66157e427c6a11410a7bc7c70e55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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<
|
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
|
-
#
|
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
|
141
|
-
|
142
|
-
["database"]
|
143
|
-
else
|
144
|
-
["database", "database::review pending"]
|
145
|
-
end
|
216
|
+
def mr_iid
|
217
|
+
return "" unless gitlab_helper
|
146
218
|
|
147
|
-
|
219
|
+
gitlab_helper.mr_json["iid"]
|
148
220
|
end
|
149
221
|
|
150
|
-
def
|
151
|
-
|
222
|
+
def mr_title
|
223
|
+
return "" unless gitlab_helper
|
224
|
+
|
225
|
+
gitlab_helper.mr_json["title"]
|
152
226
|
end
|
153
227
|
|
154
|
-
def
|
155
|
-
return
|
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
|
-
|
250
|
+
def security_mr?
|
251
|
+
mr_web_url.include?("/gitlab-org/security/")
|
158
252
|
end
|
159
253
|
|
160
254
|
def cherry_pick_mr?
|
161
|
-
|
255
|
+
Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
|
256
|
+
end
|
162
257
|
|
163
|
-
|
258
|
+
def run_all_rspec_mr?
|
259
|
+
Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
|
164
260
|
end
|
165
261
|
|
166
|
-
def
|
167
|
-
|
262
|
+
def run_as_if_foss_mr?
|
263
|
+
Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
|
264
|
+
end
|
168
265
|
|
169
|
-
|
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
|
-
|
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
|
data/lib/danger/roulette.rb
CHANGED
@@ -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&.
|
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
|
-
|
155
|
-
|
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
|
-
|
4
|
-
|
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
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
37
|
-
|
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
|
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
|
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
|
-
|
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?(
|
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
|
-
|
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(
|
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
|
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
|
@@ -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.
|
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:
|
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.
|
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.
|