dependabot-common 0.235.0 → 0.237.0

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 (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