gitlab-dangerfiles 1.1.1 → 2.0.0

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