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