dependabot-common 0.95.1 → 0.95.2

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot.rb +4 -0
  3. data/lib/dependabot/clients/bitbucket.rb +105 -0
  4. data/lib/dependabot/clients/github_with_retries.rb +121 -0
  5. data/lib/dependabot/clients/gitlab.rb +72 -0
  6. data/lib/dependabot/dependency.rb +115 -0
  7. data/lib/dependabot/dependency_file.rb +60 -0
  8. data/lib/dependabot/errors.rb +179 -0
  9. data/lib/dependabot/file_fetchers.rb +18 -0
  10. data/lib/dependabot/file_fetchers/README.md +65 -0
  11. data/lib/dependabot/file_fetchers/base.rb +368 -0
  12. data/lib/dependabot/file_parsers.rb +18 -0
  13. data/lib/dependabot/file_parsers/README.md +45 -0
  14. data/lib/dependabot/file_parsers/base.rb +31 -0
  15. data/lib/dependabot/file_parsers/base/dependency_set.rb +77 -0
  16. data/lib/dependabot/file_updaters.rb +18 -0
  17. data/lib/dependabot/file_updaters/README.md +58 -0
  18. data/lib/dependabot/file_updaters/base.rb +52 -0
  19. data/lib/dependabot/git_commit_checker.rb +412 -0
  20. data/lib/dependabot/metadata_finders.rb +18 -0
  21. data/lib/dependabot/metadata_finders/README.md +53 -0
  22. data/lib/dependabot/metadata_finders/base.rb +117 -0
  23. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +321 -0
  24. data/lib/dependabot/metadata_finders/base/changelog_pruner.rb +177 -0
  25. data/lib/dependabot/metadata_finders/base/commits_finder.rb +221 -0
  26. data/lib/dependabot/metadata_finders/base/release_finder.rb +255 -0
  27. data/lib/dependabot/pull_request_creator.rb +155 -0
  28. data/lib/dependabot/pull_request_creator/branch_namer.rb +170 -0
  29. data/lib/dependabot/pull_request_creator/commit_signer.rb +63 -0
  30. data/lib/dependabot/pull_request_creator/github.rb +277 -0
  31. data/lib/dependabot/pull_request_creator/gitlab.rb +162 -0
  32. data/lib/dependabot/pull_request_creator/labeler.rb +373 -0
  33. data/lib/dependabot/pull_request_creator/message_builder.rb +906 -0
  34. data/lib/dependabot/pull_request_updater.rb +43 -0
  35. data/lib/dependabot/pull_request_updater/github.rb +165 -0
  36. data/lib/dependabot/shared_helpers.rb +224 -0
  37. data/lib/dependabot/source.rb +120 -0
  38. data/lib/dependabot/update_checkers.rb +18 -0
  39. data/lib/dependabot/update_checkers/README.md +67 -0
  40. data/lib/dependabot/update_checkers/base.rb +220 -0
  41. data/lib/dependabot/utils.rb +33 -0
  42. data/lib/dependabot/version.rb +5 -0
  43. data/lib/rubygems_version_patch.rb +14 -0
  44. metadata +44 -2
@@ -0,0 +1,906 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "dependabot/clients/github_with_retries"
5
+ require "dependabot/clients/gitlab"
6
+ require "dependabot/metadata_finders"
7
+ require "dependabot/pull_request_creator"
8
+
9
+ # rubocop:disable Metrics/ClassLength
10
+ module Dependabot
11
+ class PullRequestCreator
12
+ class MessageBuilder
13
+ ANGULAR_PREFIXES = %w(build chore ci docs feat fix perf refactor style
14
+ test).freeze
15
+ ESLINT_PREFIXES = %w(Breaking Build Chore Docs Fix New Update
16
+ Upgrade).freeze
17
+ GITMOJI_PREFIXES = %w(art zap fire bug ambulance sparkles memo rocket
18
+ lipstick tada white_check_mark lock apple penguin
19
+ checkered_flag robot green_apple bookmark
20
+ rotating_light construction green_heart arrow_down
21
+ arrow_up pushpin construction_worker
22
+ chart_with_upwards_trend recycle heavy_minus_sign
23
+ whale heavy_plus_sign wrench globe_with_meridians
24
+ pencil2 hankey rewind twisted_rightwards_arrows
25
+ package alien truck page_facing_up boom bento
26
+ ok_hand wheelchair bulb beers speech_balloon
27
+ card_file_box loud_sound mute busts_in_silhouette
28
+ children_crossing building_construction iphone
29
+ clown_face egg see_no_evil camera_flash).freeze
30
+ ISSUE_TAG_REGEX =
31
+ /(?<=[\s(\\]|^)(?<tag>(?:\#|GH-)\d+)(?=[^A-Za-z0-9\-]|$)/.freeze
32
+ GITHUB_REF_REGEX = %r{github\.com/[^/\s]+/[^/\s]+/(?:issue|pull)}.freeze
33
+
34
+ attr_reader :source, :dependencies, :files, :credentials,
35
+ :pr_message_footer, :author_details, :vulnerabilities_fixed
36
+
37
+ def initialize(source:, dependencies:, files:, credentials:,
38
+ pr_message_footer: nil, author_details: nil,
39
+ vulnerabilities_fixed: {})
40
+ @dependencies = dependencies
41
+ @files = files
42
+ @source = source
43
+ @credentials = credentials
44
+ @pr_message_footer = pr_message_footer
45
+ @author_details = author_details
46
+ @vulnerabilities_fixed = vulnerabilities_fixed
47
+ end
48
+
49
+ def pr_name
50
+ return library_pr_name if library?
51
+
52
+ application_pr_name
53
+ end
54
+
55
+ def pr_message
56
+ commit_message_intro + metadata_cascades + prefixed_pr_message_footer
57
+ end
58
+
59
+ def commit_message
60
+ message = commit_subject + "\n\n"
61
+ message += commit_message_intro
62
+ message += metadata_links
63
+ message += "\n\n" + signoff_message if signoff_message
64
+ message
65
+ end
66
+
67
+ private
68
+
69
+ def commit_subject
70
+ subject = pr_name.gsub("⬆️", ":arrow_up:").gsub("🔒", ":lock:")
71
+ return subject unless subject.length > 72
72
+
73
+ subject = subject.gsub(/ from [^\s]*? to [^\s]*/, "")
74
+ return subject unless subject.length > 72
75
+
76
+ subject.split(" in ").first
77
+ end
78
+
79
+ def commit_message_intro
80
+ return requirement_commit_message_intro if library?
81
+
82
+ version_commit_message_intro
83
+ end
84
+
85
+ def prefixed_pr_message_footer
86
+ return "" unless pr_message_footer
87
+
88
+ "\n\n#{pr_message_footer}"
89
+ end
90
+
91
+ def signoff_message
92
+ return unless author_details.is_a?(Hash)
93
+ return unless author_details[:name] && author_details[:email]
94
+
95
+ "Signed-off-by: #{author_details[:name]} <#{author_details[:email]}>"
96
+ end
97
+
98
+ def library_pr_name
99
+ pr_name = pr_name_prefix
100
+
101
+ pr_name +=
102
+ if dependencies.count == 1
103
+ "#{dependencies.first.display_name} requirement "\
104
+ "from #{old_library_requirement(dependencies.first)} "\
105
+ "to #{new_library_requirement(dependencies.first)}"
106
+ else
107
+ names = dependencies.map(&:name)
108
+ "requirements for #{names[0..-2].join(', ')} and #{names[-1]}"
109
+ end
110
+
111
+ return pr_name if files.first.directory == "/"
112
+
113
+ pr_name + " in #{files.first.directory}"
114
+ end
115
+
116
+ # rubocop:disable Metrics/AbcSize
117
+ def application_pr_name
118
+ pr_name = pr_name_prefix
119
+
120
+ pr_name +=
121
+ if dependencies.count == 1
122
+ dependency = dependencies.first
123
+ "#{dependency.display_name} from #{previous_version(dependency)} "\
124
+ "to #{new_version(dependency)}"
125
+ elsif updating_a_property?
126
+ dependency = dependencies.first
127
+ "#{property_name} from #{previous_version(dependency)} "\
128
+ "to #{new_version(dependency)}"
129
+ elsif updating_a_dependency_set?
130
+ dependency = dependencies.first
131
+ "#{dependency_set.fetch(:group)} dependency set "\
132
+ "from #{previous_version(dependency)} "\
133
+ "to #{new_version(dependency)}"
134
+ else
135
+ names = dependencies.map(&:name)
136
+ "#{names[0..-2].join(', ')} and #{names[-1]}"
137
+ end
138
+
139
+ return pr_name if files.first.directory == "/"
140
+
141
+ pr_name + " in #{files.first.directory}"
142
+ end
143
+ # rubocop:enable Metrics/AbcSize
144
+
145
+ def pr_name_prefix
146
+ prefix = commit_prefix.to_s
147
+ prefix += security_prefix if includes_security_fixes?
148
+ prefix + pr_name_first_word
149
+ end
150
+
151
+ def commit_prefix
152
+ # If there is a previous Dependabot commit, and it used a known style,
153
+ # use that as our model for subsequent commits
154
+ case last_dependabot_commit_style
155
+ when :gitmoji then "⬆️ "
156
+ when :contentional_prefix then "#{last_dependabot_commit_prefix}: "
157
+ when :contentional_prefix_with_scope
158
+ scope = dependencies.any?(&:production?) ? "deps" : "deps-dev"
159
+ "#{last_dependabot_commit_prefix}(#{scope}): "
160
+ else
161
+ # Otherwise we need to detect the user's preferred style from the
162
+ # existing commits on their repo
163
+ build_commit_prefix_from_previous_commits
164
+ end
165
+ end
166
+
167
+ def security_prefix
168
+ return "🔒 " if commit_prefix == "⬆️ "
169
+
170
+ capitalize_first_word? ? "[Security] " : "[security] "
171
+ end
172
+
173
+ def pr_name_first_word
174
+ first_word = library? ? "update " : "bump "
175
+ capitalize_first_word? ? first_word.capitalize : first_word
176
+ end
177
+
178
+ def capitalize_first_word?
179
+ case last_dependabot_commit_style
180
+ when :gitmoji then true
181
+ when :contentional_prefix, :contentional_prefix_with_scope
182
+ last_dependabot_commit_message.match?(/: (\[Security\] )?(B|U)/)
183
+ else
184
+ if using_angular_commit_messages? || using_eslint_commit_messages?
185
+ prefixes = ANGULAR_PREFIXES + ESLINT_PREFIXES
186
+ semantic_msgs = recent_commit_messages.select do |message|
187
+ prefixes.any? { |pre| message.match?(/#{pre}[:(]/i) }
188
+ end
189
+
190
+ return true if semantic_msgs.all? { |m| m.match?(/:\s+\[?[A-Z]/) }
191
+ return false if semantic_msgs.all? { |m| m.match?(/:\s+\[?[a-z]/) }
192
+ end
193
+
194
+ !commit_prefix&.match(/^[a-z]/)
195
+ end
196
+ end
197
+
198
+ def build_commit_prefix_from_previous_commits
199
+ if using_angular_commit_messages?
200
+ scope = dependencies.any?(&:production?) ? "deps" : "deps-dev"
201
+ "#{angular_commit_prefix}(#{scope}): "
202
+ elsif using_eslint_commit_messages?
203
+ # https://eslint.org/docs/developer-guide/contributing/pull-requests
204
+ "Upgrade: "
205
+ elsif using_gitmoji_commit_messages?
206
+ "⬆️ "
207
+ end
208
+ end
209
+
210
+ def last_dependabot_commit_style
211
+ return unless (msg = last_dependabot_commit_message)
212
+
213
+ return :gitmoji if msg.start_with?("⬆️")
214
+ return :contentional_prefix if msg.match?(/^(chore|build|upgrade):/i)
215
+ return unless msg.match?(/^(chore|build|upgrade)\(/i)
216
+
217
+ :contentional_prefix_with_scope
218
+ end
219
+
220
+ def last_dependabot_commit_prefix
221
+ last_dependabot_commit_message&.split(/[:(]/)&.first
222
+ end
223
+
224
+ def requirement_commit_message_intro
225
+ msg = "Updates the requirements on "
226
+
227
+ msg +=
228
+ if dependencies.count == 1
229
+ "#{dependency_links.first} "
230
+ else
231
+ "#{dependency_links[0..-2].join(', ')} and #{dependency_links[-1]} "
232
+ end
233
+
234
+ msg + "to permit the latest version."
235
+ end
236
+
237
+ # rubocop:disable Metrics/CyclomaticComplexity
238
+ # rubocop:disable Metrics/PerceivedComplexity
239
+ def version_commit_message_intro
240
+ if dependencies.count > 1 && updating_a_property?
241
+ return multidependency_property_intro
242
+ end
243
+
244
+ if dependencies.count > 1 && updating_a_dependency_set?
245
+ return dependency_set_intro
246
+ end
247
+
248
+ return multidependency_intro if dependencies.count > 1
249
+
250
+ dependency = dependencies.first
251
+ msg = "Bumps #{dependency_links.first} "\
252
+ "from #{previous_version(dependency)} "\
253
+ "to #{new_version(dependency)}."
254
+
255
+ if switching_from_ref_to_release?(dependency)
256
+ msg += " This release includes the previously tagged commit."
257
+ end
258
+
259
+ if vulnerabilities_fixed[dependency.name]&.any?
260
+ msg += " **This update includes security fixes.**"
261
+ end
262
+
263
+ msg
264
+ end
265
+ # rubocop:enable Metrics/CyclomaticComplexity
266
+ # rubocop:enable Metrics/PerceivedComplexity
267
+
268
+ def multidependency_property_intro
269
+ dependency = dependencies.first
270
+
271
+ "Bumps `#{property_name}` "\
272
+ "from #{previous_version(dependency)} "\
273
+ "to #{new_version(dependency)}."
274
+ end
275
+
276
+ def dependency_set_intro
277
+ dependency = dependencies.first
278
+
279
+ "Bumps `#{dependency_set.fetch(:group)}` "\
280
+ "dependency set from #{previous_version(dependency)} "\
281
+ "to #{new_version(dependency)}."
282
+ end
283
+
284
+ def multidependency_intro
285
+ "Bumps #{dependency_links[0..-2].join(', ')} "\
286
+ "and #{dependency_links[-1]}. These "\
287
+ "dependencies needed to be updated together."
288
+ end
289
+
290
+ def updating_a_property?
291
+ dependencies.first.
292
+ requirements.
293
+ any? { |r| r.dig(:metadata, :property_name) }
294
+ end
295
+
296
+ def updating_a_dependency_set?
297
+ dependencies.first.
298
+ requirements.
299
+ any? { |r| r.dig(:metadata, :dependency_set) }
300
+ end
301
+
302
+ def property_name
303
+ @property_name ||= dependencies.first.requirements.
304
+ find { |r| r.dig(:metadata, :property_name) }&.
305
+ dig(:metadata, :property_name)
306
+
307
+ raise "No property name!" unless @property_name
308
+
309
+ @property_name
310
+ end
311
+
312
+ def dependency_set
313
+ @dependency_set ||= dependencies.first.requirements.
314
+ find { |r| r.dig(:metadata, :dependency_set) }&.
315
+ dig(:metadata, :dependency_set)
316
+
317
+ raise "No dependency set!" unless @dependency_set
318
+
319
+ @dependency_set
320
+ end
321
+
322
+ def dependency_links
323
+ dependencies.map do |dependency|
324
+ if source_url(dependency)
325
+ "[#{dependency.display_name}](#{source_url(dependency)})"
326
+ elsif homepage_url(dependency)
327
+ "[#{dependency.display_name}](#{homepage_url(dependency)})"
328
+ else
329
+ dependency.display_name
330
+ end
331
+ end
332
+ end
333
+
334
+ def metadata_links
335
+ if dependencies.count == 1
336
+ return metadata_links_for_dep(dependencies.first)
337
+ end
338
+
339
+ dependencies.map do |dep|
340
+ "\n\nUpdates `#{dep.display_name}` from #{previous_version(dep)} to "\
341
+ "#{new_version(dep)}"\
342
+ "#{metadata_links_for_dep(dep)}"
343
+ end.join
344
+ end
345
+
346
+ def metadata_links_for_dep(dep)
347
+ msg = ""
348
+ msg += "\n- [Release notes](#{releases_url(dep)})" if releases_url(dep)
349
+ msg += "\n- [Changelog](#{changelog_url(dep)})" if changelog_url(dep)
350
+ msg += "\n- [Upgrade guide](#{upgrade_url(dep)})" if upgrade_url(dep)
351
+ msg += "\n- [Commits](#{commits_url(dep)})" if commits_url(dep)
352
+ msg
353
+ end
354
+
355
+ def metadata_cascades
356
+ if dependencies.count == 1
357
+ return metadata_cascades_for_dep(dependencies.first)
358
+ end
359
+
360
+ dependencies.map do |dep|
361
+ msg = "\n\nUpdates `#{dep.display_name}` from "\
362
+ "#{previous_version(dep)} to #{new_version(dep)}"
363
+ if vulnerabilities_fixed[dep.name]&.any?
364
+ msg += ". **This update includes security fixes.**"
365
+ end
366
+ msg + metadata_cascades_for_dep(dep)
367
+ end.join
368
+ end
369
+
370
+ def metadata_cascades_for_dep(dep)
371
+ msg = ""
372
+ msg += vulnerabilities_cascade(dep)
373
+ msg += release_cascade(dep)
374
+ msg += changelog_cascade(dep)
375
+ msg += upgrade_guide_cascade(dep)
376
+ msg += commits_cascade(dep)
377
+ msg += maintainer_changes_cascade(dep)
378
+ msg += "\n<br />" unless msg == ""
379
+ sanitize_links_and_mentions(msg)
380
+ end
381
+
382
+ def vulnerabilities_cascade(dep)
383
+ fixed_vulns = vulnerabilities_fixed[dep.name]
384
+ return "" unless fixed_vulns&.any?
385
+
386
+ msg = "\n<details>\n<summary>Vulnerabilities fixed</summary>\n\n"
387
+ fixed_vulns.each { |v| msg += serialized_vulnerability_details(v) }
388
+ msg += "</details>"
389
+ sanitize_template_tags(msg)
390
+ end
391
+
392
+ def release_cascade(dep)
393
+ return "" unless releases_text(dep) && releases_url(dep)
394
+
395
+ msg = "\n<details>\n<summary>Release notes</summary>\n\n"
396
+ msg += "*Sourced from [#{dep.display_name}'s releases]"\
397
+ "(#{releases_url(dep)}).*\n\n"
398
+ msg +=
399
+ begin
400
+ release_note_lines = releases_text(dep).split("\n").first(50)
401
+ release_note_lines = release_note_lines.map { |line| "> #{line}\n" }
402
+ if release_note_lines.count == 50
403
+ release_note_lines << truncated_line
404
+ end
405
+ release_note_lines.join
406
+ end
407
+ msg += "</details>"
408
+ msg = link_issues(text: msg, dependency: dep)
409
+ msg = fix_relative_links(
410
+ text: msg,
411
+ base_url: source_url(dep) + "/blob/HEAD/"
412
+ )
413
+ sanitize_template_tags(msg)
414
+ end
415
+
416
+ def changelog_cascade(dep)
417
+ return "" unless changelog_url(dep) && changelog_text(dep)
418
+
419
+ msg = "\n<details>\n<summary>Changelog</summary>\n\n"
420
+ msg += "*Sourced from "\
421
+ "[#{dep.display_name}'s changelog](#{changelog_url(dep)}).*\n\n"
422
+ msg +=
423
+ begin
424
+ changelog_lines = changelog_text(dep).split("\n").first(50)
425
+ changelog_lines = changelog_lines.map { |line| "> #{line}\n" }
426
+ changelog_lines << truncated_line if changelog_lines.count == 50
427
+ changelog_lines.join
428
+ end
429
+ msg += "</details>"
430
+ msg = link_issues(text: msg, dependency: dep)
431
+ msg = fix_relative_links(text: msg, base_url: changelog_url(dep))
432
+ sanitize_template_tags(msg)
433
+ end
434
+
435
+ def upgrade_guide_cascade(dep)
436
+ return "" unless upgrade_url(dep) && upgrade_text(dep)
437
+
438
+ msg = "\n<details>\n<summary>Upgrade guide</summary>\n\n"
439
+ msg += "*Sourced from "\
440
+ "[#{dep.display_name}'s upgrade guide]"\
441
+ "(#{upgrade_url(dep)}).*\n\n"
442
+ msg +=
443
+ begin
444
+ upgrade_lines = upgrade_text(dep).split("\n").first(50)
445
+ upgrade_lines = upgrade_lines.map { |line| "> #{line}\n" }
446
+ upgrade_lines << truncated_line if upgrade_lines.count == 50
447
+ upgrade_lines.join
448
+ end
449
+ msg += "</details>"
450
+ msg = link_issues(text: msg, dependency: dep)
451
+ msg = fix_relative_links(text: msg, base_url: upgrade_url(dep))
452
+ sanitize_template_tags(msg)
453
+ end
454
+
455
+ def commits_cascade(dep)
456
+ return "" unless commits_url(dep) && commits(dep)
457
+
458
+ msg = "\n<details>\n<summary>Commits</summary>\n\n"
459
+
460
+ commits(dep).reverse.first(10).each do |commit|
461
+ title = commit[:message].strip.split("\n").first
462
+ title = title.slice(0..76) + "..." if title && title.length > 80
463
+ sha = commit[:sha][0, 7]
464
+ msg += "- [`#{sha}`](#{commit[:html_url]}) #{title}\n"
465
+ end
466
+
467
+ msg +=
468
+ if commits(dep).count > 10
469
+ "- Additional commits viewable in "\
470
+ "[compare view](#{commits_url(dep)})\n"
471
+ else
472
+ "- See full diff in [compare view](#{commits_url(dep)})\n"
473
+ end
474
+
475
+ msg += "</details>"
476
+ msg = link_issues(text: msg, dependency: dep)
477
+ sanitize_template_tags(msg)
478
+ end
479
+
480
+ def maintainer_changes_cascade(dep)
481
+ return "" unless maintainer_changes(dep)
482
+
483
+ msg = "\n<details>\n<summary>Maintainer changes</summary>\n\n"
484
+ msg += maintainer_changes(dep)
485
+ msg + "\n</details>"
486
+ end
487
+
488
+ def serialized_vulnerability_details(details)
489
+ msg = vulnerability_source_line(details)
490
+
491
+ if details["title"]
492
+ msg += "> **#{details['title'].lines.map(&:strip).join(' ')}**\n"
493
+ end
494
+
495
+ if (description = details["description"])
496
+ description.strip.lines.first(20).each { |line| msg += "> #{line}" }
497
+ msg += truncated_line if description.strip.lines.count > 20
498
+ end
499
+
500
+ msg += "\n" unless msg.end_with?("\n")
501
+ msg += "> \n"
502
+ msg += vulnerability_version_range_lines(details)
503
+ msg + "\n"
504
+ end
505
+
506
+ def vulnerability_source_line(details)
507
+ if details["source_url"] && details["source_name"]
508
+ "*Sourced from [#{details['source_name']}]"\
509
+ "(#{details['source_url']}).*\n\n"
510
+ elsif details["source_name"]
511
+ "*Sourced from #{details['source_name']}.*\n\n"
512
+ else
513
+ ""
514
+ end
515
+ end
516
+
517
+ def vulnerability_version_range_lines(details)
518
+ msg = ""
519
+ %w(patched_versions unaffected_versions affected_versions).each do |tp|
520
+ type = tp.split("_").first.capitalize
521
+ next unless details[tp]
522
+
523
+ versions_string = details[tp].any? ? details[tp].join("; ") : "none"
524
+ versions_string = versions_string.gsub(/(?<!\\)~/, '\~')
525
+ msg += "> #{type} versions: #{versions_string}\n"
526
+ end
527
+ msg
528
+ end
529
+
530
+ def truncated_line
531
+ # Tables can spill out of truncated details, so we close them
532
+ "></tr></table> ... (truncated)\n"
533
+ end
534
+
535
+ def releases_url(dependency)
536
+ metadata_finder(dependency).releases_url
537
+ end
538
+
539
+ def releases_text(dependency)
540
+ metadata_finder(dependency).releases_text
541
+ end
542
+
543
+ def changelog_url(dependency)
544
+ metadata_finder(dependency).changelog_url
545
+ end
546
+
547
+ def changelog_text(dependency)
548
+ metadata_finder(dependency).changelog_text
549
+ end
550
+
551
+ def upgrade_url(dependency)
552
+ metadata_finder(dependency).upgrade_guide_url
553
+ end
554
+
555
+ def upgrade_text(dependency)
556
+ metadata_finder(dependency).upgrade_guide_text
557
+ end
558
+
559
+ def commits_url(dependency)
560
+ metadata_finder(dependency).commits_url
561
+ end
562
+
563
+ def commits(dependency)
564
+ metadata_finder(dependency).commits
565
+ end
566
+
567
+ def maintainer_changes(dependency)
568
+ metadata_finder(dependency).maintainer_changes
569
+ end
570
+
571
+ def source_url(dependency)
572
+ metadata_finder(dependency).source_url
573
+ end
574
+
575
+ def homepage_url(dependency)
576
+ metadata_finder(dependency).homepage_url
577
+ end
578
+
579
+ def metadata_finder(dependency)
580
+ @metadata_finder ||= {}
581
+ @metadata_finder[dependency.name] ||=
582
+ MetadataFinders.
583
+ for_package_manager(dependency.package_manager).
584
+ new(dependency: dependency, credentials: credentials)
585
+ end
586
+
587
+ def previous_version(dependency)
588
+ if dependency.previous_version.match?(/^[0-9a-f]{40}$/)
589
+ return previous_ref(dependency) if ref_changed?(dependency)
590
+
591
+ "`#{dependency.previous_version[0..6]}`"
592
+ elsif dependency.version == dependency.previous_version &&
593
+ package_manager == "docker"
594
+ digest =
595
+ dependency.previous_requirements.
596
+ map { |r| r.dig(:source, "digest") || r.dig(:source, :digest) }.
597
+ compact.first
598
+ "`#{digest.split(':').last[0..6]}`"
599
+ else
600
+ dependency.previous_version
601
+ end
602
+ end
603
+
604
+ def new_version(dependency)
605
+ if dependency.version.match?(/^[0-9a-f]{40}$/)
606
+ return new_ref(dependency) if ref_changed?(dependency)
607
+
608
+ "`#{dependency.version[0..6]}`"
609
+ elsif dependency.version == dependency.previous_version &&
610
+ package_manager == "docker"
611
+ digest =
612
+ dependency.requirements.
613
+ map { |r| r.dig(:source, "digest") || r.dig(:source, :digest) }.
614
+ compact.first
615
+ "`#{digest.split(':').last[0..6]}`"
616
+ else
617
+ dependency.version
618
+ end
619
+ end
620
+
621
+ def previous_ref(dependency)
622
+ dependency.previous_requirements.map do |r|
623
+ r.dig(:source, "ref") || r.dig(:source, :ref)
624
+ end.compact.first
625
+ end
626
+
627
+ def new_ref(dependency)
628
+ dependency.requirements.map do |r|
629
+ r.dig(:source, "ref") || r.dig(:source, :ref)
630
+ end.compact.first
631
+ end
632
+
633
+ def old_library_requirement(dependency)
634
+ old_reqs =
635
+ dependency.previous_requirements - dependency.requirements
636
+
637
+ gemspec =
638
+ old_reqs.find { |r| r[:file].match?(%r{^[^/]*\.gemspec$}) }
639
+ return gemspec.fetch(:requirement) if gemspec
640
+
641
+ req = old_reqs.first.fetch(:requirement)
642
+ return req if req
643
+ return previous_ref(dependency) if ref_changed?(dependency)
644
+
645
+ raise "No previous requirement!"
646
+ end
647
+
648
+ def new_library_requirement(dependency)
649
+ updated_reqs =
650
+ dependency.requirements - dependency.previous_requirements
651
+
652
+ gemspec =
653
+ updated_reqs.find { |r| r[:file].match?(%r{^[^/]*\.gemspec$}) }
654
+ return gemspec.fetch(:requirement) if gemspec
655
+
656
+ req = updated_reqs.first.fetch(:requirement)
657
+ return req if req
658
+ return new_ref(dependency) if ref_changed?(dependency)
659
+
660
+ raise "No new requirement!"
661
+ end
662
+
663
+ def link_issues(text:, dependency:)
664
+ text.gsub(ISSUE_TAG_REGEX) do |mention|
665
+ number = mention.tr("#", "").gsub("GH-", "")
666
+ "[#{mention}](#{source_url(dependency)}/issues/#{number})"
667
+ end
668
+ end
669
+
670
+ def fix_relative_links(text:, base_url:)
671
+ text.gsub(/\[.*?\]\([^)]+\)/) do |link|
672
+ next link if link.include?("://")
673
+
674
+ relative_path = link.match(/\((.*?)\)/).captures.last
675
+ base = base_url.split("://").last.gsub(%r{[^/]*$}, "")
676
+ path = File.join(base, relative_path)
677
+ absolute_path =
678
+ base_url.sub(
679
+ %r{(?<=://).*$},
680
+ Pathname.new(path).cleanpath.to_s
681
+ )
682
+ link.gsub(relative_path, absolute_path)
683
+ end
684
+ end
685
+
686
+ def sanitize_links_and_mentions(text)
687
+ text = text.gsub(%r{(?<![A-Za-z0-9\-])@[A-Za-z0-9\-/]+}) do |mention|
688
+ next mention if mention.include?("/")
689
+
690
+ "[**#{mention.tr('@', '')}**]"\
691
+ "(https://github.com/#{mention.tr('@', '')})"
692
+ end
693
+
694
+ text.gsub(GITHUB_REF_REGEX) do |ref|
695
+ ref.gsub("github.com", "github-redirect.dependabot.com")
696
+ end
697
+ end
698
+
699
+ def sanitize_template_tags(text)
700
+ text.gsub(/\<.*?\>/) do |tag|
701
+ tag_contents = tag.match(/\<(.*?)\>/).captures.first.strip
702
+
703
+ # Unclosed calls to template overflow out of the blockquote block,
704
+ # wrecking the rest of our PRs. Other tags don't share this problem.
705
+ next "\\#{tag}" if tag_contents.start_with?("template")
706
+
707
+ tag
708
+ end
709
+ end
710
+
711
+ def ref_changed?(dependency)
712
+ return false unless previous_ref(dependency)
713
+
714
+ previous_ref(dependency) != new_ref(dependency)
715
+ end
716
+
717
+ def library?
718
+ return true if files.map(&:name).any? { |nm| nm.end_with?(".gemspec") }
719
+
720
+ dependencies.none?(&:appears_in_lockfile?)
721
+ end
722
+
723
+ def switching_from_ref_to_release?(dependency)
724
+ return false unless dependency.previous_version.match?(/^[0-9a-f]{40}$/)
725
+
726
+ Gem::Version.correct?(dependency.version)
727
+ end
728
+
729
+ def includes_security_fixes?
730
+ vulnerabilities_fixed.values.flatten.any?
731
+ end
732
+
733
+ def using_angular_commit_messages?
734
+ return false if recent_commit_messages.none?
735
+
736
+ angular_messages = recent_commit_messages.select do |message|
737
+ ANGULAR_PREFIXES.any? { |pre| message.match?(/#{pre}[:(]/i) }
738
+ end
739
+
740
+ # Definitely not using Angular commits if < 30% match angular commits
741
+ if angular_messages.count.to_f / recent_commit_messages.count < 0.3
742
+ return false
743
+ end
744
+
745
+ eslint_only_pres = ESLINT_PREFIXES.map(&:downcase) - ANGULAR_PREFIXES
746
+ angular_only_pres = ANGULAR_PREFIXES - ESLINT_PREFIXES.map(&:downcase)
747
+
748
+ uses_eslint_only_pres =
749
+ recent_commit_messages.
750
+ any? { |m| eslint_only_pres.any? { |pre| m.match?(/#{pre}[:(]/i) } }
751
+
752
+ uses_angular_only_pres =
753
+ recent_commit_messages.
754
+ any? { |m| angular_only_pres.any? { |pre| m.match?(/#{pre}[:(]/i) } }
755
+
756
+ # If using any angular-only prefixes, return true
757
+ # (i.e., we assume Angular over ESLint when both are present)
758
+ return true if uses_angular_only_pres
759
+ return false if uses_eslint_only_pres
760
+
761
+ true
762
+ end
763
+
764
+ def using_eslint_commit_messages?
765
+ return false if recent_commit_messages.none?
766
+
767
+ semantic_messages = recent_commit_messages.select do |message|
768
+ ESLINT_PREFIXES.any? { |pre| message.start_with?(/#{pre}[:(]/) }
769
+ end
770
+
771
+ semantic_messages.count.to_f / recent_commit_messages.count > 0.3
772
+ end
773
+
774
+ def angular_commit_prefix
775
+ raise "Not using angular commits!" unless using_angular_commit_messages?
776
+
777
+ recent_commits_using_chore =
778
+ recent_commit_messages.
779
+ any? { |msg| msg.start_with?("chore", "Chore") }
780
+
781
+ recent_commits_using_build =
782
+ recent_commit_messages.
783
+ any? { |msg| msg.start_with?("build", "Build") }
784
+
785
+ commit_prefix =
786
+ if recent_commits_using_chore && !recent_commits_using_build
787
+ "chore"
788
+ else
789
+ "build"
790
+ end
791
+
792
+ if capitalize_angular_commit_prefix?
793
+ commit_prefix = commit_prefix.capitalize
794
+ end
795
+
796
+ commit_prefix
797
+ end
798
+
799
+ def capitalize_angular_commit_prefix?
800
+ semantic_messages = recent_commit_messages.select do |message|
801
+ ANGULAR_PREFIXES.any? { |pre| message.match?(/#{pre}[:(]/i) }
802
+ end
803
+
804
+ if semantic_messages.none?
805
+ return last_dependabot_commit_message&.match?(/^A-Z/)
806
+ end
807
+
808
+ capitalized_msgs = semantic_messages.select { |m| m.match?(/^[A-Z]/) }
809
+ capitalized_msgs.count.to_f / semantic_messages.count > 0.5
810
+ end
811
+
812
+ def using_gitmoji_commit_messages?
813
+ return false if recent_commit_messages.none?
814
+
815
+ gitmoji_messages = recent_commit_messages.select do |message|
816
+ GITMOJI_PREFIXES.any? { |pre| message.match?(/:#{pre}:/i) }
817
+ end
818
+
819
+ gitmoji_messages.count.to_f / recent_commit_messages.count > 0.3
820
+ end
821
+
822
+ def recent_commit_messages
823
+ case source.provider
824
+ when "github" then recent_github_commit_messages
825
+ when "gitlab" then recent_gitlab_commit_messages
826
+ else raise "Unsupported provider: #{source.provider}"
827
+ end
828
+ end
829
+
830
+ def recent_github_commit_messages
831
+ @recent_github_commit_messages ||=
832
+ github_client_for_source.commits(source.repo)
833
+
834
+ @recent_github_commit_messages.
835
+ reject { |c| c.author&.type == "Bot" }.
836
+ reject { |c| c.commit&.message&.start_with?("Merge") }.
837
+ map(&:commit).
838
+ map(&:message).
839
+ compact.
840
+ map(&:strip)
841
+ end
842
+
843
+ def recent_gitlab_commit_messages
844
+ @recent_gitlab_commit_messages ||=
845
+ gitlab_client_for_source.commits(source.repo)
846
+
847
+ @recent_gitlab_commit_messages.
848
+ reject { |c| c.author_email == "support@dependabot.com" }.
849
+ reject { |c| c.message&.start_with?("merge !") }.
850
+ map(&:message).
851
+ compact.
852
+ map(&:strip)
853
+ end
854
+
855
+ def last_dependabot_commit_message
856
+ case source.provider
857
+ when "github" then last_github_dependabot_commit_message
858
+ when "gitlab" then last_gitlab_dependabot_commit_message
859
+ else raise "Unsupported provider: #{source.provider}"
860
+ end
861
+ end
862
+
863
+ def last_github_dependabot_commit_message
864
+ @recent_github_commit_messages ||=
865
+ github_client_for_source.commits(source.repo)
866
+
867
+ @recent_github_commit_messages.
868
+ reject { |c| c.commit&.message&.start_with?("Merge") }.
869
+ find { |c| c.commit.author&.name == "dependabot[bot]" }&.
870
+ commit&.
871
+ message&.
872
+ strip
873
+ end
874
+
875
+ def last_gitlab_dependabot_commit_message
876
+ @recent_gitlab_commit_messages ||=
877
+ gitlab_client_for_source.commits(source.repo)
878
+
879
+ @recent_gitlab_commit_messages.
880
+ find { |c| c.author_email == "support@dependabot.com" }&.
881
+ message&.
882
+ strip
883
+ end
884
+
885
+ def github_client_for_source
886
+ @github_client_for_source ||=
887
+ Dependabot::Clients::GithubWithRetries.for_source(
888
+ source: source,
889
+ credentials: credentials
890
+ )
891
+ end
892
+
893
+ def gitlab_client_for_source
894
+ @gitlab_client_for_source ||= Dependabot::Clients::Gitlab.for_source(
895
+ source: source,
896
+ credentials: credentials
897
+ )
898
+ end
899
+
900
+ def package_manager
901
+ @package_manager ||= dependencies.first.package_manager
902
+ end
903
+ end
904
+ end
905
+ end
906
+ # rubocop:enable Metrics/ClassLength