gitlab-dangerfiles 0.5.0 → 0.8.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 +78 -19
- data/lib/danger/roulette.rb +16 -8
- data/lib/gitlab/dangerfiles.rb +3 -0
- data/lib/gitlab/dangerfiles/base_linter.rb +97 -0
- data/lib/gitlab/dangerfiles/changes.rb +53 -0
- data/lib/gitlab/dangerfiles/commit_linter.rb +27 -103
- data/lib/gitlab/dangerfiles/merge_request_linter.rb +30 -0
- data/lib/gitlab/dangerfiles/spec_helper.rb +66 -0
- data/lib/gitlab/dangerfiles/teammate.rb +7 -1
- 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 +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8132453b5b4614eaa87813509a18230d9c94d00cdec5b9e6857b032800092b0
|
4
|
+
data.tar.gz: 2e8dfe3056a7da31abbde54e9a8d1cbb96e9c9105d7fde5058903cc2617dbcfe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d1020a4fe7e5bcce74d8c03127373ae1a97ecf7ff57a981167cb6b8b733a17d5aef4b4c40fb31b88e996f8fe5830ecd779c49dc89609c3180f8ccf558a99028
|
7
|
+
data.tar.gz: 0d13715735e7bdded2a526575e276f21e11c11fb57f2fdcecdceb9051143f1995e2527cb4a57d9427aa3d8d2ced85d13775d96819d80610ae555f3108b5e508d
|
data/lib/danger/helper.rb
CHANGED
@@ -3,7 +3,9 @@
|
|
3
3
|
require "net/http"
|
4
4
|
require "json"
|
5
5
|
require "danger"
|
6
|
+
require_relative "../gitlab/dangerfiles/changes"
|
6
7
|
require_relative "../gitlab/dangerfiles/teammate"
|
8
|
+
require_relative "../gitlab/dangerfiles/title_linting"
|
7
9
|
|
8
10
|
module Danger
|
9
11
|
# Common helper functions for our danger scripts.
|
@@ -16,6 +18,7 @@ module Danger
|
|
16
18
|
qa: "~QA",
|
17
19
|
test: "~test ~Quality for `spec/features/*`",
|
18
20
|
engineering_productivity: '~"Engineering Productivity" for CI, Danger',
|
21
|
+
ci_template: '~"ci::templates"',
|
19
22
|
}.freeze
|
20
23
|
|
21
24
|
HTTPError = Class.new(StandardError)
|
@@ -104,13 +107,38 @@ module Danger
|
|
104
107
|
end
|
105
108
|
end
|
106
109
|
|
107
|
-
# @return [Hash<
|
110
|
+
# @return [Hash<Symbol,Array<String>>]
|
108
111
|
def changes_by_category(categories)
|
109
112
|
all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
|
110
113
|
categories_for_file(file, categories).each { |category| hash[category] << file }
|
111
114
|
end
|
112
115
|
end
|
113
116
|
|
117
|
+
# @return [Gitlab::Dangerfiles::Changes]
|
118
|
+
def changes(categories)
|
119
|
+
Gitlab::Dangerfiles::Changes.new([]).tap do |changes|
|
120
|
+
git.added_files.each do |file|
|
121
|
+
categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :added, category) }
|
122
|
+
end
|
123
|
+
|
124
|
+
git.modified_files.each do |file|
|
125
|
+
categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :modified, category) }
|
126
|
+
end
|
127
|
+
|
128
|
+
git.deleted_files.each do |file|
|
129
|
+
categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :deleted, category) }
|
130
|
+
end
|
131
|
+
|
132
|
+
git.renamed_files.map { |x| x[:before] }.each do |file|
|
133
|
+
categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_before, category) }
|
134
|
+
end
|
135
|
+
|
136
|
+
git.renamed_files.map { |x| x[:after] }.each do |file|
|
137
|
+
categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_after, category) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
114
142
|
# Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`
|
115
143
|
# using filename regex and specific change regex if given.
|
116
144
|
#
|
@@ -140,39 +168,64 @@ module Danger
|
|
140
168
|
usernames.map { |u| Gitlab::Dangerfiles::Teammate.new("username" => u) }
|
141
169
|
end
|
142
170
|
|
143
|
-
def
|
144
|
-
|
171
|
+
def mr_iid
|
172
|
+
return "" unless gitlab_helper
|
173
|
+
|
174
|
+
gitlab_helper.mr_json["iid"]
|
145
175
|
end
|
146
176
|
|
147
|
-
def
|
148
|
-
return
|
177
|
+
def mr_title
|
178
|
+
return "" unless gitlab_helper
|
149
179
|
|
150
|
-
|
180
|
+
gitlab_helper.mr_json["title"]
|
151
181
|
end
|
152
182
|
|
153
|
-
def
|
154
|
-
return
|
183
|
+
def mr_web_url
|
184
|
+
return "" unless gitlab_helper
|
185
|
+
|
186
|
+
gitlab_helper.mr_json["web_url"]
|
187
|
+
end
|
188
|
+
|
189
|
+
def mr_labels
|
190
|
+
return [] unless gitlab_helper
|
155
191
|
|
156
|
-
gitlab_helper.
|
192
|
+
gitlab_helper.mr_labels
|
193
|
+
end
|
194
|
+
|
195
|
+
def mr_target_branch
|
196
|
+
return "" unless gitlab_helper
|
197
|
+
|
198
|
+
gitlab_helper.mr_json["target_branch"]
|
199
|
+
end
|
200
|
+
|
201
|
+
def draft_mr?
|
202
|
+
Gitlab::Dangerfiles::TitleLinting.has_draft_flag?(mr_title)
|
203
|
+
end
|
204
|
+
|
205
|
+
def security_mr?
|
206
|
+
mr_web_url.include?("/gitlab-org/security/")
|
157
207
|
end
|
158
208
|
|
159
209
|
def cherry_pick_mr?
|
160
|
-
|
210
|
+
Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
|
211
|
+
end
|
161
212
|
|
162
|
-
|
213
|
+
def run_all_rspec_mr?
|
214
|
+
Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
|
163
215
|
end
|
164
216
|
|
165
|
-
def
|
166
|
-
|
217
|
+
def run_as_if_foss_mr?
|
218
|
+
Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
|
219
|
+
end
|
167
220
|
|
168
|
-
|
221
|
+
def stable_branch?
|
222
|
+
/\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
|
169
223
|
end
|
170
224
|
|
171
225
|
def mr_has_labels?(*labels)
|
172
|
-
return false unless gitlab_helper
|
173
|
-
|
174
226
|
labels = labels.flatten.uniq
|
175
|
-
|
227
|
+
|
228
|
+
(labels & mr_labels) == labels
|
176
229
|
end
|
177
230
|
|
178
231
|
def labels_list(labels, sep: ", ")
|
@@ -189,10 +242,16 @@ module Danger
|
|
189
242
|
all_changed_files.grep(regex)
|
190
243
|
end
|
191
244
|
|
192
|
-
private
|
193
|
-
|
194
245
|
def has_database_scoped_labels?(current_mr_labels)
|
195
246
|
current_mr_labels.any? { |label| label.start_with?("database::") }
|
196
247
|
end
|
248
|
+
|
249
|
+
def has_ci_changes?
|
250
|
+
changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
|
251
|
+
end
|
252
|
+
|
253
|
+
def group_label(labels)
|
254
|
+
labels.find { |label| label.start_with?("group::") }
|
255
|
+
end
|
197
256
|
end
|
198
257
|
end
|
data/lib/danger/roulette.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "helper"
|
3
4
|
require_relative "../gitlab/dangerfiles/teammate"
|
5
|
+
require_relative "../gitlab/dangerfiles/weightage/maintainers"
|
6
|
+
require_relative "../gitlab/dangerfiles/weightage/reviewers"
|
4
7
|
|
5
8
|
module Danger
|
6
9
|
# Common helper functions for our danger scripts. See Danger::Helper
|
@@ -24,7 +27,7 @@ module Danger
|
|
24
27
|
#
|
25
28
|
# @return [Array<Spin>]
|
26
29
|
def spin(project, categories, timezone_experiment: false)
|
27
|
-
spins = categories.map do |category|
|
30
|
+
spins = categories.sort.map do |category|
|
28
31
|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)
|
29
32
|
|
30
33
|
spin_for_category(project, category, timezone_experiment: including_timezone)
|
@@ -37,7 +40,7 @@ module Danger
|
|
37
40
|
case spin.category
|
38
41
|
when :qa
|
39
42
|
# MR includes QA changes, but also other changes, and author isn't an SET
|
40
|
-
if categories.size > 1 && !team_mr_author&.
|
43
|
+
if categories.size > 1 && !team_mr_author&.any_capability?(project, spin.category)
|
41
44
|
spin.optional_role = :maintainer
|
42
45
|
end
|
43
46
|
when :test
|
@@ -52,6 +55,11 @@ module Danger
|
|
52
55
|
# Fetch an already picked backend maintainer, or pick one otherwise
|
53
56
|
spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
|
54
57
|
end
|
58
|
+
when :ci_template
|
59
|
+
if spin.maintainer.nil?
|
60
|
+
# Fetch an already picked backend maintainer, or pick one otherwise
|
61
|
+
spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
|
62
|
+
end
|
55
63
|
end
|
56
64
|
end
|
57
65
|
|
@@ -146,13 +154,13 @@ module Danger
|
|
146
154
|
spin_role_for_category(team, role, project, category)
|
147
155
|
end
|
148
156
|
|
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
157
|
random = new_random(mr_source_branch)
|
154
|
-
|
155
|
-
|
158
|
+
|
159
|
+
weighted_reviewers = Gitlab::Dangerfiles::Weightage::Reviewers.new(reviewers, traintainers).execute
|
160
|
+
weighted_maintainers = Gitlab::Dangerfiles::Weightage::Maintainers.new(maintainers).execute
|
161
|
+
|
162
|
+
reviewer = spin_for_person(weighted_reviewers, random: random, timezone_experiment: timezone_experiment)
|
163
|
+
maintainer = spin_for_person(weighted_maintainers, random: random, timezone_experiment: timezone_experiment)
|
156
164
|
|
157
165
|
Spin.new(category, reviewer, maintainer, false, timezone_experiment)
|
158
166
|
end
|
data/lib/gitlab/dangerfiles.rb
CHANGED
@@ -0,0 +1,97 @@
|
|
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
|
+
@subject ||= 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 subject.empty?
|
79
|
+
return false if ("A".."Z").cover?(subject[0])
|
80
|
+
|
81
|
+
first_char = subject.sub(/\A(\[.+\]|\w+:)\s/, "")[0]
|
82
|
+
first_char_downcased = first_char.downcase
|
83
|
+
return true unless ("a".."z").cover?(first_char_downcased)
|
84
|
+
|
85
|
+
first_char.downcase == first_char
|
86
|
+
end
|
87
|
+
|
88
|
+
def subject_ends_with_a_period?
|
89
|
+
subject.end_with?(".")
|
90
|
+
end
|
91
|
+
|
92
|
+
def message_parts
|
93
|
+
@message_parts ||= commit.message.split("\n", 3)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "title_linting"
|
4
|
+
|
5
|
+
module Gitlab
|
6
|
+
module Dangerfiles
|
7
|
+
Change = Struct.new(:file, :change_type, :category)
|
8
|
+
|
9
|
+
class Changes < ::SimpleDelegator
|
10
|
+
def added
|
11
|
+
select_by_change_type(:added)
|
12
|
+
end
|
13
|
+
|
14
|
+
def modified
|
15
|
+
select_by_change_type(:modified)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deleted
|
19
|
+
select_by_change_type(:deleted)
|
20
|
+
end
|
21
|
+
|
22
|
+
def renamed_before
|
23
|
+
select_by_change_type(:renamed_before)
|
24
|
+
end
|
25
|
+
|
26
|
+
def renamed_after
|
27
|
+
select_by_change_type(:renamed_after)
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_category?(category)
|
31
|
+
any? { |change| change.category == category }
|
32
|
+
end
|
33
|
+
|
34
|
+
def by_category(category)
|
35
|
+
Changes.new(select { |change| change.category == category })
|
36
|
+
end
|
37
|
+
|
38
|
+
def categories
|
39
|
+
map(&:category).uniq
|
40
|
+
end
|
41
|
+
|
42
|
+
def files
|
43
|
+
map(&:file)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def select_by_change_type(change_type)
|
49
|
+
Changes.new(select { |change| change.change_type == change_type })
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -1,40 +1,35 @@
|
|
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
|
8
|
+
class CommitLinter < BaseLinter
|
11
9
|
MAX_CHANGED_FILES_IN_COMMIT = 3
|
12
10
|
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
|
11
|
+
SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&|%)\d+(?<!`)}.freeze
|
12
|
+
|
13
|
+
def self.problems_mapping
|
14
|
+
super.merge(
|
15
|
+
{
|
16
|
+
separator_missing: "The commit subject and body must be separated by a blank line",
|
17
|
+
details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
|
18
|
+
"at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
|
19
|
+
details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
|
20
|
+
message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
|
21
|
+
"to the commit message, and are displayed as plain text outside of GitLab",
|
22
|
+
message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
|
23
|
+
"message, and may not be displayed properly everywhere",
|
24
|
+
message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
|
25
|
+
"`!123`), as short references are displayed as plain text outside of GitLab",
|
26
|
+
}
|
27
|
+
)
|
28
|
+
end
|
34
29
|
|
35
30
|
def initialize(commit)
|
36
|
-
|
37
|
-
|
31
|
+
super
|
32
|
+
|
38
33
|
@linted = false
|
39
34
|
end
|
40
35
|
|
@@ -58,19 +53,11 @@ module Gitlab
|
|
58
53
|
!details.nil? && !details.empty?
|
59
54
|
end
|
60
55
|
|
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")
|
56
|
+
def lint
|
70
57
|
return self if @linted
|
71
58
|
|
72
59
|
@linted = true
|
73
|
-
lint_subject
|
60
|
+
lint_subject
|
74
61
|
lint_separator
|
75
62
|
lint_details
|
76
63
|
lint_message
|
@@ -78,26 +65,6 @@ module Gitlab
|
|
78
65
|
self
|
79
66
|
end
|
80
67
|
|
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
68
|
private
|
102
69
|
|
103
70
|
def lint_separator
|
@@ -114,15 +81,11 @@ module Gitlab
|
|
114
81
|
end
|
115
82
|
|
116
83
|
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
|
84
|
+
line_without_urls = line.strip.gsub(%r{https?://\S+}, "")
|
122
85
|
|
123
86
|
# If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but
|
124
87
|
# only if the line _without_ the URL does not exceed this limit.
|
125
|
-
next unless line_too_long?(
|
88
|
+
next unless line_too_long?(line_without_urls)
|
126
89
|
|
127
90
|
add_problem(:details_line_too_long)
|
128
91
|
break
|
@@ -159,10 +122,6 @@ module Gitlab
|
|
159
122
|
files_changed > MAX_CHANGED_FILES_IN_COMMIT && lines_changed > MAX_CHANGED_LINES_IN_COMMIT
|
160
123
|
end
|
161
124
|
|
162
|
-
def subject
|
163
|
-
message_parts[0].delete_prefix(WIP_PREFIX)
|
164
|
-
end
|
165
|
-
|
166
125
|
def separator
|
167
126
|
message_parts[1]
|
168
127
|
end
|
@@ -171,37 +130,6 @@ module Gitlab
|
|
171
130
|
message_parts[2]&.gsub(/^Signed-off-by.*$/, "")
|
172
131
|
end
|
173
132
|
|
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
133
|
def message_contains_text_emoji?
|
206
134
|
emoji_checker.includes_text_emoji?(commit.message)
|
207
135
|
end
|
@@ -217,10 +145,6 @@ module Gitlab
|
|
217
145
|
def emoji_checker
|
218
146
|
@emoji_checker ||= Gitlab::Dangerfiles::EmojiChecker.new
|
219
147
|
end
|
220
|
-
|
221
|
-
def message_parts
|
222
|
-
@message_parts ||= commit.message.split("\n", 3)
|
223
|
-
end
|
224
148
|
end
|
225
149
|
end
|
226
150
|
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
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require "danger"
|
2
|
+
require "gitlab/dangerfiles/changes"
|
3
|
+
|
4
|
+
module DangerSpecHelper
|
5
|
+
# These functions are a subset of https://github.com/danger/danger/blob/master/spec/spec_helper.rb
|
6
|
+
# If you are expanding these files, see if it's already been done ^.
|
7
|
+
|
8
|
+
# A silent version of the user interface
|
9
|
+
def self.testing_ui
|
10
|
+
Cork::Board.new(silent: true)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Example environment (ENV) that would come from
|
14
|
+
# running a PR on TravisCI
|
15
|
+
def self.testing_env
|
16
|
+
{
|
17
|
+
"GITLAB_CI" => "true",
|
18
|
+
"DANGER_GITLAB_HOST" => "gitlab.example.com",
|
19
|
+
"CI_MERGE_REQUEST_IID" => 28_493,
|
20
|
+
"DANGER_GITLAB_API_TOKEN" => "123sbdq54erfsd3422gdfio",
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# A stubbed out Dangerfile for use in tests
|
25
|
+
def self.testing_dangerfile
|
26
|
+
env = Danger::EnvironmentManager.new(testing_env)
|
27
|
+
Danger::Dangerfile.new(env, testing_ui)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.fake_danger
|
31
|
+
Class.new do
|
32
|
+
attr_reader :git, :gitlab, :helper
|
33
|
+
|
34
|
+
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
35
|
+
def initialize(git: nil, gitlab: nil, helper: nil)
|
36
|
+
@git = git
|
37
|
+
@gitlab = gitlab
|
38
|
+
@helper = helper
|
39
|
+
end
|
40
|
+
|
41
|
+
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
RSpec.shared_context "with dangerfile" do
|
47
|
+
let(:dangerfile) { DangerSpecHelper.testing_dangerfile }
|
48
|
+
let(:added_files) { %w[added1] }
|
49
|
+
let(:modified_files) { %w[modified1] }
|
50
|
+
let(:deleted_files) { %w[deleted1] }
|
51
|
+
let(:renamed_before_file) { "renamed_before" }
|
52
|
+
let(:renamed_after_file) { "renamed_after" }
|
53
|
+
let(:renamed_files) { [{ before: renamed_before_file, after: renamed_after_file }] }
|
54
|
+
let(:change_class) { Gitlab::Dangerfiles::Change }
|
55
|
+
let(:changes_class) { Gitlab::Dangerfiles::Changes }
|
56
|
+
let(:changes) { changes_class.new([]) }
|
57
|
+
let(:mr_title) { "Fake Title" }
|
58
|
+
let(:mr_labels) { [] }
|
59
|
+
|
60
|
+
let(:fake_git) { double("fake-git", added_files: added_files, modified_files: modified_files, deleted_files: deleted_files, renamed_files: renamed_files) }
|
61
|
+
let(:fake_helper) { double("fake-helper", changes: changes, mr_iid: 1234, mr_title: mr_title, mr_labels: mr_labels) }
|
62
|
+
|
63
|
+
before do
|
64
|
+
allow(dangerfile).to receive(:git).and_return(fake_git)
|
65
|
+
end
|
66
|
+
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Gitlab
|
4
4
|
module Dangerfiles
|
5
5
|
class Teammate
|
6
|
-
attr_reader :options, :username, :name, :role, :projects, :available, :hungry, :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 = {})
|
@@ -15,6 +15,7 @@ module Gitlab
|
|
15
15
|
@projects = options["projects"]
|
16
16
|
@available = options["available"]
|
17
17
|
@hungry = options["hungry"]
|
18
|
+
@reduced_capacity = options["reduced_capacity"]
|
18
19
|
@tz_offset_hours = options["tz_offset_hours"]
|
19
20
|
end
|
20
21
|
|
@@ -32,6 +33,10 @@ module Gitlab
|
|
32
33
|
projects&.has_key?(name)
|
33
34
|
end
|
34
35
|
|
36
|
+
def any_capability?(project, category)
|
37
|
+
capabilities(project).any? { |capability| capability.end_with?(category.to_s) }
|
38
|
+
end
|
39
|
+
|
35
40
|
def reviewer?(project, category, labels)
|
36
41
|
has_capability?(project, category, :reviewer, labels)
|
37
42
|
end
|
@@ -93,6 +98,7 @@ module Gitlab
|
|
93
98
|
when :engineering_productivity
|
94
99
|
return false unless role[/Engineering Productivity/]
|
95
100
|
return true if kind == :reviewer
|
101
|
+
return true if capabilities(project).include?("#{kind} engineering_productivity")
|
96
102
|
|
97
103
|
capabilities(project).include?("#{kind} backend")
|
98
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.8.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-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: danger
|
@@ -135,10 +135,18 @@ 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
|
139
|
+
- lib/gitlab/dangerfiles/changes.rb
|
138
140
|
- lib/gitlab/dangerfiles/commit_linter.rb
|
139
141
|
- lib/gitlab/dangerfiles/emoji_checker.rb
|
142
|
+
- lib/gitlab/dangerfiles/merge_request_linter.rb
|
143
|
+
- lib/gitlab/dangerfiles/spec_helper.rb
|
140
144
|
- lib/gitlab/dangerfiles/teammate.rb
|
145
|
+
- lib/gitlab/dangerfiles/title_linting.rb
|
141
146
|
- lib/gitlab/dangerfiles/version.rb
|
147
|
+
- lib/gitlab/dangerfiles/weightage.rb
|
148
|
+
- lib/gitlab/dangerfiles/weightage/maintainers.rb
|
149
|
+
- lib/gitlab/dangerfiles/weightage/reviewers.rb
|
142
150
|
homepage: https://gitlab.com/gitlab-org/gitlab-dangerfiles
|
143
151
|
licenses:
|
144
152
|
- MIT
|
@@ -162,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
170
|
- !ruby/object:Gem::Version
|
163
171
|
version: '0'
|
164
172
|
requirements: []
|
165
|
-
rubygems_version: 3.1.
|
173
|
+
rubygems_version: 3.1.4
|
166
174
|
signing_key:
|
167
175
|
specification_version: 4
|
168
176
|
summary: This gem provides common Dangerfile and plugins for GitLab projects.
|