gitlab-dangerfiles 0.5.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d044eef9bd28cb411b658eb1211c49a4f0ec769a93121c2fd7ce698fcd10a95c
4
- data.tar.gz: 56ab60d3e31b399f314a1653b03c81f36efc84493585f00502e36dbc38d0554b
3
+ metadata.gz: e8132453b5b4614eaa87813509a18230d9c94d00cdec5b9e6857b032800092b0
4
+ data.tar.gz: 2e8dfe3056a7da31abbde54e9a8d1cbb96e9c9105d7fde5058903cc2617dbcfe
5
5
  SHA512:
6
- metadata.gz: 9ffef7ad5e6f4d1ba093f254f210ed70391a395123b7e7d372b4cd947145d9a4958d88454e414686bf292822434ab54087a14c912cf7c16e832ff3b3aeb0d340
7
- data.tar.gz: 804db12887f8acf6bcd4cded8d420537c508bf66a684d53522fcaa96b6f7c60f959ad0b86484bded1c9aac19abcf6aa6be38c90e1ba9e47b759670b9a0eaeb0a
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<String,Array<String>>]
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 sanitize_mr_title(title)
144
- title.gsub(/^WIP: */, "").gsub(/`/, '\\\`')
171
+ def mr_iid
172
+ return "" unless gitlab_helper
173
+
174
+ gitlab_helper.mr_json["iid"]
145
175
  end
146
176
 
147
- def draft_mr?
148
- return false unless gitlab_helper
177
+ def mr_title
178
+ return "" unless gitlab_helper
149
179
 
150
- DRAFT_REGEX.match?(gitlab_helper.mr_json["title"])
180
+ gitlab_helper.mr_json["title"]
151
181
  end
152
182
 
153
- def security_mr?
154
- return false unless gitlab_helper
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.mr_json["web_url"].include?("/gitlab-org/security/")
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
- return false unless gitlab_helper
210
+ Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
211
+ end
161
212
 
162
- /cherry[\s-]*pick/i.match?(gitlab_helper.mr_json["title"])
213
+ def run_all_rspec_mr?
214
+ Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
163
215
  end
164
216
 
165
- def stable_branch?
166
- return false unless gitlab_helper
217
+ def run_as_if_foss_mr?
218
+ Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
219
+ end
167
220
 
168
- /\A\d+-\d+-stable-ee/i.match?(gitlab_helper.mr_json["target_branch"])
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
- (labels & gitlab_helper.mr_labels) == labels
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
@@ -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&.reviewer?(project, spin.category, [])
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
- 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)
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
@@ -2,5 +2,8 @@ require "gitlab/dangerfiles/version"
2
2
 
3
3
  module Gitlab
4
4
  module Dangerfiles
5
+ def self.import_plugins(danger)
6
+ danger.import_plugin(File.expand_path("../danger/*.rb", __dir__))
7
+ end
5
8
  end
6
9
  end
@@ -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
- 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
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+\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
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
- @commit = commit
37
- @problems = {}
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 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")
56
+ def lint
70
57
  return self if @linted
71
58
 
72
59
  @linted = true
73
- lint_subject(subject_description)
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
- 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
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?(line.length - url_size)
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
@@ -1,5 +1,5 @@
1
1
  module Gitlab
2
2
  module Dangerfiles
3
- VERSION = "0.5.0"
3
+ VERSION = "0.8.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.5.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: 2020-09-29 00:00:00.000000000 Z
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.2
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.