dependabot-common 0.212.0 → 0.214.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/clients/azure.rb +10 -1
  3. data/lib/dependabot/clients/bitbucket.rb +49 -14
  4. data/lib/dependabot/clients/github_with_retries.rb +15 -19
  5. data/lib/dependabot/config/file.rb +1 -1
  6. data/lib/dependabot/dependency.rb +27 -2
  7. data/lib/dependabot/dependency_file.rb +11 -3
  8. data/lib/dependabot/errors.rb +3 -3
  9. data/lib/dependabot/experiments.rb +19 -0
  10. data/lib/dependabot/file_fetchers/base.rb +164 -80
  11. data/lib/dependabot/file_parsers/base/dependency_set.rb +106 -41
  12. data/lib/dependabot/git_commit_checker.rb +138 -91
  13. data/lib/dependabot/git_metadata_fetcher.rb +22 -18
  14. data/lib/dependabot/pull_request_creator/azure.rb +6 -2
  15. data/lib/dependabot/pull_request_creator/branch_namer.rb +15 -4
  16. data/lib/dependabot/pull_request_creator/github.rb +1 -1
  17. data/lib/dependabot/pull_request_creator/labeler.rb +6 -6
  18. data/lib/dependabot/pull_request_creator/message_builder/issue_linker.rb +5 -5
  19. data/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb +33 -5
  20. data/lib/dependabot/pull_request_creator/message_builder/metadata_presenter.rb +1 -3
  21. data/lib/dependabot/pull_request_creator/message_builder.rb +78 -6
  22. data/lib/dependabot/pull_request_creator/pr_name_prefixer.rb +3 -2
  23. data/lib/dependabot/pull_request_creator.rb +8 -3
  24. data/lib/dependabot/pull_request_updater/azure.rb +1 -1
  25. data/lib/dependabot/pull_request_updater/github.rb +15 -12
  26. data/lib/dependabot/pull_request_updater.rb +2 -1
  27. data/lib/dependabot/source.rb +9 -9
  28. data/lib/dependabot/update_checkers/base.rb +13 -6
  29. data/lib/dependabot/version.rb +1 -1
  30. metadata +36 -57
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
3
4
  require "dependabot/config"
4
5
  require "dependabot/dependency_file"
5
6
  require "dependabot/source"
@@ -25,6 +26,12 @@ module Dependabot
25
26
  Dependabot::Clients::CodeCommit::NotFound
26
27
  ].freeze
27
28
 
29
+ GIT_SUBMODULE_INACCESSIBLE_ERROR =
30
+ /^fatal: unable to access '(?<url>.*)': The requested URL returned error: (?<code>\d+)$/
31
+ GIT_SUBMODULE_CLONE_ERROR =
32
+ /^fatal: clone of '(?<url>.*)' into submodule path '.*' failed$/
33
+ GIT_SUBMODULE_ERROR_REGEX = /(#{GIT_SUBMODULE_INACCESSIBLE_ERROR})|(#{GIT_SUBMODULE_CLONE_ERROR})/
34
+
28
35
  def self.required_files_in?(_filename_array)
29
36
  raise NotImplementedError
30
37
  end
@@ -69,6 +76,7 @@ module Dependabot
69
76
  end
70
77
 
71
78
  def commit
79
+ return cloned_commit if cloned_commit
72
80
  return source.commit if source.commit
73
81
 
74
82
  branch = target_branch || default_branch_for_repo
@@ -84,7 +92,11 @@ module Dependabot
84
92
  def clone_repo_contents
85
93
  @clone_repo_contents ||=
86
94
  _clone_repo_contents(target_directory: repo_contents_path)
87
- rescue Dependabot::SharedHelpers::HelperSubprocessFailed
95
+ rescue Dependabot::SharedHelpers::HelperSubprocessFailed => e
96
+ if e.message.include?("fatal: Remote branch #{target_branch} not found in upstream origin")
97
+ raise Dependabot::BranchNotFound, target_branch
98
+ end
99
+
88
100
  raise Dependabot::RepoNotFound, source
89
101
  end
90
102
 
@@ -168,6 +180,97 @@ module Dependabot
168
180
  end
169
181
  end
170
182
 
183
+ def cloned_commit
184
+ return if repo_contents_path.nil? || !File.directory?(File.join(repo_contents_path, ".git"))
185
+
186
+ SharedHelpers.with_git_configured(credentials: credentials) do
187
+ Dir.chdir(repo_contents_path) do
188
+ return SharedHelpers.run_shell_command("git rev-parse HEAD")&.strip
189
+ end
190
+ end
191
+ end
192
+
193
+ def default_branch_for_repo
194
+ @default_branch_for_repo ||= client_for_provider.
195
+ fetch_default_branch(repo)
196
+ rescue *CLIENT_NOT_FOUND_ERRORS
197
+ raise Dependabot::RepoNotFound, source
198
+ end
199
+
200
+ def update_linked_paths(repo, path, commit, github_response)
201
+ case github_response.type
202
+ when "submodule"
203
+ sub_source = Source.from_url(github_response.submodule_git_url)
204
+ return unless sub_source
205
+
206
+ @linked_paths[path] = {
207
+ repo: sub_source.repo,
208
+ provider: sub_source.provider,
209
+ commit: github_response.sha,
210
+ path: "/"
211
+ }
212
+ when "symlink"
213
+ updated_path = File.join(File.dirname(path), github_response.target)
214
+ @linked_paths[path] = {
215
+ repo: repo,
216
+ provider: "github",
217
+ commit: commit,
218
+ path: Pathname.new(updated_path).cleanpath.to_path
219
+ }
220
+ end
221
+ end
222
+
223
+ def recurse_submodules_when_cloning?
224
+ false
225
+ end
226
+
227
+ def client_for_provider
228
+ case source.provider
229
+ when "github" then github_client
230
+ when "gitlab" then gitlab_client
231
+ when "azure" then azure_client
232
+ when "bitbucket" then bitbucket_client
233
+ when "codecommit" then codecommit_client
234
+ else raise "Unsupported provider '#{source.provider}'."
235
+ end
236
+ end
237
+
238
+ def github_client
239
+ @github_client ||=
240
+ Dependabot::Clients::GithubWithRetries.for_source(
241
+ source: source,
242
+ credentials: credentials
243
+ )
244
+ end
245
+
246
+ def gitlab_client
247
+ @gitlab_client ||=
248
+ Dependabot::Clients::GitlabWithRetries.for_source(
249
+ source: source,
250
+ credentials: credentials
251
+ )
252
+ end
253
+
254
+ def azure_client
255
+ @azure_client ||=
256
+ Dependabot::Clients::Azure.
257
+ for_source(source: source, credentials: credentials)
258
+ end
259
+
260
+ def bitbucket_client
261
+ # TODO: When self-hosted Bitbucket is supported this should use
262
+ # `Bitbucket.for_source`
263
+ @bitbucket_client ||=
264
+ Dependabot::Clients::BitbucketWithRetries.
265
+ for_bitbucket_dot_org(credentials: credentials)
266
+ end
267
+
268
+ def codecommit_client
269
+ @codecommit_client ||=
270
+ Dependabot::Clients::CodeCommit.
271
+ for_source(source: source, credentials: credentials)
272
+ end
273
+
171
274
  #################################################
172
275
  # INTERNAL METHODS (not for use by sub-classes) #
173
276
  #################################################
@@ -254,29 +357,6 @@ module Dependabot
254
357
  end
255
358
  end
256
359
 
257
- def update_linked_paths(repo, path, commit, github_response)
258
- case github_response.type
259
- when "submodule"
260
- sub_source = Source.from_url(github_response.submodule_git_url)
261
- return unless sub_source
262
-
263
- @linked_paths[path] = {
264
- repo: sub_source.repo,
265
- provider: sub_source.provider,
266
- commit: github_response.sha,
267
- path: "/"
268
- }
269
- when "symlink"
270
- updated_path = File.join(File.dirname(path), github_response.target)
271
- @linked_paths[path] = {
272
- repo: repo,
273
- provider: "github",
274
- commit: commit,
275
- path: Pathname.new(updated_path).cleanpath.to_path
276
- }
277
- end
278
- end
279
-
280
360
  def _build_github_file_struct(file)
281
361
  OpenStruct.new(
282
362
  name: file.name,
@@ -473,13 +553,6 @@ module Dependabot
473
553
  end
474
554
  # rubocop:enable Metrics/AbcSize
475
555
 
476
- def default_branch_for_repo
477
- @default_branch_for_repo ||= client_for_provider.
478
- fetch_default_branch(repo)
479
- rescue *CLIENT_NOT_FOUND_ERRORS
480
- raise Dependabot::RepoNotFound, source
481
- end
482
-
483
556
  # Update the @linked_paths hash by exploiting a side-effect of
484
557
  # recursively calling `repo_contents` for each directory up the tree
485
558
  # until a submodule or symlink is found
@@ -504,6 +577,10 @@ module Dependabot
504
577
  max_by(&:length)
505
578
  end
506
579
 
580
+ # rubocop:disable Metrics/AbcSize
581
+ # rubocop:disable Metrics/MethodLength
582
+ # rubocop:disable Metrics/PerceivedComplexity
583
+ # rubocop:disable Metrics/BlockLength
507
584
  def _clone_repo_contents(target_directory:)
508
585
  SharedHelpers.with_git_configured(credentials: credentials) do
509
586
  path = target_directory || File.join("tmp", source.repo)
@@ -512,62 +589,69 @@ module Dependabot
512
589
  return path if Dir.exist?(File.join(path, ".git"))
513
590
 
514
591
  FileUtils.mkdir_p(path)
515
- br_opt = " --branch #{source.branch} --single-branch" if source.branch
516
- SharedHelpers.run_shell_command(
517
- <<~CMD
518
- git clone --no-tags --no-recurse-submodules --depth 1#{br_opt} #{source.url} #{path}
519
- CMD
520
- )
521
- path
522
- end
523
- end
524
592
 
525
- def client_for_provider
526
- case source.provider
527
- when "github" then github_client
528
- when "gitlab" then gitlab_client
529
- when "azure" then azure_client
530
- when "bitbucket" then bitbucket_client
531
- when "codecommit" then codecommit_client
532
- else raise "Unsupported provider '#{source.provider}'."
533
- end
534
- end
593
+ clone_options = StringIO.new
594
+ clone_options << "--no-tags --depth 1"
595
+ clone_options << if recurse_submodules_when_cloning?
596
+ " --recurse-submodules --shallow-submodules"
597
+ else
598
+ " --no-recurse-submodules"
599
+ end
600
+ clone_options << " --branch #{source.branch} --single-branch" if source.branch
535
601
 
536
- def github_client
537
- @github_client ||=
538
- Dependabot::Clients::GithubWithRetries.for_source(
539
- source: source,
540
- credentials: credentials
541
- )
542
- end
602
+ submodule_cloning_failed = false
603
+ begin
604
+ SharedHelpers.run_shell_command(
605
+ <<~CMD
606
+ git clone #{clone_options.string} #{source.url} #{path}
607
+ CMD
608
+ )
609
+ rescue SharedHelpers::HelperSubprocessFailed => e
610
+ raise unless GIT_SUBMODULE_ERROR_REGEX && e.message.downcase.include?("submodule")
543
611
 
544
- def gitlab_client
545
- @gitlab_client ||=
546
- Dependabot::Clients::GitlabWithRetries.for_source(
547
- source: source,
548
- credentials: credentials
549
- )
550
- end
612
+ submodule_cloning_failed = true
613
+ match = e.message.match(GIT_SUBMODULE_ERROR_REGEX)
614
+ url = match.named_captures["url"]
615
+ code = match.named_captures["code"]
551
616
 
552
- def azure_client
553
- @azure_client ||=
554
- Dependabot::Clients::Azure.
555
- for_source(source: source, credentials: credentials)
556
- end
617
+ # Submodules might be in the repo but unrelated to dependencies,
618
+ # so ignoring this error to try the update anyway since the base repo exists.
619
+ Dependabot.logger.error("Cloning of submodule failed: #{url} error: #{code || 'unknown'}")
620
+ end
557
621
 
558
- def bitbucket_client
559
- # TODO: When self-hosted Bitbucket is supported this should use
560
- # `Bitbucket.for_source`
561
- @bitbucket_client ||=
562
- Dependabot::Clients::BitbucketWithRetries.
563
- for_bitbucket_dot_org(credentials: credentials)
564
- end
622
+ if source.commit
623
+ # This code will only be called for testing. Production will never pass a commit
624
+ # since Dependabot always wants to use the latest commit on a branch.
625
+ Dir.chdir(path) do
626
+ fetch_options = StringIO.new
627
+ fetch_options << "--depth 1"
628
+ fetch_options << if recurse_submodules_when_cloning? && !submodule_cloning_failed
629
+ " --recurse-submodules=on-demand"
630
+ else
631
+ " --no-recurse-submodules"
632
+ end
633
+ # Need to fetch the commit due to the --depth 1 above.
634
+ SharedHelpers.run_shell_command("git fetch #{fetch_options.string} origin #{source.commit}")
635
+
636
+ reset_options = StringIO.new
637
+ reset_options << "--hard"
638
+ reset_options << if recurse_submodules_when_cloning? && !submodule_cloning_failed
639
+ " --recurse-submodules"
640
+ else
641
+ " --no-recurse-submodules"
642
+ end
643
+ # Set HEAD to this commit so later calls so git reset HEAD will work.
644
+ SharedHelpers.run_shell_command("git reset #{reset_options.string} #{source.commit}")
645
+ end
646
+ end
565
647
 
566
- def codecommit_client
567
- @codecommit_client ||=
568
- Dependabot::Clients::CodeCommit.
569
- for_source(source: source, credentials: credentials)
648
+ path
649
+ end
570
650
  end
651
+ # rubocop:enable Metrics/AbcSize
652
+ # rubocop:enable Metrics/MethodLength
653
+ # rubocop:enable Metrics/PerceivedComplexity
654
+ # rubocop:enable Metrics/BlockLength
571
655
  end
572
656
  end
573
657
  end
@@ -14,34 +14,42 @@ module Dependabot
14
14
  raise ArgumentError, "must be an array of Dependency objects"
15
15
  end
16
16
 
17
- @dependencies = dependencies
18
17
  @case_sensitive = case_sensitive
18
+ @dependencies = Hash.new { |hsh, key| hsh[key] = DependencySlot.new }
19
+ dependencies.each { |dep| self << dep }
19
20
  end
20
21
 
21
- attr_reader :dependencies
22
+ def dependencies
23
+ @dependencies.values.filter_map(&:combined)
24
+ end
22
25
 
23
26
  def <<(dep)
24
27
  raise ArgumentError, "must be a Dependency object" unless dep.is_a?(Dependency)
25
28
 
26
- existing_dependency = dependency_for_name(dep.name)
29
+ @dependencies[key_for_dependency(dep)] << dep
30
+ self
31
+ end
27
32
 
28
- return self if existing_dependency&.to_h == dep.to_h
33
+ def +(other)
34
+ raise ArgumentError, "must be a DependencySet" unless other.is_a?(DependencySet)
29
35
 
30
- if existing_dependency
31
- dependencies[dependencies.index(existing_dependency)] =
32
- combined_dependency(existing_dependency, dep)
33
- else
34
- dependencies << dep
36
+ other_names = other.dependencies.map(&:name)
37
+ other_names.each do |name|
38
+ all_versions = other.all_versions_for_name(name)
39
+ all_versions.each { |dep| self << dep }
35
40
  end
36
41
 
37
42
  self
38
43
  end
39
44
 
40
- def +(other)
41
- raise ArgumentError, "must be a DependencySet" unless other.is_a?(DependencySet)
45
+ def all_versions_for_name(name)
46
+ key = key_for_name(name)
47
+ @dependencies.key?(key) ? @dependencies[key].all_versions : []
48
+ end
42
49
 
43
- other.dependencies.each { |dep| self << dep }
44
- self
50
+ def dependency_for_name(name)
51
+ key = key_for_name(name)
52
+ @dependencies.key?(key) ? @dependencies[key].combined : nil
45
53
  end
46
54
 
47
55
  private
@@ -50,41 +58,98 @@ module Dependabot
50
58
  @case_sensitive
51
59
  end
52
60
 
53
- def dependency_for_name(name)
54
- return dependencies.find { |d| d.name == name } if case_sensitive?
61
+ def key_for_name(name)
62
+ case_sensitive? ? name : name.downcase
63
+ end
55
64
 
56
- dependencies.find { |d| d.name&.downcase == name&.downcase }
65
+ def key_for_dependency(dep)
66
+ key_for_name(dep.name)
57
67
  end
58
68
 
59
- def combined_dependency(old_dep, new_dep)
60
- package_manager = old_dep.package_manager
61
- v_cls = Utils.version_class_for_package_manager(package_manager)
62
-
63
- # If we already have a requirement use the existing version
64
- # (if present). Otherwise, use whatever the lowest version is
65
- new_version =
66
- if old_dep.requirements.any? then old_dep.version || new_dep.version
67
- elsif !v_cls.correct?(new_dep.version) then old_dep.version
68
- elsif !v_cls.correct?(old_dep.version) then new_dep.version
69
- elsif v_cls.new(new_dep.version) > v_cls.new(old_dep.version)
70
- old_dep.version
69
+ # There can only be one entry per dependency name in a `DependencySet`. Each entry
70
+ # is assigned a `DependencySlot`.
71
+ #
72
+ # In some ecosystems (like `npm_and_yarn`), however, multiple versions of a
73
+ # dependency may be encountered and added to the set. The `DependencySlot` retains
74
+ # all added versions and presents a single unified dependency for the entry
75
+ # that combines the attributes of these versions.
76
+ #
77
+ # The combined dependency is accessible via `DependencySet#dependencies` or
78
+ # `DependencySet#dependency_for_name`. The list of individual versions of the
79
+ # dependency is accessible via `DependencySet#all_versions_for_name`.
80
+ class DependencySlot
81
+ attr_reader :all_versions, :combined
82
+
83
+ def initialize
84
+ @all_versions = []
85
+ @combined = nil
86
+ end
87
+
88
+ def <<(dep)
89
+ return self if @all_versions.include?(dep)
90
+
91
+ @combined = if @combined
92
+ combined_dependency(@combined, dep)
93
+ else
94
+ Dependency.new(
95
+ name: dep.name,
96
+ version: dep.version,
97
+ requirements: dep.requirements,
98
+ package_manager: dep.package_manager,
99
+ subdependency_metadata: dep.subdependency_metadata
100
+ )
101
+ end
102
+
103
+ index_of_same_version =
104
+ @all_versions.find_index { |other| other.version == dep.version }
105
+
106
+ if index_of_same_version.nil?
107
+ @all_versions << dep
71
108
  else
72
- new_dep.version
109
+ same_version = @all_versions[index_of_same_version]
110
+ @all_versions[index_of_same_version] = combined_dependency(same_version, dep)
73
111
  end
74
112
 
75
- subdependency_metadata = (
76
- (old_dep.subdependency_metadata || []) +
77
- (new_dep.subdependency_metadata || [])
78
- ).uniq
79
-
80
- Dependency.new(
81
- name: old_dep.name,
82
- version: new_version,
83
- requirements: (old_dep.requirements + new_dep.requirements).uniq,
84
- package_manager: package_manager,
85
- subdependency_metadata: subdependency_metadata
86
- )
113
+ self
114
+ end
115
+
116
+ private
117
+
118
+ # Produces a new dependency by merging the attributes of `old_dep` with those of
119
+ # `new_dep`. Requirements and subdependency metadata will be combined and deduped.
120
+ # The version of the combined dependency is determined by the logic below.
121
+ def combined_dependency(old_dep, new_dep)
122
+ version = if old_dep.top_level? # Prefer a direct dependency over a transitive one
123
+ old_dep.version || new_dep.version
124
+ elsif !version_class.correct?(new_dep.version)
125
+ old_dep.version
126
+ elsif !version_class.correct?(old_dep.version)
127
+ new_dep.version
128
+ elsif version_class.new(new_dep.version) > version_class.new(old_dep.version)
129
+ old_dep.version
130
+ else
131
+ new_dep.version
132
+ end
133
+ requirements = (old_dep.requirements + new_dep.requirements).uniq
134
+ subdependency_metadata = (
135
+ (old_dep.subdependency_metadata || []) +
136
+ (new_dep.subdependency_metadata || [])
137
+ ).uniq
138
+
139
+ Dependency.new(
140
+ name: old_dep.name,
141
+ version: version,
142
+ requirements: requirements,
143
+ package_manager: old_dep.package_manager,
144
+ subdependency_metadata: subdependency_metadata
145
+ )
146
+ end
147
+
148
+ def version_class
149
+ @version_class ||= Utils.version_class_for_package_manager(@combined.package_manager)
150
+ end
87
151
  end
152
+ private_constant :DependencySlot
88
153
  end
89
154
  end
90
155
  end