dependabot-common 0.95.1 → 0.95.2

Sign up to get free protection for your applications and to get access to all the features.
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