gitlab-dangerfiles 0.8.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: e8132453b5b4614eaa87813509a18230d9c94d00cdec5b9e6857b032800092b0
4
- data.tar.gz: 2e8dfe3056a7da31abbde54e9a8d1cbb96e9c9105d7fde5058903cc2617dbcfe
3
+ metadata.gz: 0d3407bad03ff3220a55329e625c1fd5a962b4ffe29b4ab1da68a7e998761024
4
+ data.tar.gz: a9f31b18452500fa6e437a847b1d726cbbe2c47a1912c5e40fd18196ca3716fa
5
5
  SHA512:
6
- metadata.gz: 8d1020a4fe7e5bcce74d8c03127373ae1a97ecf7ff57a981167cb6b8b733a17d5aef4b4c40fb31b88e996f8fe5830ecd779c49dc89609c3180f8ccf558a99028
7
- data.tar.gz: 0d13715735e7bdded2a526575e276f21e11c11fb57f2fdcecdceb9051143f1995e2527cb4a57d9427aa3d8d2ced85d13775d96819d80610ae555f3108b5e508d
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.
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.add_dependency "danger"
30
+ spec.add_dependency "danger-gitlab"
31
31
 
32
32
  spec.add_development_dependency "rspec", "~> 3.0"
33
33
  spec.add_development_dependency "rspec-parameterized"
@@ -0,0 +1,417 @@
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] whether a MR is a Draft or not.
287
+ def draft_mr?
288
+ Gitlab::Dangerfiles::TitleLinting.has_draft_flag?(mr_title)
289
+ end
290
+
291
+ # @return [Boolean] whether a MR is opened in the security mirror or not.
292
+ def security_mr?
293
+ mr_web_url.include?("/gitlab-org/security/")
294
+ end
295
+
296
+ # @return [Boolean] whether a MR title includes "cherry-pick" or not.
297
+ def cherry_pick_mr?
298
+ Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
299
+ end
300
+
301
+ # @return [Boolean] whether a MR title includes "RUN ALL RSPEC" or not.
302
+ def run_all_rspec_mr?
303
+ Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
304
+ end
305
+
306
+ # @return [Boolean] whether a MR title includes "RUN AS-IF-FOSS" or not.
307
+ def run_as_if_foss_mr?
308
+ Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
309
+ end
310
+
311
+ # @return [Boolean] whether a MR targets a stable branch or not.
312
+ def stable_branch?
313
+ /\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
314
+ end
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.
331
+ def mr_has_labels?(*labels)
332
+ labels = labels.flatten.uniq
333
+
334
+ (labels & mr_labels) == labels
335
+ end
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+.
345
+ def labels_list(labels, sep: ", ")
346
+ labels.map { |label| %Q{~"#{label}"} }.join(sep)
347
+ end
348
+
349
+ # @deprecated Use {#quick_action_label} instead.
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)
362
+ return "" unless labels.any?
363
+
364
+ "/label #{labels_list(labels, sep: " ")}"
365
+ end
366
+
367
+ # @param regex [Regexp] A Regexp to match against.
368
+ #
369
+ # @return [Array<String>] changed files matching the given +regex+.
370
+ def changed_files(regex)
371
+ all_changed_files.grep(regex)
372
+ end
373
+
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::") }
377
+ end
378
+
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
389
+ end
390
+
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
415
+ end
416
+ end
417
+ end