dependabot-common 0.235.0 → 0.237.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/clients/azure.rb +3 -3
  3. data/lib/dependabot/config/file.rb +32 -9
  4. data/lib/dependabot/config/file_fetcher.rb +3 -3
  5. data/lib/dependabot/config/ignore_condition.rb +34 -8
  6. data/lib/dependabot/config/update_config.rb +42 -6
  7. data/lib/dependabot/config.rb +1 -1
  8. data/lib/dependabot/dependency_file.rb +89 -14
  9. data/lib/dependabot/dependency_group.rb +29 -5
  10. data/lib/dependabot/errors.rb +101 -13
  11. data/lib/dependabot/file_fetchers/base.rb +250 -93
  12. data/lib/dependabot/file_updaters/artifact_updater.rb +37 -10
  13. data/lib/dependabot/file_updaters/vendor_updater.rb +13 -3
  14. data/lib/dependabot/logger.rb +7 -2
  15. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +13 -6
  16. data/lib/dependabot/pull_request_creator/commit_signer.rb +33 -7
  17. data/lib/dependabot/pull_request_creator/github.rb +13 -10
  18. data/lib/dependabot/pull_request_creator/message.rb +21 -2
  19. data/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb +37 -16
  20. data/lib/dependabot/pull_request_creator/message_builder/metadata_presenter.rb +5 -3
  21. data/lib/dependabot/pull_request_creator/message_builder.rb +5 -18
  22. data/lib/dependabot/pull_request_creator/pr_name_prefixer.rb +10 -4
  23. data/lib/dependabot/pull_request_updater/github.rb +2 -2
  24. data/lib/dependabot/shared_helpers.rb +117 -33
  25. data/lib/dependabot/simple_instrumentor.rb +22 -3
  26. data/lib/dependabot/source.rb +65 -17
  27. data/lib/dependabot/update_checkers/version_filters.rb +12 -1
  28. data/lib/dependabot/utils.rb +21 -2
  29. data/lib/dependabot/workspace/base.rb +42 -7
  30. data/lib/dependabot/workspace/change_attempt.rb +31 -3
  31. data/lib/dependabot/workspace/git.rb +34 -4
  32. data/lib/dependabot/workspace.rb +16 -2
  33. data/lib/dependabot.rb +1 -1
  34. metadata +37 -9
@@ -1,7 +1,8 @@
1
- # typed: false
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "stringio"
5
+ require "sorbet-runtime"
5
6
  require "dependabot/config"
6
7
  require "dependabot/dependency_file"
7
8
  require "dependabot/source"
@@ -17,15 +18,33 @@ require "dependabot/shared_helpers"
17
18
  module Dependabot
18
19
  module FileFetchers
19
20
  class Base
20
- attr_reader :source, :credentials, :repo_contents_path, :options
21
+ extend T::Sig
22
+ extend T::Helpers
21
23
 
22
- CLIENT_NOT_FOUND_ERRORS = [
23
- Octokit::NotFound,
24
- Gitlab::Error::NotFound,
25
- Dependabot::Clients::Azure::NotFound,
26
- Dependabot::Clients::Bitbucket::NotFound,
27
- Dependabot::Clients::CodeCommit::NotFound
28
- ].freeze
24
+ abstract!
25
+
26
+ sig { returns(Dependabot::Source) }
27
+ attr_reader :source
28
+
29
+ sig { returns(T::Array[T::Hash[String, String]]) }
30
+ attr_reader :credentials
31
+
32
+ sig { returns(T.nilable(String)) }
33
+ attr_reader :repo_contents_path
34
+
35
+ sig { returns(T::Hash[String, String]) }
36
+ attr_reader :options
37
+
38
+ CLIENT_NOT_FOUND_ERRORS = T.let(
39
+ [
40
+ Octokit::NotFound,
41
+ Gitlab::Error::NotFound,
42
+ Dependabot::Clients::Azure::NotFound,
43
+ Dependabot::Clients::Bitbucket::NotFound,
44
+ Dependabot::Clients::CodeCommit::NotFound
45
+ ].freeze,
46
+ T::Array[T.class_of(StandardError)]
47
+ )
29
48
 
30
49
  GIT_SUBMODULE_INACCESSIBLE_ERROR =
31
50
  /^fatal: unable to access '(?<url>.*)': The requested URL returned error: (?<code>\d+)$/
@@ -33,13 +52,11 @@ module Dependabot
33
52
  /^fatal: clone of '(?<url>.*)' into submodule path '.*' failed$/
34
53
  GIT_SUBMODULE_ERROR_REGEX = /(#{GIT_SUBMODULE_INACCESSIBLE_ERROR})|(#{GIT_SUBMODULE_CLONE_ERROR})/
35
54
 
36
- def self.required_files_in?(_filename_array)
37
- raise NotImplementedError
38
- end
55
+ sig { abstract.params(filenames: T::Array[String]).returns(T::Boolean) }
56
+ def self.required_files_in?(filenames); end
39
57
 
40
- def self.required_files_message
41
- raise NotImplementedError
42
- end
58
+ sig { abstract.returns(String) }
59
+ def self.required_files_message; end
43
60
 
44
61
  # Creates a new FileFetcher for retrieving `DependencyFile`s.
45
62
  #
@@ -52,37 +69,58 @@ module Dependabot
52
69
  # by repo_contents_path and still use an API trip.
53
70
  #
54
71
  # options supports custom feature enablement
72
+ sig do
73
+ params(
74
+ source: Dependabot::Source,
75
+ credentials: T::Array[T::Hash[String, String]],
76
+ repo_contents_path: T.nilable(String),
77
+ options: T::Hash[String, String]
78
+ )
79
+ .void
80
+ end
55
81
  def initialize(source:, credentials:, repo_contents_path: nil, options: {})
56
82
  @source = source
57
83
  @credentials = credentials
58
84
  @repo_contents_path = repo_contents_path
59
- @linked_paths = {}
85
+ @linked_paths = T.let({}, T::Hash[T.untyped, T.untyped])
86
+ @submodules = T.let([], T::Array[T.untyped])
60
87
  @options = options
61
88
  end
62
89
 
90
+ sig { returns(String) }
63
91
  def repo
64
92
  source.repo
65
93
  end
66
94
 
95
+ sig { returns(String) }
67
96
  def directory
68
97
  Pathname.new(source.directory || "/").cleanpath.to_path
69
98
  end
70
99
 
100
+ sig { returns(T.nilable(String)) }
71
101
  def target_branch
72
102
  source.branch
73
103
  end
74
104
 
105
+ sig { returns(T::Array[DependencyFile]) }
75
106
  def files
76
- @files ||= fetch_files
107
+ @files ||= T.let(
108
+ fetch_files.each { |f| f.job_directory = directory },
109
+ T.nilable(T::Array[DependencyFile])
110
+ )
77
111
  end
78
112
 
113
+ sig { abstract.returns(T::Array[DependencyFile]) }
114
+ def fetch_files; end
115
+
116
+ sig { returns(T.nilable(String)) }
79
117
  def commit
80
- return cloned_commit if cloned_commit
81
- return source.commit if source.commit
118
+ return T.must(cloned_commit) if cloned_commit
119
+ return T.must(source.commit) if source.commit
82
120
 
83
121
  branch = target_branch || default_branch_for_repo
84
122
 
85
- @commit ||= client_for_provider.fetch_commit(repo, branch)
123
+ @commit ||= T.let(T.unsafe(client_for_provider).fetch_commit(repo, branch), T.nilable(String))
86
124
  rescue *CLIENT_NOT_FOUND_ERRORS
87
125
  raise Dependabot::BranchNotFound, branch
88
126
  rescue Octokit::Conflict => e
@@ -90,9 +128,12 @@ module Dependabot
90
128
  end
91
129
 
92
130
  # Returns the path to the cloned repo
131
+ sig { returns(String) }
93
132
  def clone_repo_contents
94
- @clone_repo_contents ||=
95
- _clone_repo_contents(target_directory: repo_contents_path)
133
+ @clone_repo_contents ||= T.let(
134
+ _clone_repo_contents(target_directory: repo_contents_path),
135
+ T.nilable(String)
136
+ )
96
137
  rescue Dependabot::SharedHelpers::HelperSubprocessFailed => e
97
138
  if e.message.include?("fatal: Remote branch #{target_branch} not found in upstream origin")
98
139
  raise Dependabot::BranchNotFound, target_branch
@@ -100,19 +141,20 @@ module Dependabot
100
141
  raise Dependabot::OutOfDisk
101
142
  end
102
143
 
103
- raise Dependabot::RepoNotFound, source
144
+ raise Dependabot::RepoNotFound.new(source, e.message)
104
145
  end
105
146
 
106
- def ecosystem_versions
107
- nil
108
- end
147
+ sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
148
+ def ecosystem_versions; end
109
149
 
110
150
  private
111
151
 
152
+ sig { params(name: String).returns(T.nilable(Dependabot::DependencyFile)) }
112
153
  def fetch_support_file(name)
113
154
  fetch_file_if_present(name)&.tap { |f| f.support_file = true }
114
155
  end
115
156
 
157
+ sig { params(filename: String, fetch_submodules: T::Boolean).returns(T.nilable(DependencyFile)) }
116
158
  def fetch_file_if_present(filename, fetch_submodules: false)
117
159
  unless repo_contents_path.nil?
118
160
  begin
@@ -136,6 +178,7 @@ module Dependabot
136
178
  nil
137
179
  end
138
180
 
181
+ sig { params(filename: T.any(Pathname, String)).returns(Dependabot::DependencyFile) }
139
182
  def load_cloned_file_if_present(filename)
140
183
  path = Pathname.new(File.join(directory, filename)).cleanpath.to_path
141
184
  repo_path = File.join(clone_repo_contents, path)
@@ -154,10 +197,19 @@ module Dependabot
154
197
  directory: directory,
155
198
  type: type,
156
199
  content: content,
157
- symlink_target: symlink_target
200
+ symlink_target: symlink_target,
201
+ support_file: in_submodule?(path)
158
202
  )
159
203
  end
160
204
 
205
+ sig do
206
+ params(
207
+ filename: T.any(Pathname, String),
208
+ type: String,
209
+ fetch_submodules: T::Boolean
210
+ )
211
+ .returns(Dependabot::DependencyFile)
212
+ end
161
213
  def fetch_file_from_host(filename, type: "file", fetch_submodules: false)
162
214
  return load_cloned_file_if_present(filename) unless repo_contents_path.nil?
163
215
 
@@ -167,7 +219,7 @@ module Dependabot
167
219
 
168
220
  linked_path = symlinked_subpath(clean_path)
169
221
  type = "symlink" if linked_path
170
- symlink_target = clean_path.sub(linked_path, @linked_paths.dig(linked_path, :path)) if type == "symlink"
222
+ symlink_target = clean_path.sub(T.must(linked_path), @linked_paths.dig(linked_path, :path)) if type == "symlink"
171
223
 
172
224
  DependencyFile.new(
173
225
  name: Pathname.new(filename).cleanpath.to_path,
@@ -181,61 +233,87 @@ module Dependabot
181
233
  end
182
234
 
183
235
  # Finds the first subpath in path that is a symlink
236
+ sig { params(path: String).returns(T.nilable(String)) }
184
237
  def symlinked_subpath(path)
185
238
  subpaths(path).find { |subpath| @linked_paths.key?(subpath) }
186
239
  end
187
240
 
241
+ sig { params(path: String).returns(T::Boolean) }
242
+ def in_submodule?(path)
243
+ subpaths(path.delete_prefix("/")).any? { |subpath| @submodules.include?(subpath) }
244
+ end
245
+
188
246
  # Given a "foo/bar/baz" path, returns ["foo", "foo/bar", "foo/bar/baz"]
247
+ sig { params(path: String).returns(T::Array[String]) }
189
248
  def subpaths(path)
190
249
  components = path.split("/")
191
- components.map { |component| components[0..components.index(component)].join("/") }
250
+ components.map { |component| T.must(components[0..components.index(component)]).join("/") }
192
251
  end
193
252
 
253
+ sig do
254
+ params(
255
+ dir: T.any(Pathname, String),
256
+ ignore_base_directory: T::Boolean,
257
+ raise_errors: T::Boolean,
258
+ fetch_submodules: T::Boolean
259
+ )
260
+ .returns(T::Array[T.untyped])
261
+ end
194
262
  def repo_contents(dir: ".", ignore_base_directory: false,
195
263
  raise_errors: true, fetch_submodules: false)
196
264
  dir = File.join(directory, dir) unless ignore_base_directory
197
265
  path = Pathname.new(dir).cleanpath.to_path.gsub(%r{^/*}, "")
198
266
 
199
- @repo_contents ||= {}
200
- @repo_contents[dir] ||= if repo_contents_path
201
- _cloned_repo_contents(path)
202
- else
203
- _fetch_repo_contents(path, raise_errors: raise_errors,
204
- fetch_submodules: fetch_submodules)
205
- end
267
+ @repo_contents ||= T.let({}, T.nilable(T::Hash[String, T::Array[T.untyped]]))
268
+ @repo_contents[dir.to_s] ||= if repo_contents_path
269
+ _cloned_repo_contents(path)
270
+ else
271
+ _fetch_repo_contents(path, raise_errors: raise_errors,
272
+ fetch_submodules: fetch_submodules)
273
+ end
206
274
  end
207
275
 
276
+ sig { returns(T.nilable(String)) }
208
277
  def cloned_commit
209
278
  return if repo_contents_path.nil? || !File.directory?(File.join(repo_contents_path, ".git"))
210
279
 
211
280
  SharedHelpers.with_git_configured(credentials: credentials) do
212
- Dir.chdir(repo_contents_path) do
213
- return SharedHelpers.run_shell_command("git rev-parse HEAD")&.strip
281
+ Dir.chdir(T.must(repo_contents_path)) do
282
+ return SharedHelpers.run_shell_command("git rev-parse HEAD").strip
214
283
  end
215
284
  end
216
285
  end
217
286
 
287
+ sig { returns(String) }
218
288
  def default_branch_for_repo
219
- @default_branch_for_repo ||= client_for_provider
220
- .fetch_default_branch(repo)
289
+ @default_branch_for_repo ||= T.let(T.unsafe(client_for_provider).fetch_default_branch(repo), T.nilable(String))
221
290
  rescue *CLIENT_NOT_FOUND_ERRORS
222
291
  raise Dependabot::RepoNotFound, source
223
292
  end
224
293
 
294
+ sig do
295
+ params(
296
+ repo: String,
297
+ path: String,
298
+ commit: String,
299
+ github_response: Sawyer::Resource
300
+ )
301
+ .returns(T.nilable(T::Hash[String, T.untyped]))
302
+ end
225
303
  def update_linked_paths(repo, path, commit, github_response)
226
- case github_response.type
304
+ case T.unsafe(github_response).type
227
305
  when "submodule"
228
- sub_source = Source.from_url(github_response.submodule_git_url)
306
+ sub_source = Source.from_url(T.unsafe(github_response).submodule_git_url)
229
307
  return unless sub_source
230
308
 
231
309
  @linked_paths[path] = {
232
310
  repo: sub_source.repo,
233
311
  provider: sub_source.provider,
234
- commit: github_response.sha,
312
+ commit: T.unsafe(github_response).sha,
235
313
  path: "/"
236
314
  }
237
315
  when "symlink"
238
- updated_path = File.join(File.dirname(path), github_response.target)
316
+ updated_path = File.join(File.dirname(path), T.unsafe(github_response).target)
239
317
  @linked_paths[path] = {
240
318
  repo: repo,
241
319
  provider: "github",
@@ -245,10 +323,22 @@ module Dependabot
245
323
  end
246
324
  end
247
325
 
326
+ sig { returns(T::Boolean) }
248
327
  def recurse_submodules_when_cloning?
249
328
  false
250
329
  end
251
330
 
331
+ sig do
332
+ returns(
333
+ T.any(
334
+ Dependabot::Clients::GithubWithRetries,
335
+ Dependabot::Clients::GitlabWithRetries,
336
+ Dependabot::Clients::Azure,
337
+ Dependabot::Clients::BitbucketWithRetries,
338
+ Dependabot::Clients::CodeCommit
339
+ )
340
+ )
341
+ end
252
342
  def client_for_provider
253
343
  case source.provider
254
344
  when "github" then github_client
@@ -260,46 +350,75 @@ module Dependabot
260
350
  end
261
351
  end
262
352
 
353
+ sig { returns(Dependabot::Clients::GithubWithRetries) }
263
354
  def github_client
264
355
  @github_client ||=
265
- Dependabot::Clients::GithubWithRetries.for_source(
266
- source: source,
267
- credentials: credentials
356
+ T.let(
357
+ Dependabot::Clients::GithubWithRetries.for_source(
358
+ source: source,
359
+ credentials: credentials
360
+ ),
361
+ T.nilable(Dependabot::Clients::GithubWithRetries)
268
362
  )
269
363
  end
270
364
 
365
+ sig { returns(Dependabot::Clients::GitlabWithRetries) }
271
366
  def gitlab_client
272
367
  @gitlab_client ||=
273
- Dependabot::Clients::GitlabWithRetries.for_source(
274
- source: source,
275
- credentials: credentials
368
+ T.let(
369
+ Dependabot::Clients::GitlabWithRetries.for_source(
370
+ source: source,
371
+ credentials: credentials
372
+ ),
373
+ T.nilable(Dependabot::Clients::GitlabWithRetries)
276
374
  )
277
375
  end
278
376
 
377
+ sig { returns(Dependabot::Clients::Azure) }
279
378
  def azure_client
280
379
  @azure_client ||=
281
- Dependabot::Clients::Azure
282
- .for_source(source: source, credentials: credentials)
380
+ T.let(
381
+ Dependabot::Clients::Azure.for_source(
382
+ source: source,
383
+ credentials: credentials
384
+ ),
385
+ T.nilable(Dependabot::Clients::Azure)
386
+ )
283
387
  end
284
388
 
389
+ sig { returns(Dependabot::Clients::BitbucketWithRetries) }
285
390
  def bitbucket_client
286
391
  # TODO: When self-hosted Bitbucket is supported this should use
287
392
  # `Bitbucket.for_source`
288
393
  @bitbucket_client ||=
289
- Dependabot::Clients::BitbucketWithRetries
290
- .for_bitbucket_dot_org(credentials: credentials)
394
+ T.let(
395
+ Dependabot::Clients::BitbucketWithRetries.for_bitbucket_dot_org(
396
+ credentials: credentials
397
+ ),
398
+ T.nilable(Dependabot::Clients::BitbucketWithRetries)
399
+ )
291
400
  end
292
401
 
402
+ sig { returns(Dependabot::Clients::CodeCommit) }
293
403
  def codecommit_client
294
404
  @codecommit_client ||=
295
- Dependabot::Clients::CodeCommit
296
- .for_source(source: source, credentials: credentials)
405
+ T.let(
406
+ Dependabot::Clients::CodeCommit.for_source(
407
+ source: source,
408
+ credentials: credentials
409
+ ),
410
+ T.nilable(Dependabot::Clients::CodeCommit)
411
+ )
297
412
  end
298
413
 
299
414
  #################################################
300
415
  # INTERNAL METHODS (not for use by sub-classes) #
301
416
  #################################################
302
417
 
418
+ sig do
419
+ params(path: String, fetch_submodules: T::Boolean, raise_errors: T::Boolean)
420
+ .returns(T::Array[OpenStruct])
421
+ end
303
422
  def _fetch_repo_contents(path, fetch_submodules: false,
304
423
  raise_errors: true)
305
424
  path = path.gsub(" ", "%20")
@@ -331,6 +450,10 @@ module Dependabot
331
450
  retry
332
451
  end
333
452
 
453
+ sig do
454
+ params(provider: String, repo: String, path: String, commit: String)
455
+ .returns(T::Array[OpenStruct])
456
+ end
334
457
  def _fetch_repo_contents_fully_specified(provider, repo, path, commit)
335
458
  case provider
336
459
  when "github"
@@ -347,9 +470,10 @@ module Dependabot
347
470
  end
348
471
  end
349
472
 
473
+ sig { params(repo: String, path: String, commit: String).returns(T::Array[OpenStruct]) }
350
474
  def _github_repo_contents(repo, path, commit)
351
475
  path = path.gsub(" ", "%20")
352
- github_response = github_client.contents(repo, path: path, ref: commit)
476
+ github_response = T.unsafe(github_client).contents(repo, path: path, ref: commit)
353
477
 
354
478
  if github_response.respond_to?(:type)
355
479
  update_linked_paths(repo, path, commit, github_response)
@@ -359,6 +483,7 @@ module Dependabot
359
483
  github_response.map { |f| _build_github_file_struct(f) }
360
484
  end
361
485
 
486
+ sig { params(relative_path: String).returns(T::Array[OpenStruct]) }
362
487
  def _cloned_repo_contents(relative_path)
363
488
  repo_path = File.join(clone_repo_contents, relative_path)
364
489
  return [] unless Dir.exist?(repo_path)
@@ -384,37 +509,40 @@ module Dependabot
384
509
  end
385
510
  end
386
511
 
512
+ sig { params(file: Sawyer::Resource).returns(OpenStruct) }
387
513
  def _build_github_file_struct(file)
388
514
  OpenStruct.new(
389
- name: file.name,
390
- path: file.path,
391
- type: file.type,
392
- sha: file.sha,
393
- size: file.size
515
+ name: T.unsafe(file).name,
516
+ path: T.unsafe(file).path,
517
+ type: T.unsafe(file).type,
518
+ sha: T.unsafe(file).sha,
519
+ size: T.unsafe(file).size
394
520
  )
395
521
  end
396
522
 
523
+ sig { params(repo: String, path: String, commit: String).returns(T::Array[OpenStruct]) }
397
524
  def _gitlab_repo_contents(repo, path, commit)
398
- gitlab_client
399
- .repo_tree(repo, path: path, ref: commit, per_page: 100)
400
- .map do |file|
401
- # GitLab API essentially returns the output from `git ls-tree`
402
- type = case file.type
403
- when "blob" then "file"
404
- when "tree" then "dir"
405
- when "commit" then "submodule"
406
- else file.fetch("type")
407
- end
408
-
409
- OpenStruct.new(
410
- name: file.name,
411
- path: file.path,
412
- type: type,
413
- size: 0 # GitLab doesn't return file size
414
- )
415
- end
525
+ T.unsafe(gitlab_client)
526
+ .repo_tree(repo, path: path, ref: commit, per_page: 100)
527
+ .map do |file|
528
+ # GitLab API essentially returns the output from `git ls-tree`
529
+ type = case file.type
530
+ when "blob" then "file"
531
+ when "tree" then "dir"
532
+ when "commit" then "submodule"
533
+ else file.fetch("type")
534
+ end
535
+
536
+ OpenStruct.new(
537
+ name: file.name,
538
+ path: file.path,
539
+ type: type,
540
+ size: 0 # GitLab doesn't return file size
541
+ )
542
+ end
416
543
  end
417
544
 
545
+ sig { params(path: String, commit: String).returns(T::Array[OpenStruct]) }
418
546
  def _azure_repo_contents(path, commit)
419
547
  response = azure_client.fetch_repo_contents(commit, path)
420
548
 
@@ -434,12 +562,14 @@ module Dependabot
434
562
  end
435
563
  end
436
564
 
565
+ sig { params(repo: String, path: String, commit: String).returns(T::Array[OpenStruct]) }
437
566
  def _bitbucket_repo_contents(repo, path, commit)
438
- response = bitbucket_client.fetch_repo_contents(
439
- repo,
440
- commit,
441
- path
442
- )
567
+ response = T.unsafe(bitbucket_client)
568
+ .fetch_repo_contents(
569
+ repo,
570
+ commit,
571
+ path
572
+ )
443
573
 
444
574
  response.map do |file|
445
575
  type = case file.fetch("type")
@@ -457,6 +587,7 @@ module Dependabot
457
587
  end
458
588
  end
459
589
 
590
+ sig { params(repo: String, path: String, commit: String).returns(T::Array[OpenStruct]) }
460
591
  def _codecommit_repo_contents(repo, path, commit)
461
592
  response = codecommit_client.fetch_repo_contents(
462
593
  repo,
@@ -474,11 +605,12 @@ module Dependabot
474
605
  end
475
606
  end
476
607
 
608
+ sig { params(path: String, fetch_submodules: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
477
609
  def _full_specification_for(path, fetch_submodules:)
478
610
  if fetch_submodules && _linked_dir_for(path)
479
611
  linked_dir_details = @linked_paths[_linked_dir_for(path)]
480
612
  sub_path =
481
- path.gsub(%r{^#{Regexp.quote(_linked_dir_for(path))}(/|$)}, "")
613
+ path.gsub(%r{^#{Regexp.quote(T.must(_linked_dir_for(path)))}(/|$)}, "")
482
614
  new_path =
483
615
  Pathname.new(File.join(linked_dir_details.fetch(:path), sub_path))
484
616
  .cleanpath.to_path
@@ -499,6 +631,7 @@ module Dependabot
499
631
  end
500
632
  end
501
633
 
634
+ sig { params(path: String, fetch_submodules: T::Boolean).returns(String) }
502
635
  def _fetch_file_content(path, fetch_submodules: false)
503
636
  path = path.gsub(%r{^/*}, "")
504
637
 
@@ -519,17 +652,18 @@ module Dependabot
519
652
  retry
520
653
  end
521
654
 
655
+ sig { params(provider: String, repo: String, path: String, commit: String).returns(String) }
522
656
  def _fetch_file_content_fully_specified(provider, repo, path, commit)
523
657
  case provider
524
658
  when "github"
525
659
  _fetch_file_content_from_github(path, repo, commit)
526
660
  when "gitlab"
527
- tmp = gitlab_client.get_file(repo, path, commit).content
661
+ tmp = T.unsafe(gitlab_client).get_file(repo, path, commit).content
528
662
  decode_binary_string(tmp)
529
663
  when "azure"
530
664
  azure_client.fetch_file_contents(commit, path)
531
665
  when "bitbucket"
532
- bitbucket_client.fetch_file_contents(repo, commit, path)
666
+ T.unsafe(bitbucket_client).fetch_file_contents(repo, commit, path)
533
667
  when "codecommit"
534
668
  codecommit_client.fetch_file_contents(repo, commit, path)
535
669
  else raise "Unsupported provider '#{source.provider}'."
@@ -537,8 +671,9 @@ module Dependabot
537
671
  end
538
672
 
539
673
  # rubocop:disable Metrics/AbcSize
674
+ sig { params(path: String, repo: String, commit: String).returns(String) }
540
675
  def _fetch_file_content_from_github(path, repo, commit)
541
- tmp = github_client.contents(repo, path: path, ref: commit)
676
+ tmp = T.unsafe(github_client).contents(repo, path: path, ref: commit)
542
677
 
543
678
  raise Octokit::NotFound if tmp.is_a?(Array)
544
679
 
@@ -549,7 +684,7 @@ module Dependabot
549
684
  commit: commit,
550
685
  path: Pathname.new(tmp.target).cleanpath.to_path
551
686
  }
552
- tmp = github_client.contents(
687
+ tmp = T.unsafe(github_client).contents(
553
688
  repo,
554
689
  path: Pathname.new(tmp.target).cleanpath.to_path,
555
690
  ref: commit
@@ -559,7 +694,7 @@ module Dependabot
559
694
  if tmp.content == ""
560
695
  # The file may have exceeded the 1MB limit
561
696
  # see https://github.blog/changelog/2022-05-03-increased-file-size-limit-when-retrieving-file-contents-via-rest-api/
562
- github_client.contents(repo, path: path, ref: commit, accept: "application/vnd.github.v3.raw")
697
+ T.unsafe(github_client).contents(repo, path: path, ref: commit, accept: "application/vnd.github.v3.raw")
563
698
  else
564
699
  decode_binary_string(tmp.content)
565
700
  end
@@ -573,7 +708,7 @@ module Dependabot
573
708
  file_details = repo_contents(dir: dir).find { |f| f.name == basename }
574
709
  raise unless file_details
575
710
 
576
- tmp = github_client.blob(repo, file_details.sha)
711
+ tmp = T.unsafe(github_client).blob(repo, file_details.sha)
577
712
  return tmp.content if tmp.encoding == "utf-8"
578
713
 
579
714
  decode_binary_string(tmp.content)
@@ -583,6 +718,7 @@ module Dependabot
583
718
  # Update the @linked_paths hash by exploiting a side-effect of
584
719
  # recursively calling `repo_contents` for each directory up the tree
585
720
  # until a submodule or symlink is found
721
+ sig { params(path: String).returns(T.nilable(T::Array[T.untyped])) }
586
722
  def _find_linked_dirs(path)
587
723
  path = Pathname.new(path).cleanpath.to_path.gsub(%r{^/*}, "")
588
724
  dir = File.dirname(path)
@@ -597,6 +733,7 @@ module Dependabot
597
733
  )
598
734
  end
599
735
 
736
+ sig { params(path: String).returns(T.nilable(String)) }
600
737
  def _linked_dir_for(path)
601
738
  linked_dirs = @linked_paths.keys
602
739
  linked_dirs
@@ -608,6 +745,7 @@ module Dependabot
608
745
  # rubocop:disable Metrics/MethodLength
609
746
  # rubocop:disable Metrics/PerceivedComplexity
610
747
  # rubocop:disable Metrics/BlockLength
748
+ sig { params(target_directory: T.nilable(String)).returns(String) }
611
749
  def _clone_repo_contents(target_directory:)
612
750
  SharedHelpers.with_git_configured(credentials: credentials) do
613
751
  path = target_directory || File.join("tmp", source.repo)
@@ -633,11 +771,13 @@ module Dependabot
633
771
  git clone #{clone_options.string} #{source.url} #{path}
634
772
  CMD
635
773
  )
774
+
775
+ @submodules = find_submodules(path) if recurse_submodules_when_cloning?
636
776
  rescue SharedHelpers::HelperSubprocessFailed => e
637
777
  raise unless e.message.match(GIT_SUBMODULE_ERROR_REGEX) && e.message.downcase.include?("submodule")
638
778
 
639
779
  submodule_cloning_failed = true
640
- match = e.message.match(GIT_SUBMODULE_ERROR_REGEX)
780
+ match = T.must(e.message.match(GIT_SUBMODULE_ERROR_REGEX))
641
781
  url = match.named_captures["url"]
642
782
  code = match.named_captures["code"]
643
783
 
@@ -680,10 +820,27 @@ module Dependabot
680
820
  # rubocop:enable Metrics/PerceivedComplexity
681
821
  # rubocop:enable Metrics/BlockLength
682
822
 
823
+ sig { params(str: String).returns(String) }
683
824
  def decode_binary_string(str)
684
825
  bom = (+"\xEF\xBB\xBF").force_encoding(Encoding::BINARY)
685
826
  Base64.decode64(str).delete_prefix(bom).force_encoding("UTF-8").encode
686
827
  end
828
+
829
+ sig { params(path: String).returns(T::Array[String]) }
830
+ def find_submodules(path)
831
+ SharedHelpers.run_shell_command(
832
+ <<~CMD
833
+ git -C #{path} ls-files --stage
834
+ CMD
835
+ ).split("\n").filter_map do |line|
836
+ info = line.split
837
+
838
+ type = info.first
839
+ path = T.must(info.last)
840
+
841
+ next path if type == DependencyFile::Mode::SUBMODULE
842
+ end
843
+ end
687
844
  end
688
845
  end
689
846
  end