Skip to content

Commit 9c4bae6

Browse files
committed
Auto merge of #10829 - ehuss:corrupted-checkout, r=weihanglo
Fix corrupted git checkout recovery. This fixes an issue where cargo would not recover from a corrupted git checkout correctly when using `net.git-fetch-with-cli`. Git dependencies have two clones, the "db" and the "checkout". The "db" is shared amongst multiple checkout revisions from the same repository. The "checkout" is each individual revision. There was some code in `copy_to` which creates the "checkout" that tries to recover from an error. The "checkout" can be invalid if cargo was interrupted while cloning it, or if there is fs corruption. However, that code was failing when using the git CLI. For reasons I did not dig into, the "db" does not have a HEAD ref, so that special-case fetch was failing with a `couldn't find remote ref HEAD` error from `git`. This changes it so that if the "checkout" is invalid, it just gets blown away and a new clone is created (instead of calling `git fetch` from the "db"). I believe there is some long history for this `copy_to` code where it made more sense in the past. Previously, the "checkout" directories used the `GitReference` string as-is. So, for example, a branch would checkout into a directory with that branch name. At some point, it was changed so that each checkout uses a short hash of the actual revision. Rebuilding the checkout made sense when it was possible for that checkout revision to change (like if new commits were pushed to a branch). That recovery is no longer necessary since a checkout is only ever one revision. Fixes #10826
2 parents ca190ac + c46e57b commit 9c4bae6

File tree

2 files changed

+72
-32
lines changed

2 files changed

+72
-32
lines changed

src/cargo/sources/git/utils.rs

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -151,30 +151,16 @@ impl GitDatabase {
151151
dest: &Path,
152152
cargo_config: &Config,
153153
) -> CargoResult<GitCheckout<'_>> {
154-
let mut checkout = None;
155-
if let Ok(repo) = git2::Repository::open(dest) {
156-
let mut co = GitCheckout::new(dest, self, rev, repo);
157-
if !co.is_fresh() {
158-
// After a successful fetch operation the subsequent reset can
159-
// fail sometimes for corrupt repositories where the fetch
160-
// operation succeeds but the object isn't actually there in one
161-
// way or another. In these situations just skip the error and
162-
// try blowing away the whole repository and trying with a
163-
// clone.
164-
co.fetch(cargo_config)?;
165-
match co.reset(cargo_config) {
166-
Ok(()) => {
167-
assert!(co.is_fresh());
168-
checkout = Some(co);
169-
}
170-
Err(e) => debug!("failed reset after fetch {:?}", e),
171-
}
172-
} else {
173-
checkout = Some(co);
174-
}
175-
};
176-
let checkout = match checkout {
177-
Some(c) => c,
154+
// If the existing checkout exists, and it is fresh, use it.
155+
// A non-fresh checkout can happen if the checkout operation was
156+
// interrupted. In that case, the checkout gets deleted and a new
157+
// clone is created.
158+
let checkout = match git2::Repository::open(dest)
159+
.ok()
160+
.map(|repo| GitCheckout::new(dest, self, rev, repo))
161+
.filter(|co| co.is_fresh())
162+
{
163+
Some(co) => co,
178164
None => GitCheckout::clone_into(dest, self, rev, cargo_config)?,
179165
};
180166
checkout.update_submodules(cargo_config)?;
@@ -311,14 +297,6 @@ impl<'a> GitCheckout<'a> {
311297
}
312298
}
313299

314-
fn fetch(&mut self, cargo_config: &Config) -> CargoResult<()> {
315-
info!("fetch {}", self.repo.path().display());
316-
let url = self.database.path.into_url()?;
317-
let reference = GitReference::Rev(self.revision.to_string());
318-
fetch(&mut self.repo, url.as_str(), &reference, cargo_config)?;
319-
Ok(())
320-
}
321-
322300
fn reset(&self, config: &Config) -> CargoResult<()> {
323301
// If we're interrupted while performing this reset (e.g., we die because
324302
// of a signal) Cargo needs to be sure to try to check out this repo

tests/testsuite/git.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3510,3 +3510,65 @@ fn git_with_force_push() {
35103510
amend_commit("awesome-four");
35113511
verify("awesome-four");
35123512
}
3513+
3514+
#[cargo_test]
3515+
fn corrupted_checkout() {
3516+
// Test what happens if the checkout is corrupted somehow.
3517+
_corrupted_checkout(false);
3518+
}
3519+
3520+
#[cargo_test]
3521+
fn corrupted_checkout_with_cli() {
3522+
// Test what happens if the checkout is corrupted somehow with git cli.
3523+
if disable_git_cli() {
3524+
return;
3525+
}
3526+
_corrupted_checkout(true);
3527+
}
3528+
3529+
fn _corrupted_checkout(with_cli: bool) {
3530+
let git_project = git::new("dep1", |project| {
3531+
project
3532+
.file("Cargo.toml", &basic_manifest("dep1", "0.5.0"))
3533+
.file("src/lib.rs", "")
3534+
});
3535+
let p = project()
3536+
.file(
3537+
"Cargo.toml",
3538+
&format!(
3539+
r#"
3540+
[package]
3541+
name = "foo"
3542+
version = "0.1.0"
3543+
3544+
[dependencies]
3545+
dep1 = {{ git = "{}" }}
3546+
"#,
3547+
git_project.url()
3548+
),
3549+
)
3550+
.file("src/lib.rs", "")
3551+
.build();
3552+
3553+
p.cargo("fetch").run();
3554+
3555+
let mut paths = t!(glob::glob(
3556+
paths::home()
3557+
.join(".cargo/git/checkouts/dep1-*/*")
3558+
.to_str()
3559+
.unwrap()
3560+
));
3561+
let path = paths.next().unwrap().unwrap();
3562+
let ok = path.join(".cargo-ok");
3563+
3564+
// Deleting this file simulates an interrupted checkout.
3565+
t!(fs::remove_file(&ok));
3566+
3567+
// This should refresh the checkout.
3568+
let mut e = p.cargo("fetch");
3569+
if with_cli {
3570+
e.env("CARGO_NET_GIT_FETCH_WITH_CLI", "true");
3571+
}
3572+
e.run();
3573+
assert!(ok.exists());
3574+
}

0 commit comments

Comments
 (0)