Skip to content

Commit 130dbb9

Browse files
authored
Fix Cargo config file fetching from parent directories (#13790)
Cargo searches for .cargo/config.toml hierarchically from the current directory up to the repository root. This fix implements the parent directory search functionality to properly locate config files that exist in parent directories. The implementation walks up the directory tree from the job directory, trying to fetch .cargo/config.toml (and the legacy .cargo/config) from each parent level until one is found or the repository root is reached. Closes #13523
1 parent 5824aca commit 130dbb9

File tree

2 files changed

+133
-18
lines changed

2 files changed

+133
-18
lines changed

cargo/lib/dependabot/cargo/file_fetcher.rb

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# https://doc.rust-lang.org/cargo/reference/manifest.html#the-workspace-section
1515
module Dependabot
1616
module Cargo
17-
class FileFetcher < Dependabot::FileFetchers::Base
17+
class FileFetcher < Dependabot::FileFetchers::Base # rubocop:disable Metrics/ClassLength
1818
extend T::Sig
1919
extend T::Helpers
2020

@@ -50,13 +50,11 @@ def fetch_files
5050
fetched_files << T.must(cargo_config) if cargo_config
5151
fetched_files << T.must(rust_toolchain) if rust_toolchain
5252
fetched_files += fetch_path_dependency_and_workspace_files
53-
# If the main Cargo.toml uses workspace dependencies, ensure we have the workspace root
5453
parsed_manifest = parsed_file(cargo_toml)
5554
if uses_workspace_dependencies?(parsed_manifest) || workspace_member?(parsed_manifest)
5655
workspace_root = find_workspace_root(cargo_toml)
5756
fetched_files << workspace_root if workspace_root && !fetched_files.include?(workspace_root)
5857
end
59-
# Filter excluded files from final collection
6058
fetched_files.reject do |file|
6159
Dependabot::FileFiltering.should_exclude_path?(file.name, "file from final collection", @exclude_paths)
6260
end.uniq
@@ -70,20 +68,17 @@ def fetch_files
7068
def fetch_path_dependency_and_workspace_files(files = nil)
7169
fetched_files = files || [cargo_toml]
7270
fetched_files += path_dependency_files(fetched_files)
73-
fetched_files += fetched_files.flat_map { |f| workspace_files(f) }
71+
@workspace_files ||= T.let({}, T.nilable(T::Hash[String, T::Array[Dependabot::DependencyFile]]))
72+
fetched_files += fetched_files.flat_map do |f|
73+
@workspace_files[f.name] ||= fetch_workspace_files(file: f, previously_fetched_files: [])
74+
end
7475
updated_files = fetched_files.reject(&:support_file?).uniq
7576
updated_files += fetched_files.uniq.reject { |f| updated_files.map(&:name).include?(f.name) }
7677
return updated_files if updated_files == files
7778

7879
fetch_path_dependency_and_workspace_files(updated_files)
7980
end
8081

81-
sig { params(cargo_toml: Dependabot::DependencyFile).returns(T::Array[Dependabot::DependencyFile]) }
82-
def workspace_files(cargo_toml)
83-
@workspace_files ||= T.let({}, T.nilable(T::Hash[String, T::Array[Dependabot::DependencyFile]]))
84-
@workspace_files[cargo_toml.name] ||= fetch_workspace_files(file: cargo_toml, previously_fetched_files: [])
85-
end
86-
8782
sig { params(fetched_files: T::Array[Dependabot::DependencyFile]).returns(T::Array[Dependabot::DependencyFile]) }
8883
def path_dependency_files(fetched_files)
8984
@path_dependency_files ||= T.let({}, T.nilable(T::Hash[String, T::Array[Dependabot::DependencyFile]]))
@@ -442,15 +437,17 @@ def cargo_config
442437
fetch_support_file(".cargo/config")&.tap { |f| f.name = ".cargo/config.toml" },
443438
T.nilable(Dependabot::DependencyFile)
444439
)
440+
@cargo_config ||= T.let(
441+
fetch_cargo_config_from_parent_dirs,
442+
T.nilable(Dependabot::DependencyFile)
443+
)
445444
end
446445

447446
sig { returns(T.nilable(Dependabot::DependencyFile)) }
448447
def rust_toolchain
449448
return @rust_toolchain if defined?(@rust_toolchain)
450449

451450
@rust_toolchain = fetch_support_file("rust-toolchain")
452-
# Per https://rust-lang.github.io/rustup/overrides.html the file can have a `.toml` extension,
453-
# but the non-extension version is preferred. Renaming here to simplify finding it later in the code.
454451
@rust_toolchain ||= T.let(
455452
fetch_support_file("rust-toolchain.toml")&.tap { |f| f.name = "rust-toolchain" },
456453
T.nilable(Dependabot::DependencyFile)
@@ -459,9 +456,7 @@ def rust_toolchain
459456

460457
sig { override.params(filename: T.any(Pathname, String)).returns(Dependabot::DependencyFile) }
461458
def load_cloned_file_if_present(filename)
462-
file = super
463-
file.name = Pathname.new(file.name).cleanpath.to_s.gsub(%r{^/+}, "")
464-
file
459+
super.tap { |f| f.name = Pathname.new(f.name).cleanpath.to_s.gsub(%r{^/+}, "") }
465460
end
466461

467462
sig do
@@ -472,9 +467,45 @@ def load_cloned_file_if_present(filename)
472467
).returns(Dependabot::DependencyFile)
473468
end
474469
def fetch_file_from_host(filename, type: "file", fetch_submodules: false)
475-
file = super
476-
file.name = Pathname.new(file.name).cleanpath.to_s.gsub(%r{^/+}, "")
477-
file
470+
super.tap { |f| f.name = Pathname.new(f.name).cleanpath.to_s.gsub(%r{^/+}, "") }
471+
end
472+
473+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
474+
def fetch_cargo_config_from_parent_dirs
475+
return nil if directory.empty?
476+
477+
# Count directory depth to determine how many levels to search up
478+
depth = directory.split("/").count { |s| !s.empty? }
479+
return nil if depth.zero?
480+
481+
# Try each parent directory level
482+
depth.times do |i|
483+
parent_path = ([".."] * (i + 1)).join("/")
484+
config = try_fetch_config_at_path(parent_path)
485+
return config if config
486+
end
487+
488+
nil
489+
end
490+
491+
sig { params(parent_path: String).returns(T.nilable(Dependabot::DependencyFile)) }
492+
def try_fetch_config_at_path(parent_path)
493+
[".cargo/config.toml", ".cargo/config"].each do |config_name|
494+
full_path = File.join(parent_path, config_name)
495+
Dependabot.logger.debug("Attempting to fetch config from: #{full_path}")
496+
config = fetch_file_from_host(
497+
full_path,
498+
fetch_submodules: false
499+
)
500+
Dependabot.logger.debug("Successfully fetched config from: #{full_path}")
501+
config.support_file = true
502+
config.name = ".cargo/config.toml"
503+
return config
504+
rescue Dependabot::DependencyFileNotFound
505+
Dependabot.logger.debug("No config found at: #{full_path}")
506+
next
507+
end
508+
nil
478509
end
479510
end
480511
end

cargo/spec/dependabot/cargo/file_fetcher_spec.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,84 @@
148148
end
149149
end
150150

151+
context "with a config file at repository root and Cargo.toml in subdirectory" do
152+
let(:source) do
153+
Dependabot::Source.new(
154+
provider: "github",
155+
repo: "gocardless/bump",
156+
directory: "my_dir"
157+
)
158+
end
159+
160+
let(:url) do
161+
"https://api.github.com/repos/gocardless/bump/contents/my_dir/"
162+
end
163+
164+
before do
165+
# Mock the subdirectory listing (includes Cargo.toml and Cargo.lock)
166+
stub_request(:get, "https://api.github.com/repos/gocardless/bump/contents/my_dir?ref=sha")
167+
.with(headers: { "Authorization" => "token token" })
168+
.to_return(
169+
status: 200,
170+
body: fixture("github", "contents_cargo_with_lockfile.json"),
171+
headers: json_header
172+
)
173+
174+
# Mock Cargo.toml in subdirectory
175+
stub_request(:get, url + "Cargo.toml?ref=sha")
176+
.with(headers: { "Authorization" => "token token" })
177+
.to_return(
178+
status: 200,
179+
body: fixture("github", "contents_cargo_manifest.json"),
180+
headers: json_header
181+
)
182+
183+
# Mock Cargo.lock in subdirectory
184+
stub_request(:get, url + "Cargo.lock?ref=sha")
185+
.with(headers: { "Authorization" => "token token" })
186+
.to_return(
187+
status: 200,
188+
body: fixture("github", "contents_cargo_lockfile.json"),
189+
headers: json_header
190+
)
191+
192+
# No config in subdirectory's .cargo directory
193+
stub_request(:get, url + ".cargo?ref=sha")
194+
.with(headers: { "Authorization" => "token token" })
195+
.to_return(status: 404, headers: json_header)
196+
197+
stub_request(:get, url + ".cargo/config.toml?ref=sha")
198+
.with(headers: { "Authorization" => "token token" })
199+
.to_return(status: 404, headers: json_header)
200+
201+
stub_request(:get, url + ".cargo/config?ref=sha")
202+
.with(headers: { "Authorization" => "token token" })
203+
.to_return(status: 404, headers: json_header)
204+
205+
# Config at repository root
206+
stub_request(:get, "https://api.github.com/repos/gocardless/bump/contents/.cargo?ref=sha")
207+
.with(headers: { "Authorization" => "token token" })
208+
.to_return(
209+
status: 200,
210+
body: fixture("github", "contents_cargo_dir.json"),
211+
headers: json_header
212+
)
213+
214+
stub_request(:get, "https://api.github.com/repos/gocardless/bump/contents/.cargo/config.toml?ref=sha")
215+
.with(headers: { "Authorization" => "token token" })
216+
.to_return(
217+
status: 200,
218+
body: fixture("github", "contents_cargo_config.json"),
219+
headers: json_header
220+
)
221+
end
222+
223+
it "fetches the Cargo.toml, Cargo.lock, and config.toml from root" do
224+
expect(file_fetcher_instance.files.map(&:name))
225+
.to match_array(%w(Cargo.lock Cargo.toml .cargo/config.toml))
226+
end
227+
end
228+
151229
context "without a lockfile" do
152230
before do
153231
stub_request(:get, url + "?ref=sha")
@@ -825,6 +903,12 @@
825903
stub_request(:get, %r{#{Regexp.escape(url)}\w+/\.cargo\?ref=sha})
826904
.with(headers: { "Authorization" => "token token" })
827905
.to_return(status: 404, headers: json_header)
906+
stub_request(:get, %r{#{Regexp.escape(url)}.*\.cargo/config\.toml\?ref=sha})
907+
.with(headers: { "Authorization" => "token token" })
908+
.to_return(status: 404, headers: json_header)
909+
stub_request(:get, %r{#{Regexp.escape(url)}.*\.cargo/config\?ref=sha})
910+
.with(headers: { "Authorization" => "token token" })
911+
.to_return(status: 404, headers: json_header)
828912

829913
# All the manifest requests
830914
stub_request(:get, url + "detached_crate_fail_1/Cargo.toml?ref=sha")

0 commit comments

Comments
 (0)