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 +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.
|