gitlab-dangerfiles 1.1.1 → 2.0.0

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: f5e1069be5416d6980879af3330451834b57881c46101119bfc5917e2057edd7
4
- data.tar.gz: e911c514f8d32d65e4aecef6ad111c80c808dd273b12775d57438532ba8a05b3
3
+ metadata.gz: 0d3407bad03ff3220a55329e625c1fd5a962b4ffe29b4ab1da68a7e998761024
4
+ data.tar.gz: a9f31b18452500fa6e437a847b1d726cbbe2c47a1912c5e40fd18196ca3716fa
5
5
  SHA512:
6
- metadata.gz: b3814724620240a6485ffce4191fb8a2461c267dda5dad2466b6dc5173fbe848b66cae92aaea69ef82a49dd4726db3a7e77c106fdbd73ea58738eaf4613c4cba
7
- data.tar.gz: 0fcabbe03f856d29951ae2f35e9f6f9784fa22af8d8d6bfa13a41452b4a1c35a454135751443882e6a2762325ef05dd6b8bd1b3dde58c5c7661db65ffef4c5d7
6
+ metadata.gz: 3390ab204b7572654e748198fb1d03b70056ab26b32a8ea29cc2c624e307e59c5cfab5306afb8900c71df62671b984ccdbb21e7c1e9adb16b2ab43a9299774c0
7
+ data.tar.gz: '06879db4e54bff63e540793f8c94f3c9412d6481641144bfa80934bb36a82872c76e9693c86aa9b381d78528da06a27795f620ff9127829e4904938807958ede'
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --protected
2
+ --no-private
3
+ -
4
+ CODE_OF_CONDUCT.md
5
+ LICENSE.txt
data/Gemfile CHANGED
@@ -5,3 +5,4 @@ gemspec
5
5
 
6
6
  gem "rake", "~> 12.0"
7
7
  gem "guard-rspec"
8
+ gem "yard"
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2020 Rémy Coutable
3
+ Copyright (c) 2020-2021 GitLab
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Gitlab::Dangerfiles
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/gitlab/dangerfiles`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ The goal of this gem is to centralize Danger plugins and rules that to be used by multiple GitLab projects.
6
4
 
7
5
  ## Installation
8
6
 
@@ -14,15 +12,57 @@ gem 'gitlab-dangerfiles'
14
12
 
15
13
  And then execute:
16
14
 
17
- $ bundle install
15
+ ```sh
16
+ $ bundle install
17
+ ```
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install gitlab-dangerfiles
21
+ ```sh
22
+ $ gem install gitlab-dangerfiles
23
+ ```
22
24
 
23
25
  ## Usage
24
26
 
25
- TODO: Write usage instructions here
27
+ ### Importing plugins and rules
28
+
29
+ In your project's `Dangerfile`, add the following two line to import the plugins and rules from this gem:
30
+
31
+ ```ruby
32
+ # Get an instance of Gitlab::Dangerfiles
33
+ gitlab_dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
34
+
35
+ # Import all plugins from the gem
36
+ gitlab_dangerfiles.import_plugins
37
+
38
+ # Import all rules from the gem
39
+ gitlab_dangerfiles.import_dangerfiles
40
+
41
+ # Or import a subset of rules from the gem
42
+ gitlab_dangerfiles.import_dangerfiles(rules: [:changes_size])
43
+ ```
44
+
45
+ ### Plugins
46
+
47
+ Danger plugins are located under `lib/danger/plugins`.
48
+
49
+ - `Danger::Helper` available in `Dangerfile`s as `helper`
50
+ - `Danger::Roulette` available in `Dangerfile`s as `roulette`
51
+
52
+ For the full documentation about the plugins, please see https://www.rubydoc.info/gems/gitlab-dangerfiles.
53
+
54
+ ### Rules
55
+
56
+
57
+ Danger rules are located under `lib/danger/rules`.
58
+
59
+ #### `changes_size`
60
+
61
+ ##### Available configurations
62
+
63
+ - `code_size_thresholds`: A hash of the form `{ high: 42, medium: 12 }` where
64
+ `:high` is the lines changed threshold which triggers an error, and
65
+ `:medium` is the lines changed threshold which triggers a warning.
26
66
 
27
67
  ## Development
28
68
 
@@ -3,8 +3,8 @@ require_relative "lib/gitlab/dangerfiles/version"
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "gitlab-dangerfiles"
5
5
  spec.version = Gitlab::Dangerfiles::VERSION
6
- spec.authors = ["Rémy Coutable"]
7
- spec.email = ["remy@rymai.me"]
6
+ spec.authors = ["GitLab"]
7
+ spec.email = ["gitlab_rubygems@gitlab.com"]
8
8
 
9
9
  spec.summary = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
10
10
  spec.description = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
19
- spec.metadata["changelog_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
19
+ spec.metadata["changelog_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles/-/releases"
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
22
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
 
6
6
  require_relative "../../gitlab/dangerfiles/changes"
7
+ require_relative "../../gitlab/dangerfiles/config"
7
8
  require_relative "../../gitlab/dangerfiles/teammate"
8
9
  require_relative "../../gitlab/dangerfiles/title_linting"
9
10
 
@@ -11,7 +12,6 @@ module Danger
11
12
  # Common helper functions for our danger scripts.
12
13
  class Helper < Danger::Plugin
13
14
  RELEASE_TOOLS_BOT = "gitlab-release-tools-bot"
14
- DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
15
15
  CATEGORY_LABELS = {
16
16
  docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
17
17
  none: "",
@@ -22,36 +22,47 @@ module Danger
22
22
  product_intelligence: '~"product intelligence"',
23
23
  }.freeze
24
24
 
25
- HTTPError = Class.new(StandardError)
26
-
27
- def gitlab_helper
28
- # Unfortunately the following does not work:
29
- # - respond_to?(:gitlab)
30
- # - respond_to?(:gitlab, true)
31
- gitlab
32
- rescue NoMethodError
33
- nil
25
+ # Allows to set specific rule's configuration by passing a block.
26
+ #
27
+ # @yield [c] Yield a Gitlab::Dangerfiles::Config object
28
+ #
29
+ # @yieldparam [Gitlab::Dangerfiles::Config] The Gitlab::Dangerfiles::Config object
30
+ # @yieldreturn [Gitlab::Dangerfiles::Config] The Gitlab::Dangerfiles::Config object
31
+ #
32
+ # @example
33
+ # helper.config do |config|
34
+ # config.code_size_thresholds = { high: 42, medium: 12 }
35
+ # end
36
+ #
37
+ # @return [Gitlab::Dangerfiles::Config]
38
+ def config
39
+ (@config ||= Gitlab::Dangerfiles::Config.new).tap do |c|
40
+ yield c if block_given?
41
+ end
34
42
  end
35
43
 
36
- def html_link(str)
37
- ci? ? gitlab_helper.html_link(str) : str
44
+ # @example
45
+ # <a href='https://gitlab.com/artsy/eigen/blob/561827e46167077b5e53515b4b7349b8ae04610b/file.txt'>file.txt</a>
46
+ #
47
+ # @param [String, Array<String>] paths
48
+ # A list of strings to convert to gitlab anchors
49
+ # @param [Boolean] full_path
50
+ # Shows the full path as the link's text, defaults to +true+.
51
+ #
52
+ # @see https://danger.systems/reference.html Danger reference where #html_link is described
53
+ # @see https://github.com/danger/danger/blob/eca19719d3e585fe1cc46bc5377f9aa955ebf609/lib/danger/danger_core/plugins/dangerfile_gitlab_plugin.rb#L216 Danger reference where #html_link is implemented
54
+ #
55
+ # @return [String] a list of HTML anchors for a file, or multiple files
56
+ def html_link(paths, full_path: true)
57
+ ci? ? gitlab_helper.html_link(paths, full_path: full_path) : paths
38
58
  end
39
59
 
60
+ # @return [Boolean] whether we're in the CI context or not.
40
61
  def ci?
41
62
  !gitlab_helper.nil?
42
63
  end
43
64
 
44
- # @param [String] url
45
- def http_get_json(url)
46
- rsp = Net::HTTP.get_response(URI.parse(url))
47
-
48
- unless rsp.is_a?(Net::HTTPOK)
49
- raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
50
- end
51
-
52
- JSON.parse(rsp.body)
53
- end
54
-
65
+ # @return [Array<String>] a list of filenames added in this MR.
55
66
  def added_files
56
67
  @added_files ||= if changes_from_api
57
68
  changes_from_api.select { |file| file["new_file"] }.map { |file| file["new_path"] }
@@ -60,6 +71,7 @@ module Danger
60
71
  end
61
72
  end
62
73
 
74
+ # @return [Array<String>] a list of filenames modifier in this MR.
63
75
  def modified_files
64
76
  @modified_files ||= if changes_from_api
65
77
  changes_from_api.select { |file| !file["new_file"] && !file["deleted_file"] && !file["renamed_file"] }.map { |file| file["new_path"] }
@@ -68,6 +80,7 @@ module Danger
68
80
  end
69
81
  end
70
82
 
83
+ # @return [Array<String>] a list of filenames renamed in this MR.
71
84
  def renamed_files
72
85
  @renamed_files ||= if changes_from_api
73
86
  changes_from_api.select { |file| file["renamed_file"] }.each_with_object([]) do |file, memo|
@@ -78,6 +91,7 @@ module Danger
78
91
  end
79
92
  end
80
93
 
94
+ # @return [Array<String>] a list of filenames deleted in this MR.
81
95
  def deleted_files
82
96
  @deleted_files ||= if changes_from_api
83
97
  changes_from_api.select { |file| file["deleted_file"] }.map { |file| file["new_path"] }
@@ -86,43 +100,20 @@ module Danger
86
100
  end
87
101
  end
88
102
 
89
- def diff_for_file(filename)
90
- if changes_from_api
91
- changes_hash = changes_from_api.find { |file| file["new_path"] == filename }
92
- changes_hash["diff"] if changes_hash
93
- else
94
- git.diff_for_file(filename)&.patch
95
- end
96
- end
97
-
98
- def changes_from_api
99
- return nil unless ci?
100
- return nil if defined?(@force_changes_from_git)
101
-
102
- @changes_from_api ||= gitlab_helper.mr_changes.to_h["changes"]
103
- rescue
104
- # Fallback to the Git strategy in any case
105
- @force_changes_from_git = true
106
- nil
107
- end
108
-
109
- # Returns a list of all files that have been added, modified or renamed.
110
- # `modified_files` might contain paths that already have been renamed,
111
- # so we need to remove them from the list.
112
- #
113
- # Considering these changes:
103
+ # @example
104
+ # # Considering these changes:
105
+ # # - A new_file.rb
106
+ # # - D deleted_file.rb
107
+ # # - M modified_file.rb
108
+ # # - R renamed_file_before.rb -> renamed_file_after.rb
109
+ # # it will return:
114
110
  #
115
- # - A new_file.rb
116
- # - D deleted_file.rb
117
- # - M modified_file.rb
118
- # - R renamed_file_before.rb -> renamed_file_after.rb
111
+ # #=> ['new_file.rb', 'modified_file.rb', 'renamed_file_after.rb']
119
112
  #
120
- # it will return
121
- # ```
122
- # [ 'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb' ]
123
- # ```
124
113
  #
125
- # @return [Array<String>]
114
+ # @return [Array<String>] a list of all files that have been added, modified or renamed.
115
+ # +modified_files+ might contain paths that already have been renamed,
116
+ # so we need to remove them from the list.
126
117
  def all_changed_files
127
118
  Set.new
128
119
  .merge(added_files)
@@ -133,16 +124,19 @@ module Danger
133
124
  .sort
134
125
  end
135
126
 
136
- # Returns a string containing changed lines as git diff
127
+ # @param filename [String] A file name for which we want the diff.
137
128
  #
138
- # Considering changing a line in lib/gitlab/usage_data.rb it will return:
129
+ # @example
130
+ # # Considering changing a line in lib/gitlab/usage_data.rb, it will return:
139
131
  #
140
- # [ "--- a/lib/gitlab/usage_data.rb",
141
- # "+++ b/lib/gitlab/usage_data.rb",
142
- # "+ # Test change",
143
- # "- # Old change" ]
144
- def changed_lines(changed_file)
145
- diff = diff_for_file(changed_file)
132
+ # ["--- a/lib/gitlab/usage_data.rb",
133
+ # "+++ b/lib/gitlab/usage_data.rb",
134
+ # "+ # Test change",
135
+ # "- # Old change"]
136
+ #
137
+ # @return [Array<String>] an array of changed lines in Git diff format.
138
+ def changed_lines(filename)
139
+ diff = diff_for_file(filename)
146
140
  return [] unless diff
147
141
 
148
142
  diff.split("\n").select { |line| %r{^[+-]}.match?(line) }
@@ -152,6 +146,14 @@ module Danger
152
146
  gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
153
147
  end
154
148
 
149
+ # @param items [Array<String>] An array of items to transform into a bullet list.
150
+ #
151
+ # @example
152
+ # markdown_list(%w[foo bar])
153
+ # # => * foo
154
+ # * bar
155
+ #
156
+ # @return [String] a bullet list for the given +items+. If there are more than 10 items, wrap the list in a +<details></details>+ block.
155
157
  def markdown_list(items)
156
158
  list = items.map { |item| "* `#{item}`" }.join("\n")
157
159
 
@@ -162,14 +164,24 @@ module Danger
162
164
  end
163
165
  end
164
166
 
165
- # @return [Hash<Symbol,Array<String>>]
167
+ # @param categories [{Regexp => Array<Symbol>}, {Array<Regexp> => Array<Symbol>}] A hash of the form +{ filename_regex => categories, [filename_regex, changes_regex] => categories }+.
168
+ # +filename_regex+ is the regex pattern to match file names. +changes_regex+ is the regex pattern to
169
+ # match changed lines in files that match +filename_regex+
170
+ #
171
+ # @return [{Symbol => Array<String>}] a hash of the type +{ category1: ["file1", "file2"], category2: ["file3", "file4"] }+
172
+ # using filename regex (+filename_regex+) and specific change regex (+changes_regex+) from the given +categories+ hash.
166
173
  def changes_by_category(categories)
167
174
  all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
168
175
  categories_for_file(file, categories).each { |category| hash[category] << file }
169
176
  end
170
177
  end
171
178
 
172
- # @return [Gitlab::Dangerfiles::Changes]
179
+ # @param categories [{Regexp => Array<Symbol>}, {Array<Regexp> => Array<Symbol>}] A hash of the form +{ filename_regex => categories, [filename_regex, changes_regex] => categories }+.
180
+ # +filename_regex+ is the regex pattern to match file names. +changes_regex+ is the regex pattern to
181
+ # match changed lines in files that match +filename_regex+
182
+ #
183
+ # @return [Gitlab::Dangerfiles::Changes] a +Gitlab::Dangerfiles::Changes+ object that represents the changes of an MR
184
+ # using filename regex (+filename_regex+) and specific change regex (+changes_regex+) from the given +categories+ hash.
173
185
  def changes(categories)
174
186
  Gitlab::Dangerfiles::Changes.new([]).tap do |changes|
175
187
  added_files.each do |file|
@@ -194,16 +206,19 @@ module Danger
194
206
  end
195
207
  end
196
208
 
197
- # Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`
198
- # using filename regex and specific change regex if given.
209
+ # @param filename [String] A file name.
210
+ # @param categories [{Regexp => Array<Symbol>}, {Array<Regexp> => Array<Symbol>}] A hash of the form +{ filename_regex => categories, [filename_regex, changes_regex] => categories }+.
211
+ # +filename_regex+ is the regex pattern to match file names. +changes_regex+ is the regex pattern to
212
+ # match changed lines in files that match +filename_regex+
199
213
  #
200
- # @return Array<Symbol>
201
- def categories_for_file(file, categories)
214
+ # @return [Array<Symbol>] the categories a file is in, e.g., +[:frontend]+, +[:backend]+, or +%i[frontend engineering_productivity]+
215
+ # using filename regex (+filename_regex+) and specific change regex (+changes_regex+) from the given +categories+ hash.
216
+ def categories_for_file(filename, categories)
202
217
  _, categories = categories.find do |key, _|
203
218
  filename_regex, changes_regex = Array(key)
204
219
 
205
- found = filename_regex.match?(file)
206
- found &&= changed_lines(file).any? { |changed_line| changes_regex.match?(changed_line) } if changes_regex
220
+ found = filename_regex.match?(filename)
221
+ found &&= changed_lines(filename).any? { |changed_line| changes_regex.match?(changed_line) } if changes_regex
207
222
 
208
223
  found
209
224
  end
@@ -211,114 +226,192 @@ module Danger
211
226
  Array(categories || :unknown)
212
227
  end
213
228
 
214
- # Returns the GFM for a category label, making its best guess if it's not
215
- # a category we know about.
229
+ # @param category [Symbol] A category.
216
230
  #
217
- # @return[String]
231
+ # @return [String] the GFM for a category label, making its best guess if it's not
232
+ # a category we know about.
218
233
  def label_for_category(category)
219
234
  CATEGORY_LABELS.fetch(category, "~#{category}")
220
235
  end
221
236
 
222
- def new_teammates(usernames)
223
- usernames.map { |u| Gitlab::Dangerfiles::Teammate.new("username" => u) }
224
- end
225
-
237
+ # @return [String] +""+ when not in the CI context, and the MR IID as a string otherwise.
226
238
  def mr_iid
227
239
  return "" unless ci?
228
240
 
229
- gitlab_helper.mr_json["iid"]
241
+ gitlab_helper.mr_json["iid"].to_s
230
242
  end
231
243
 
244
+ # @return [String] +`whoami`+ when not in the CI context, and the MR author username otherwise.
232
245
  def mr_author
233
246
  return `whoami`.strip unless ci?
234
247
 
235
248
  gitlab_helper.mr_author
236
249
  end
237
250
 
251
+ # @return [String] +""+ when not in the CI context, and the MR title otherwise.
238
252
  def mr_title
239
253
  return "" unless ci?
240
254
 
241
255
  gitlab_helper.mr_json["title"]
242
256
  end
243
257
 
258
+ # @return [String] +""+ when not in the CI context, and the MR URL otherwise.
244
259
  def mr_web_url
245
260
  return "" unless ci?
246
261
 
247
262
  gitlab_helper.mr_json["web_url"]
248
263
  end
249
264
 
265
+ # @return [Array<String>] +[]+ when not in the CI context, and the MR labels otherwise.
250
266
  def mr_labels
251
267
  return [] unless ci?
252
268
 
253
269
  gitlab_helper.mr_labels
254
270
  end
255
271
 
272
+ # @return [String] +`git rev-parse --abbrev-ref HEAD`+ when not in the CI context, and the MR source branch otherwise.
256
273
  def mr_source_branch
257
274
  return `git rev-parse --abbrev-ref HEAD`.strip unless ci?
258
275
 
259
276
  gitlab_helper.mr_json["source_branch"]
260
277
  end
261
278
 
279
+ # @return [String] +""+ when not in the CI context, and the MR target branch otherwise.
262
280
  def mr_target_branch
263
281
  return "" unless ci?
264
282
 
265
283
  gitlab_helper.mr_json["target_branch"]
266
284
  end
267
285
 
286
+ # @return [Boolean] whether a MR is a Draft or not.
268
287
  def draft_mr?
269
288
  Gitlab::Dangerfiles::TitleLinting.has_draft_flag?(mr_title)
270
289
  end
271
290
 
291
+ # @return [Boolean] whether a MR is opened in the security mirror or not.
272
292
  def security_mr?
273
293
  mr_web_url.include?("/gitlab-org/security/")
274
294
  end
275
295
 
296
+ # @return [Boolean] whether a MR title includes "cherry-pick" or not.
276
297
  def cherry_pick_mr?
277
298
  Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
278
299
  end
279
300
 
301
+ # @return [Boolean] whether a MR title includes "RUN ALL RSPEC" or not.
280
302
  def run_all_rspec_mr?
281
303
  Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
282
304
  end
283
305
 
306
+ # @return [Boolean] whether a MR title includes "RUN AS-IF-FOSS" or not.
284
307
  def run_as_if_foss_mr?
285
308
  Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
286
309
  end
287
310
 
311
+ # @return [Boolean] whether a MR targets a stable branch or not.
288
312
  def stable_branch?
289
313
  /\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
290
314
  end
291
315
 
316
+ # Whether a MR has any database-scoped labels (i.e. +"database::*"+) set or not.
317
+ #
318
+ # @return [Boolean]
319
+ def has_database_scoped_labels?
320
+ mr_labels.any? { |label| label.start_with?("database::") }
321
+ end
322
+
323
+ # @return [Boolean] whether a MR has any CI-related changes (i.e. +".gitlab-ci.yml"+ or +".gitlab/ci/*"+) or not.
324
+ def has_ci_changes?
325
+ changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
326
+ end
327
+
328
+ # @param labels [Array<String>] An array of labels.
329
+ #
330
+ # @return [Boolean] whether a MR has the given +labels+ set or not.
292
331
  def mr_has_labels?(*labels)
293
332
  labels = labels.flatten.uniq
294
333
 
295
334
  (labels & mr_labels) == labels
296
335
  end
297
336
 
337
+ # @param labels [Array<String>] An array of labels.
338
+ # @param sep [String] A separator.
339
+ #
340
+ # @example
341
+ # labels_list(["foo", "bar baz"], sep: "; ")
342
+ # # => '~"foo"; ~"bar baz"'
343
+ #
344
+ # @return [String] the list of +labels+ ready for being used in a Markdown comment, separated by +sep+.
298
345
  def labels_list(labels, sep: ", ")
299
346
  labels.map { |label| %Q{~"#{label}"} }.join(sep)
300
347
  end
301
348
 
349
+ # @deprecated Use {#quick_action_label} instead.
302
350
  def prepare_labels_for_mr(labels)
351
+ quick_action_label(labels)
352
+ end
353
+
354
+ # @param labels [Array<String>] An array of labels.
355
+ #
356
+ # @example
357
+ # quick_action_label(["foo", "bar baz"])
358
+ # # => '/label ~"foo" ~"bar baz"'
359
+ #
360
+ # @return [String] a quick action to set the +given+ labels. Returns +""+ if +labels+ is empty.
361
+ def quick_action_label(labels)
303
362
  return "" unless labels.any?
304
363
 
305
364
  "/label #{labels_list(labels, sep: " ")}"
306
365
  end
307
366
 
367
+ # @param regex [Regexp] A Regexp to match against.
368
+ #
369
+ # @return [Array<String>] changed files matching the given +regex+.
308
370
  def changed_files(regex)
309
371
  all_changed_files.grep(regex)
310
372
  end
311
373
 
312
- def has_database_scoped_labels?(current_mr_labels)
313
- current_mr_labels.any? { |label| label.start_with?("database::") }
374
+ # @return [Array<String>] the group labels (i.e. +"group::*"+) set on the MR.
375
+ def group_label
376
+ mr_labels.find { |label| label.start_with?("group::") }
314
377
  end
315
378
 
316
- def has_ci_changes?
317
- changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
379
+ private
380
+
381
+ # @return [Danger::RequestSources::GitLab, nil] the +gitlab+ helper, or +nil+ when it's not available.
382
+ def gitlab_helper
383
+ # Unfortunately the following does not work:
384
+ # - respond_to?(:gitlab)
385
+ # - respond_to?(:gitlab, true)
386
+ gitlab
387
+ rescue NoMethodError
388
+ nil
318
389
  end
319
390
 
320
- def group_label(labels)
321
- labels.find { |label| label.start_with?("group::") }
391
+ # @param filename [String] A filename for which we want the diff.
392
+ #
393
+ # @return [String] the raw diff as a string for the given +filename+.
394
+ def diff_for_file(filename)
395
+ if changes_from_api
396
+ changes_hash = changes_from_api.find { |file| file["new_path"] == filename }
397
+ changes_hash["diff"] if changes_hash
398
+ else
399
+ git.diff_for_file(filename)&.patch
400
+ end
401
+ end
402
+
403
+ # Fetches MR changes from the API instead of Git (default).
404
+ #
405
+ # @return [Array<Hash>, nil]
406
+ def changes_from_api
407
+ return nil unless ci?
408
+ return nil if defined?(@force_changes_from_git)
409
+
410
+ @changes_from_api ||= gitlab_helper.mr_changes.to_h["changes"]
411
+ rescue
412
+ # Fallback to the Git strategy in any case
413
+ @force_changes_from_git = true
414
+ nil
322
415
  end
323
416
  end
324
417
  end
@@ -16,13 +16,21 @@ module Danger
16
16
  }.freeze
17
17
 
18
18
  Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role, :timezone_experiment)
19
+ HTTPError = Class.new(StandardError)
19
20
 
21
+ # Finds the +Gitlab::Dangerfiles::Teammate+ object whose username matches the MR author username.
22
+ #
23
+ # @return [Gitlab::Dangerfiles::Teammate]
20
24
  def team_mr_author
21
- team.find { |person| person.username == helper.mr_author }
25
+ company_members.find { |person| person.username == helper.mr_author }
22
26
  end
23
27
 
24
28
  # Assigns GitLab team members to be reviewer and maintainer
25
- # for each change category that a Merge Request contains.
29
+ # for the given +categories+.
30
+ #
31
+ # @param project [String] A project path.
32
+ # @param categories [Array<Symbol>] An array of categories symbols.
33
+ # @param timezone_experiment [Boolean] Whether to select reviewers based in timezone or not.
26
34
  #
27
35
  # @return [Array<Spin>]
28
36
  def spin(project, categories = [nil], timezone_experiment: false)
@@ -33,6 +41,7 @@ module Danger
33
41
  end
34
42
 
35
43
  backend_spin = spins.find { |spin| spin.category == :backend }
44
+ frontend_spin = spins.find { |spin| spin.category == :frontend }
36
45
 
37
46
  spins.each do |spin|
38
47
  including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(spin.category, timezone_experiment)
@@ -61,64 +70,32 @@ module Danger
61
70
  end
62
71
  when :product_intelligence
63
72
  spin.optional_role = :maintainer
73
+
74
+ if spin.maintainer.nil?
75
+ # Fetch an already picked maintainer, or pick one otherwise
76
+ spin.maintainer = backend_spin&.maintainer || frontend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
77
+ end
64
78
  end
65
79
  end
66
80
 
67
81
  spins
68
82
  end
69
83
 
70
- # Looks up the current list of GitLab team members and parses it into a
71
- # useful form
72
- #
73
- # @return [Array<Teammate>]
74
- def team
75
- @team ||= begin
76
- data = helper.http_get_json(ROULETTE_DATA_URL)
77
- data.map { |hash| Gitlab::Dangerfiles::Teammate.new(hash) }
78
- rescue JSON::ParserError
79
- raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
80
- end
81
- end
82
-
83
- # Like +team+, but only returns teammates in the current project, based on
84
- # project_name.
85
- #
86
- # @return [Array<Teammate>]
87
- def project_team(project_name)
88
- team.select { |member| member.in_project?(project_name) }
89
- rescue => err
90
- warn("Reviewer roulette failed to load team data: #{err.message}")
91
- []
92
- end
93
-
94
- # Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
95
- # selection will change on next spin
96
- # @param [Array<Teammate>] people
97
- def spin_for_person(people, random:, timezone_experiment: false)
98
- shuffled_people = people.shuffle(random: random)
99
-
100
- if timezone_experiment
101
- shuffled_people.find(&method(:valid_person_with_timezone?))
102
- else
103
- shuffled_people.find(&method(:valid_person?))
104
- end
105
- end
106
-
107
84
  private
108
85
 
109
- # @param [Teammate] person
86
+ # @param [Gitlab::Dangerfiles::Teammate] person
110
87
  # @return [Boolean]
111
88
  def valid_person?(person)
112
89
  !mr_author?(person) && person.available
113
90
  end
114
91
 
115
- # @param [Teammate] person
92
+ # @param [Gitlab::Dangerfiles::Teammate] person
116
93
  # @return [Boolean]
117
94
  def valid_person_with_timezone?(person)
118
95
  valid_person?(person) && HOURS_WHEN_PERSON_CAN_BE_PICKED.cover?(person.local_hour)
119
96
  end
120
97
 
121
- # @param [Teammate] person
98
+ # @param [Gitlab::Dangerfiles::Teammate] person
122
99
  # @return [Boolean]
123
100
  def mr_author?(person)
124
101
  person.username == helper.mr_author
@@ -134,6 +111,22 @@ module Danger
134
111
  end
135
112
  end
136
113
 
114
+ # Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
115
+ # selection will change on next spin.
116
+ #
117
+ # @param [Array<Gitlab::Dangerfiles::Teammate>] people
118
+ #
119
+ # @return [Gitlab::Dangerfiles::Teammate]
120
+ def spin_for_person(people, random:, timezone_experiment: false)
121
+ shuffled_people = people.shuffle(random: random)
122
+
123
+ if timezone_experiment
124
+ shuffled_people.find(&method(:valid_person_with_timezone?))
125
+ else
126
+ shuffled_people.find(&method(:valid_person?))
127
+ end
128
+ end
129
+
137
130
  def spin_for_category(project, category, timezone_experiment: false)
138
131
  team = project_team(project)
139
132
  reviewers, traintainers, maintainers =
@@ -151,5 +144,44 @@ module Danger
151
144
 
152
145
  Spin.new(category, reviewer, maintainer, false, timezone_experiment)
153
146
  end
147
+
148
+ # Fetches the given +url+ and parse its response as JSON.
149
+ #
150
+ # @param [String] url
151
+ #
152
+ # @return [Hash, Array]
153
+ def http_get_json(url)
154
+ rsp = Net::HTTP.get_response(URI.parse(url))
155
+
156
+ unless rsp.is_a?(Net::HTTPOK)
157
+ raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
158
+ end
159
+
160
+ JSON.parse(rsp.body)
161
+ end
162
+
163
+ # Looks up the current list of GitLab team members and parses it into a
164
+ # useful form.
165
+ #
166
+ # @return [Array<Gitlab::Dangerfiles::Teammate>]
167
+ def company_members
168
+ @company_members ||= begin
169
+ data = http_get_json(ROULETTE_DATA_URL)
170
+ data.map { |hash| Gitlab::Dangerfiles::Teammate.new(hash) }
171
+ rescue JSON::ParserError
172
+ raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
173
+ end
174
+ end
175
+
176
+ # Like +team+, but only returns teammates in the current project, based on
177
+ # project_name.
178
+ #
179
+ # @return [Array<Gitlab::Dangerfiles::Teammate>]
180
+ def project_team(project_name)
181
+ company_members.select { |member| member.in_project?(project_name) }
182
+ rescue => err
183
+ warn("Reviewer roulette failed to load team data: #{err.message}")
184
+ []
185
+ end
154
186
  end
155
187
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ thresholds = helper.config.code_size_thresholds
4
+ lines_changed = git.lines_of_code
5
+
6
+ if lines_changed > thresholds[:high]
7
+ warn "This merge request is definitely too big (#{lines_changed} lines changed), please split it into multiple merge requests."
8
+ elsif lines_changed > thresholds[:medium]
9
+ warn "This merge request is quite big (#{lines_changed} lines changed), please consider splitting it into multiple merge requests."
10
+ end
@@ -2,8 +2,87 @@ 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/plugins/*.rb", __dir__))
5
+ RULES_DIR = File.expand_path("../danger/rules", __dir__)
6
+ EXISTING_RULES = Dir.glob(File.join(RULES_DIR, "*")).each_with_object([]) do |path, memo|
7
+ if File.directory?(path)
8
+ memo << File.basename(path)
9
+ end
10
+ end
11
+ LOCAL_RULES = %w[
12
+ changes_size
13
+ ].freeze
14
+ CI_ONLY_RULES = %w[
15
+ ].freeze
16
+
17
+ # This class provides utility methods to import plugins and dangerfiles easily.
18
+ class Engine
19
+ # @param dangerfile [Danger::Dangerfile] A +Danger::Dangerfile+ object.
20
+ #
21
+ # @example
22
+ # # In your main Dangerfile:
23
+ # dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
24
+ #
25
+ # @return [Gitlab::Dangerfiles::Engine]
26
+ def initialize(dangerfile)
27
+ @dangerfile = dangerfile
28
+ end
29
+
30
+ # Import all available plugins.
31
+ #
32
+ # @example
33
+ # # In your main Dangerfile:
34
+ # dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
35
+ #
36
+ # # Import all plugins
37
+ # dangerfiles.import_plugins
38
+ def import_plugins
39
+ danger_plugin.import_plugin(File.expand_path("../danger/plugins/*.rb", __dir__))
40
+ end
41
+
42
+ # Import available Dangerfiles.
43
+ #
44
+ # @param rules [Symbol, Array<String>] Can be either +:all+ (default) to import all rules,
45
+ # or an array of rules.
46
+ # Available rules are: +changes_size+.
47
+ #
48
+ # @example
49
+ # # In your main Dangerfile:
50
+ # dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
51
+ #
52
+ # # Import all rules
53
+ # dangerfiles.import_dangerfiles
54
+ #
55
+ # # Or import only a subset of rules
56
+ # dangerfiles.import_dangerfiles(rules: %w[changes_size])
57
+ def import_dangerfiles(rules: :all)
58
+ filtered_rules(rules).each do |rule|
59
+ danger_plugin.import_dangerfile(path: File.join(RULES_DIR, rule))
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :dangerfile
66
+
67
+ def allowed_rules
68
+ return LOCAL_RULES unless helper_plugin.respond_to?(:ci?)
69
+
70
+ helper_plugin.ci? ? LOCAL_RULES | CI_ONLY_RULES : LOCAL_RULES
71
+ end
72
+
73
+ def filtered_rules(rules)
74
+ rules = EXISTING_RULES if rules == :all
75
+
76
+ Array(rules).map(&:to_s) & EXISTING_RULES & allowed_rules
77
+ end
78
+
79
+ def danger_plugin
80
+ @danger_plugin ||= dangerfile.plugins[Danger::DangerfileDangerPlugin]
81
+ end
82
+
83
+ def helper_plugin
84
+ @helper_plugin ||= dangerfile.plugins[Danger::Helper]
85
+ end
7
86
  end
8
87
  end
9
88
  end
@@ -4,41 +4,63 @@ require_relative "title_linting"
4
4
 
5
5
  module Gitlab
6
6
  module Dangerfiles
7
+ # @!attribute file
8
+ # @return [String] the file name that's changed.
9
+ # @!attribute change_type
10
+ # @return [Symbol] the type of change (+:added+, +:modified+, +:deleted+, +:renamed_before+, +:renamed_after+).
11
+ # @!attribute category
12
+ # @return [Symbol] the category of the change.
13
+ # This is defined by consumers of the gem through +helper.changes_by_category+ or +helper.changes+.
7
14
  Change = Struct.new(:file, :change_type, :category)
8
15
 
9
16
  class Changes < ::SimpleDelegator
17
+ # Return an +Gitlab::Dangerfiles::Changes+ object with only the changes for the added files.
18
+ #
19
+ # @return [Gitlab::Dangerfiles::Changes]
10
20
  def added
11
21
  select_by_change_type(:added)
12
22
  end
13
23
 
24
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the modified files.
14
25
  def modified
15
26
  select_by_change_type(:modified)
16
27
  end
17
28
 
29
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the deleted files.
18
30
  def deleted
19
31
  select_by_change_type(:deleted)
20
32
  end
21
33
 
34
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the renamed files (before the rename).
22
35
  def renamed_before
23
36
  select_by_change_type(:renamed_before)
24
37
  end
25
38
 
39
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the renamed files (after the rename).
26
40
  def renamed_after
27
41
  select_by_change_type(:renamed_after)
28
42
  end
29
43
 
44
+ # @param category [Symbol] A category of change.
45
+ #
46
+ # @return [Boolean] whether there are any change for the given +category+.
30
47
  def has_category?(category)
31
48
  any? { |change| change.category == category }
32
49
  end
33
50
 
51
+ # @param category [Symbol] a category of change.
52
+ #
53
+ # @return [Gitlab::Dangerfiles::Changes] changes for the given +category+.
34
54
  def by_category(category)
35
55
  Changes.new(select { |change| change.category == category })
36
56
  end
37
57
 
58
+ # @return [Array<Symbol>] an array of the unique categories of changes.
38
59
  def categories
39
60
  map(&:category).uniq
40
61
  end
41
62
 
63
+ # @return [Array<String>] an array of the changed files.
42
64
  def files
43
65
  map(&:file)
44
66
  end
@@ -15,7 +15,7 @@ module Gitlab
15
15
  {
16
16
  separator_missing: "The commit subject and body must be separated by a blank line",
17
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",
18
+ "at least #{MAX_CHANGED_FILES_IN_COMMIT} files should describe these changes in the commit body",
19
19
  details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
20
20
  message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
21
21
  "to the commit message, and are displayed as plain text outside of GitLab",
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Dangerfiles
5
+ class Config
6
+ # @!attribute code_size_thresholds
7
+ # @return [{ high: Integer, medium: Integer }] a hash of the form +{ high: 42, medium: 12 }+ where +:high+ is the lines changed threshold which triggers an error, and +:medium+ is the lines changed threshold which triggers a warning. Also, see +DEFAULT_CHANGES_SIZE_THRESHOLDS+ for the format of the hash.
8
+ attr_accessor :code_size_thresholds
9
+
10
+ DEFAULT_CHANGES_SIZE_THRESHOLDS = { high: 2_000, medium: 500 }.freeze
11
+
12
+ def initialize
13
+ @code_size_thresholds = DEFAULT_CHANGES_SIZE_THRESHOLDS
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,6 +4,7 @@ require "json"
4
4
 
5
5
  module Gitlab
6
6
  module Dangerfiles
7
+ # @api private
7
8
  class EmojiChecker
8
9
  DIGESTS = File.expand_path("../../../fixtures/emojis/digests.json", __dir__)
9
10
  ALIASES = File.expand_path("../../../fixtures/emojis/aliases.json", __dir__)
@@ -101,11 +101,6 @@ module Gitlab
101
101
  return true if capabilities(project).include?("#{kind} engineering_productivity")
102
102
 
103
103
  capabilities(project).include?("#{kind} backend")
104
- when :product_intelligence
105
- return false unless role[/Engineer, Product Intelligence/]
106
-
107
- # any reviewer of project from Product Intelligence team can review MR
108
- kind == :reviewer && capabilities(project).any?
109
104
  when nil
110
105
  capabilities(project).include?("#{kind}")
111
106
  else
@@ -1,5 +1,5 @@
1
1
  module Gitlab
2
2
  module Dangerfiles
3
- VERSION = "1.1.1"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Gitlab
4
4
  module Dangerfiles
5
+ # @api private
5
6
  module Weightage
6
7
  CAPACITY_MULTIPLIER = 2 # change this number to change what it means to be a reduced capacity reviewer 1/this number
7
8
  BASE_REVIEWER_WEIGHT = 1
@@ -5,6 +5,7 @@ require_relative "../weightage"
5
5
  module Gitlab
6
6
  module Dangerfiles
7
7
  module Weightage
8
+ # @api private
8
9
  class Maintainers
9
10
  def initialize(maintainers)
10
11
  @maintainers = maintainers
@@ -18,6 +18,7 @@ module Gitlab
18
18
  # | hungry traintainer | 8 |
19
19
  # +------------------------------+--------------------------------+
20
20
  #
21
+ # @api private
21
22
  class Reviewers
22
23
  DEFAULT_REVIEWER_WEIGHT = Gitlab::Dangerfiles::Weightage::CAPACITY_MULTIPLIER * Gitlab::Dangerfiles::Weightage::BASE_REVIEWER_WEIGHT
23
24
  TRAINTAINER_WEIGHT = 3
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: 1.1.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - Rémy Coutable
7
+ - GitLab
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-01 00:00:00.000000000 Z
11
+ date: 2021-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: danger-gitlab
@@ -110,7 +110,7 @@ dependencies:
110
110
  version: '0'
111
111
  description: This gem provides common Dangerfile and plugins for GitLab projects.
112
112
  email:
113
- - remy@rymai.me
113
+ - gitlab_rubygems@gitlab.com
114
114
  executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
@@ -119,6 +119,7 @@ files:
119
119
  - ".gitlab-ci.yml"
120
120
  - ".gitlab/merge_request_templates/Release.md"
121
121
  - ".rspec"
122
+ - ".yardopts"
122
123
  - CODE_OF_CONDUCT.md
123
124
  - Gemfile
124
125
  - Guardfile
@@ -132,12 +133,14 @@ files:
132
133
  - gitlab-dangerfiles.gemspec
133
134
  - lib/danger/plugins/helper.rb
134
135
  - lib/danger/plugins/roulette.rb
136
+ - lib/danger/rules/changes_size/Dangerfile
135
137
  - lib/gitlab-dangerfiles.rb
136
138
  - lib/gitlab/Dangerfile
137
139
  - lib/gitlab/dangerfiles.rb
138
140
  - lib/gitlab/dangerfiles/base_linter.rb
139
141
  - lib/gitlab/dangerfiles/changes.rb
140
142
  - lib/gitlab/dangerfiles/commit_linter.rb
143
+ - lib/gitlab/dangerfiles/config.rb
141
144
  - lib/gitlab/dangerfiles/emoji_checker.rb
142
145
  - lib/gitlab/dangerfiles/merge_request_linter.rb
143
146
  - lib/gitlab/dangerfiles/spec_helper.rb
@@ -154,7 +157,7 @@ metadata:
154
157
  allowed_push_host: https://rubygems.org
155
158
  homepage_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles
156
159
  source_code_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles
157
- changelog_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles
160
+ changelog_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles/-/releases
158
161
  post_install_message:
159
162
  rdoc_options: []
160
163
  require_paths:
@@ -170,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
173
  - !ruby/object:Gem::Version
171
174
  version: '0'
172
175
  requirements: []
173
- rubygems_version: 3.1.4
176
+ rubygems_version: 3.1.6
174
177
  signing_key:
175
178
  specification_version: 4
176
179
  summary: This gem provides common Dangerfile and plugins for GitLab projects.