gitlab-dangerfiles 1.0.0 → 2.1.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: f9f8e727ceb4fd8d6a8163b5c2637cb117426e30d3002ae5772b7031c83bc98a
4
- data.tar.gz: 650de232d49a5cb7a022ed53e4607ddc8941ba0615e5cc73214da44d2e838967
3
+ metadata.gz: 2e4bc738b166a81e3e3ebd009c67eb6205bd40155f624d62ee3b8b4e2a4c3203
4
+ data.tar.gz: 7b0be83cf24f080a0d7d468db5656526de8a3937206624dcbba2cb59d44a5911
5
5
  SHA512:
6
- metadata.gz: fe6821b98e683ead18db025365f4efe6821ca92773f8baacfec88ab12131ab1597d735a1262d2cf65928d24b9b72182a1db2d2cb3435edd6c52248eb029ecd87
7
- data.tar.gz: e984610303236d5d6f964ed04c61f924560cf2e29b04c1b485434f0755dc9dd676d486b879972f02b4463da4d499fd070e2a8629bfb570cee93e88b0e4eaf531
6
+ metadata.gz: bae09716c429d549b9624223997b20b3f0c75470aa72ccb6293f145b294bbbf97bd04c7e4ce0aeb791898b9eb3ba23f1b0d87b2e3b77d667d9c07de6f2d63749
7
+ data.tar.gz: 2c2a4745c78288a38dec0e93f0193b5507b46680a7d029f34e1f8e90400e882ab17ef144d00f2085be06d91dbc4685df7d36af7af47bf029a94f38916f6c4586
data/.gitignore CHANGED
@@ -9,6 +9,7 @@
9
9
  /pkg/
10
10
  /spec/reports/
11
11
  /tmp/
12
+ /doc/
12
13
 
13
14
  # rspec failure tracking
14
15
  .rspec_status
data/.gitlab-ci.yml CHANGED
@@ -11,7 +11,7 @@ workflow:
11
11
  # For tags, create a pipeline.
12
12
  - if: '$CI_COMMIT_TAG'
13
13
 
14
- default:
14
+ .default:
15
15
  image: ruby:2.7
16
16
  tags:
17
17
  - gitlab-org
@@ -29,15 +29,50 @@ default:
29
29
  policy: pull
30
30
 
31
31
  test:rspec:
32
+ extends: .default
32
33
  stage: test
33
34
  script:
34
35
  - bundle exec rspec
35
36
 
36
37
  test:rufo:
38
+ extends: .default
37
39
  stage: test
38
40
  script:
39
41
  - bundle exec rufo --check .
40
42
 
41
43
  include:
44
+ - template: Security/Dependency-Scanning.gitlab-ci.yml
45
+ - template: Security/License-Scanning.gitlab-ci.yml
46
+ - template: Security/SAST.gitlab-ci.yml
47
+ - template: Security/Secret-Detection.gitlab-ci.yml
42
48
  - project: 'gitlab-org/quality/pipeline-common'
43
49
  file: '/ci/gem-release.yml'
50
+
51
+ # run security jobs on MRs
52
+ # see: https://gitlab.com/gitlab-org/gitlab/-/issues/218444#note_478761991
53
+
54
+ brakeman-sast:
55
+ rules:
56
+ - if: '$CI_MERGE_REQUEST_IID'
57
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
58
+
59
+ gemnasium-dependency_scanning:
60
+ rules:
61
+ - if: '$CI_MERGE_REQUEST_IID'
62
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
63
+
64
+ bundler-audit-dependency_scanning:
65
+ rules:
66
+ - if: '$CI_MERGE_REQUEST_IID'
67
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
68
+
69
+ license_scanning:
70
+ rules:
71
+ - if: '$CI_MERGE_REQUEST_IID'
72
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
73
+
74
+ secret_detection:
75
+ rules:
76
+ - if: '$CI_MERGE_REQUEST_IID'
77
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
78
+
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,68 @@ 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.
66
+
67
+ #### `commit_messages`
68
+
69
+ ##### Available configurations
70
+
71
+ - `max_commits_count`: The maximum number of allowed non-squashed/non-fixup commits for a given MR.
72
+ A warning is triggered if the MR has more commits.
73
+
74
+ ## Documentation
75
+
76
+ Latest documentation can be found at <https://www.rubydoc.info/gems/gitlab-dangerfiles>.
26
77
 
27
78
  ## Development
28
79
 
@@ -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.
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ require_relative "../../gitlab/dangerfiles/changes"
7
+ require_relative "../../gitlab/dangerfiles/config"
8
+ require_relative "../../gitlab/dangerfiles/teammate"
9
+ require_relative "../../gitlab/dangerfiles/title_linting"
10
+
11
+ module Danger
12
+ # Common helper functions for our danger scripts.
13
+ class Helper < Danger::Plugin
14
+ RELEASE_TOOLS_BOT = "gitlab-release-tools-bot"
15
+ CATEGORY_LABELS = {
16
+ docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
17
+ none: "",
18
+ qa: "~QA",
19
+ test: "~test ~Quality for `spec/features/*`",
20
+ engineering_productivity: '~"Engineering Productivity" for CI, Danger',
21
+ ci_template: '~"ci::templates"',
22
+ product_intelligence: '~"product intelligence"',
23
+ }.freeze
24
+
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
42
+ end
43
+
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
58
+ end
59
+
60
+ # @return [Boolean] whether we're in the CI context or not.
61
+ def ci?
62
+ !gitlab_helper.nil?
63
+ end
64
+
65
+ # @return [Array<String>] a list of filenames added in this MR.
66
+ def added_files
67
+ @added_files ||= if changes_from_api
68
+ changes_from_api.select { |file| file["new_file"] }.map { |file| file["new_path"] }
69
+ else
70
+ git.added_files.to_a
71
+ end
72
+ end
73
+
74
+ # @return [Array<String>] a list of filenames modifier in this MR.
75
+ def modified_files
76
+ @modified_files ||= if changes_from_api
77
+ changes_from_api.select { |file| !file["new_file"] && !file["deleted_file"] && !file["renamed_file"] }.map { |file| file["new_path"] }
78
+ else
79
+ git.modified_files.to_a
80
+ end
81
+ end
82
+
83
+ # @return [Array<String>] a list of filenames renamed in this MR.
84
+ def renamed_files
85
+ @renamed_files ||= if changes_from_api
86
+ changes_from_api.select { |file| file["renamed_file"] }.each_with_object([]) do |file, memo|
87
+ memo << { before: file["old_path"], after: file["new_path"] }
88
+ end
89
+ else
90
+ git.renamed_files.to_a
91
+ end
92
+ end
93
+
94
+ # @return [Array<String>] a list of filenames deleted in this MR.
95
+ def deleted_files
96
+ @deleted_files ||= if changes_from_api
97
+ changes_from_api.select { |file| file["deleted_file"] }.map { |file| file["new_path"] }
98
+ else
99
+ git.deleted_files.to_a
100
+ end
101
+ end
102
+
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:
110
+ #
111
+ # #=> ['new_file.rb', 'modified_file.rb', 'renamed_file_after.rb']
112
+ #
113
+ #
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.
117
+ def all_changed_files
118
+ Set.new
119
+ .merge(added_files)
120
+ .merge(modified_files)
121
+ .merge(renamed_files.map { |x| x[:after] })
122
+ .subtract(renamed_files.map { |x| x[:before] })
123
+ .to_a
124
+ .sort
125
+ end
126
+
127
+ # @param filename [String] A file name for which we want the diff.
128
+ #
129
+ # @example
130
+ # # Considering changing a line in lib/gitlab/usage_data.rb, it will return:
131
+ #
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)
140
+ return [] unless diff
141
+
142
+ diff.split("\n").select { |line| %r{^[+-]}.match?(line) }
143
+ end
144
+
145
+ def release_automation?
146
+ gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
147
+ end
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.
157
+ def markdown_list(items)
158
+ list = items.map { |item| "* `#{item}`" }.join("\n")
159
+
160
+ if items.size > 10
161
+ "\n<details>\n\n#{list}\n\n</details>\n"
162
+ else
163
+ list
164
+ end
165
+ end
166
+
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.
173
+ def changes_by_category(categories)
174
+ all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
175
+ categories_for_file(file, categories).each { |category| hash[category] << file }
176
+ end
177
+ end
178
+
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.
185
+ def changes(categories)
186
+ Gitlab::Dangerfiles::Changes.new([]).tap do |changes|
187
+ added_files.each do |file|
188
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :added, category) }
189
+ end
190
+
191
+ modified_files.each do |file|
192
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :modified, category) }
193
+ end
194
+
195
+ deleted_files.each do |file|
196
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :deleted, category) }
197
+ end
198
+
199
+ renamed_files.map { |x| x[:before] }.each do |file|
200
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_before, category) }
201
+ end
202
+
203
+ renamed_files.map { |x| x[:after] }.each do |file|
204
+ categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_after, category) }
205
+ end
206
+ end
207
+ end
208
+
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+
213
+ #
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)
217
+ _, categories = categories.find do |key, _|
218
+ filename_regex, changes_regex = Array(key)
219
+
220
+ found = filename_regex.match?(filename)
221
+ found &&= changed_lines(filename).any? { |changed_line| changes_regex.match?(changed_line) } if changes_regex
222
+
223
+ found
224
+ end
225
+
226
+ Array(categories || :unknown)
227
+ end
228
+
229
+ # @param category [Symbol] A category.
230
+ #
231
+ # @return [String] the GFM for a category label, making its best guess if it's not
232
+ # a category we know about.
233
+ def label_for_category(category)
234
+ CATEGORY_LABELS.fetch(category, "~#{category}")
235
+ end
236
+
237
+ # @return [String] +""+ when not in the CI context, and the MR IID as a string otherwise.
238
+ def mr_iid
239
+ return "" unless ci?
240
+
241
+ gitlab_helper.mr_json["iid"].to_s
242
+ end
243
+
244
+ # @return [String] +`whoami`+ when not in the CI context, and the MR author username otherwise.
245
+ def mr_author
246
+ return `whoami`.strip unless ci?
247
+
248
+ gitlab_helper.mr_author
249
+ end
250
+
251
+ # @return [String] +""+ when not in the CI context, and the MR title otherwise.
252
+ def mr_title
253
+ return "" unless ci?
254
+
255
+ gitlab_helper.mr_json["title"]
256
+ end
257
+
258
+ # @return [String] +""+ when not in the CI context, and the MR URL otherwise.
259
+ def mr_web_url
260
+ return "" unless ci?
261
+
262
+ gitlab_helper.mr_json["web_url"]
263
+ end
264
+
265
+ # @return [Array<String>] +[]+ when not in the CI context, and the MR labels otherwise.
266
+ def mr_labels
267
+ return [] unless ci?
268
+
269
+ gitlab_helper.mr_labels
270
+ end
271
+
272
+ # @return [String] +`git rev-parse --abbrev-ref HEAD`+ when not in the CI context, and the MR source branch otherwise.
273
+ def mr_source_branch
274
+ return `git rev-parse --abbrev-ref HEAD`.strip unless ci?
275
+
276
+ gitlab_helper.mr_json["source_branch"]
277
+ end
278
+
279
+ # @return [String] +""+ when not in the CI context, and the MR target branch otherwise.
280
+ def mr_target_branch
281
+ return "" unless ci?
282
+
283
+ gitlab_helper.mr_json["target_branch"]
284
+ end
285
+
286
+ # @return [Boolean] +true+ when not in the CI context, and whether the MR is set to be squashed otherwise.
287
+ def squash_mr?
288
+ return true unless ci?
289
+
290
+ gitlab.mr_json["squash"]
291
+ end
292
+
293
+ # @return [Boolean] whether a MR is a Draft or not.
294
+ def draft_mr?
295
+ return false unless ci?
296
+
297
+ gitlab.mr_json["work_in_progress"]
298
+ end
299
+
300
+ # @return [Boolean] whether a MR is opened in the security mirror or not.
301
+ def security_mr?
302
+ mr_web_url.include?("/gitlab-org/security/")
303
+ end
304
+
305
+ # @return [Boolean] whether a MR title includes "cherry-pick" or not.
306
+ def cherry_pick_mr?
307
+ Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
308
+ end
309
+
310
+ # @return [Boolean] whether a MR title includes "RUN ALL RSPEC" or not.
311
+ def run_all_rspec_mr?
312
+ Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
313
+ end
314
+
315
+ # @return [Boolean] whether a MR title includes "RUN AS-IF-FOSS" or not.
316
+ def run_as_if_foss_mr?
317
+ Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
318
+ end
319
+
320
+ # @return [Boolean] whether a MR targets a stable branch or not.
321
+ def stable_branch?
322
+ /\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
323
+ end
324
+
325
+ # Whether a MR has any database-scoped labels (i.e. +"database::*"+) set or not.
326
+ #
327
+ # @return [Boolean]
328
+ def has_database_scoped_labels?
329
+ mr_labels.any? { |label| label.start_with?("database::") }
330
+ end
331
+
332
+ # @return [Boolean] whether a MR has any CI-related changes (i.e. +".gitlab-ci.yml"+ or +".gitlab/ci/*"+) or not.
333
+ def has_ci_changes?
334
+ changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
335
+ end
336
+
337
+ # @param labels [Array<String>] An array of labels.
338
+ #
339
+ # @return [Boolean] whether a MR has the given +labels+ set or not.
340
+ def mr_has_labels?(*labels)
341
+ labels = labels.flatten.uniq
342
+
343
+ (labels & mr_labels) == labels
344
+ end
345
+
346
+ # @param labels [Array<String>] An array of labels.
347
+ # @param sep [String] A separator.
348
+ #
349
+ # @example
350
+ # labels_list(["foo", "bar baz"], sep: "; ")
351
+ # # => '~"foo"; ~"bar baz"'
352
+ #
353
+ # @return [String] the list of +labels+ ready for being used in a Markdown comment, separated by +sep+.
354
+ def labels_list(labels, sep: ", ")
355
+ labels.map { |label| %Q{~"#{label}"} }.join(sep)
356
+ end
357
+
358
+ # @deprecated Use {#quick_action_label} instead.
359
+ def prepare_labels_for_mr(labels)
360
+ quick_action_label(labels)
361
+ end
362
+
363
+ # @param labels [Array<String>] An array of labels.
364
+ #
365
+ # @example
366
+ # quick_action_label(["foo", "bar baz"])
367
+ # # => '/label ~"foo" ~"bar baz"'
368
+ #
369
+ # @return [String] a quick action to set the +given+ labels. Returns +""+ if +labels+ is empty.
370
+ def quick_action_label(labels)
371
+ return "" unless labels.any?
372
+
373
+ "/label #{labels_list(labels, sep: " ")}"
374
+ end
375
+
376
+ # @param regex [Regexp] A Regexp to match against.
377
+ #
378
+ # @return [Array<String>] changed files matching the given +regex+.
379
+ def changed_files(regex)
380
+ all_changed_files.grep(regex)
381
+ end
382
+
383
+ # @return [Array<String>] the group labels (i.e. +"group::*"+) set on the MR.
384
+ def group_label
385
+ mr_labels.find { |label| label.start_with?("group::") }
386
+ end
387
+
388
+ private
389
+
390
+ # @return [Danger::RequestSources::GitLab, nil] the +gitlab+ helper, or +nil+ when it's not available.
391
+ def gitlab_helper
392
+ # Unfortunately the following does not work:
393
+ # - respond_to?(:gitlab)
394
+ # - respond_to?(:gitlab, true)
395
+ gitlab
396
+ rescue NoMethodError
397
+ nil
398
+ end
399
+
400
+ # @param filename [String] A filename for which we want the diff.
401
+ #
402
+ # @return [String] the raw diff as a string for the given +filename+.
403
+ def diff_for_file(filename)
404
+ if changes_from_api
405
+ changes_hash = changes_from_api.find { |file| file["new_path"] == filename }
406
+ changes_hash["diff"] if changes_hash
407
+ else
408
+ git.diff_for_file(filename)&.patch
409
+ end
410
+ end
411
+
412
+ # Fetches MR changes from the API instead of Git (default).
413
+ #
414
+ # @return [Array<Hash>, nil]
415
+ def changes_from_api
416
+ return nil unless ci?
417
+ return nil if defined?(@force_changes_from_git)
418
+
419
+ @changes_from_api ||= gitlab_helper.mr_changes.to_h["changes"]
420
+ rescue
421
+ # Fallback to the Git strategy in any case
422
+ @force_changes_from_git = true
423
+ nil
424
+ end
425
+ end
426
+ end