Improve error message when git ref cannot be fetched (#1826)

Follow-up to #1781 improving the error message when a ref cannot be
fetched
This commit is contained in:
Zanie Blue 2024-02-21 19:22:00 -06:00 committed by GitHub
parent f441f8fa9b
commit 10be62e9d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 103 additions and 36 deletions

View file

@ -6,12 +6,12 @@ use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::{env, str}; use std::{env, str};
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context, Result};
use cargo_util::{paths, ProcessBuilder}; use cargo_util::{paths, ProcessBuilder};
use git2::{self, ErrorClass, ObjectType}; use git2::{self, ErrorClass, ObjectType};
use reqwest::Client; use reqwest::Client;
use reqwest::StatusCode; use reqwest::StatusCode;
use tracing::{debug, error, warn}; use tracing::{debug, warn};
use url::Url; use url::Url;
use uv_fs::Normalized; use uv_fs::Normalized;
@ -78,6 +78,18 @@ impl GitReference {
GitReference::DefaultBranch => "HEAD", GitReference::DefaultBranch => "HEAD",
} }
} }
pub(crate) fn kind_str(&self) -> &str {
match self {
GitReference::Branch(_) => "branch",
GitReference::Tag(_) => "tag",
GitReference::BranchOrTag(_) => "branch or tag",
GitReference::FullCommit(_) => "commit",
GitReference::ShortCommit(_) => "short commit",
GitReference::Ref(_) => "ref",
GitReference::DefaultBranch => "default branch",
}
}
} }
/// A short abbreviated OID. /// A short abbreviated OID.
@ -245,6 +257,7 @@ impl GitDatabase {
impl GitReference { impl GitReference {
/// Resolves self to an object ID with objects the `repo` currently has. /// Resolves self to an object ID with objects the `repo` currently has.
pub(crate) fn resolve(&self, repo: &git2::Repository) -> Result<git2::Oid> { pub(crate) fn resolve(&self, repo: &git2::Repository) -> Result<git2::Oid> {
let refkind = self.kind_str();
let id = match self { let id = match self {
// Note that we resolve the named tag here in sync with where it's // Note that we resolve the named tag here in sync with where it's
// fetched into via `fetch` below. // fetched into via `fetch` below.
@ -255,7 +268,7 @@ impl GitReference {
let obj = obj.peel(ObjectType::Commit)?; let obj = obj.peel(ObjectType::Commit)?;
Ok(obj.id()) Ok(obj.id())
})() })()
.with_context(|| format!("failed to find tag `{s}`"))?, .with_context(|| format!("failed to find {refkind} `{s}`"))?,
// Resolve the remote name since that's all we're configuring in // Resolve the remote name since that's all we're configuring in
// `fetch` below. // `fetch` below.
@ -263,10 +276,10 @@ impl GitReference {
let name = format!("origin/{s}"); let name = format!("origin/{s}");
let b = repo let b = repo
.find_branch(&name, git2::BranchType::Remote) .find_branch(&name, git2::BranchType::Remote)
.with_context(|| format!("failed to find branch `{s}`"))?; .with_context(|| format!("failed to find {refkind} `{s}`"))?;
b.get() b.get()
.target() .target()
.ok_or_else(|| anyhow::format_err!("branch `{s}` did not have a target"))? .ok_or_else(|| anyhow::format_err!("{refkind} `{s}` did not have a target"))?
} }
// Attempt to resolve the branch, then the tag. // Attempt to resolve the branch, then the tag.
@ -283,7 +296,7 @@ impl GitReference {
let obj = obj.peel(ObjectType::Commit).ok()?; let obj = obj.peel(ObjectType::Commit).ok()?;
Some(obj.id()) Some(obj.id())
}) })
.ok_or_else(|| anyhow::format_err!("failed to find branch or tag `{s}`"))? .ok_or_else(|| anyhow::format_err!("failed to find {refkind} `{s}`"))?
} }
// We'll be using the HEAD commit // We'll be using the HEAD commit
@ -980,36 +993,50 @@ pub(crate) fn fetch(
debug!("Performing a Git fetch for: {remote_url}"); debug!("Performing a Git fetch for: {remote_url}");
match strategy { match strategy {
FetchStrategy::Cli => { FetchStrategy::Cli => {
match refspec_strategy { let result = match refspec_strategy {
RefspecStrategy::All => fetch_with_cli(repo, remote_url, refspecs.as_slice(), tags), RefspecStrategy::All => fetch_with_cli(repo, remote_url, refspecs.as_slice(), tags),
RefspecStrategy::First => { RefspecStrategy::First => {
let num_refspecs = refspecs.len();
// Try each refspec // Try each refspec
let errors = refspecs let mut errors = refspecs
.into_iter() .iter()
.map(|refspec| { .map_while(|refspec| {
( let fetch_result =
refspec.clone(), fetch_with_cli(repo, remote_url, &[refspec.clone()], tags);
fetch_with_cli(repo, remote_url, &[refspec], tags),
) // Stop after the first success and log failures
match fetch_result {
Err(ref err) => {
debug!("failed to fetch refspec `{refspec}`: {err}");
Some(fetch_result)
}
Ok(()) => None,
}
}) })
// Stop after the first success
.take_while(|(_, result)| result.is_err())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if errors.len() == num_refspecs { if errors.len() == refspecs.len() {
// If all of the fetches failed, report to the user if let Some(result) = errors.pop() {
for (refspec, err) in errors { // Use the last error for the message
if let Err(err) = err { result
error!("failed to fetch refspec `{refspec}`: {err}"); } else {
// Can only occur if there were no refspecs to fetch
Ok(())
} }
}
Err(anyhow!("failed to fetch all refspecs"))
} else { } else {
Ok(()) Ok(())
} }
} }
};
match reference {
// With the default branch, adding context is confusing
GitReference::DefaultBranch => result,
_ => result.with_context(|| {
format!(
"failed to fetch {} `{}`",
reference.kind_str(),
reference.as_str()
)
}),
} }
} }
FetchStrategy::Libgit2 => { FetchStrategy::Libgit2 => {

View file

@ -148,15 +148,15 @@ impl TestContext {
] ]
} }
/// Canonical snapshot filters for this test context. /// Standard snapshot filters _plus_ those for this test context.
pub fn filters(&self) -> Vec<(&str, &str)> { pub fn filters(&self) -> Vec<(&str, &str)> {
let mut filters = INSTA_FILTERS.to_vec(); // Put test context snapshots before the default filters
// This ensures we don't replace other patterns inside paths from the test context first
for (pattern, replacement) in &self.filters { self.filters
filters.push((pattern, replacement)); .iter()
} .map(|(p, r)| (p.as_str(), r.as_str()))
.chain(INSTA_FILTERS.iter().copied())
filters .collect()
} }
} }

View file

@ -815,10 +815,15 @@ fn install_git_public_https() {
/// Install a package from a public GitHub repository at a ref that does not exist /// Install a package from a public GitHub repository at a ref that does not exist
#[test] #[test]
#[cfg(feature = "git")] #[cfg(feature = "git")]
fn install_git_public_https_missing_ref() { fn install_git_public_https_missing_branch_or_tag() {
let context = TestContext::new("3.8"); let context = TestContext::new("3.8");
uv_snapshot!(context.filters(), command(&context) let mut filters = context.filters();
// Windows does not style the command the same as Unix, so we must omit it from the snapshot
filters.push(("`git fetch .*`", "`git fetch [...]`"));
filters.push(("exit status", "exit code"));
uv_snapshot!(filters, command(&context)
// 2.0.0 does not exist // 2.0.0 does not exist
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0") .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0")
, @r###" , @r###"
@ -830,7 +835,42 @@ fn install_git_public_https_missing_ref() {
error: Failed to download and build: uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0 error: Failed to download and build: uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0
Caused by: Git operation failed Caused by: Git operation failed
Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/8dab139913c4b566 Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/8dab139913c4b566
Caused by: failed to fetch all refspecs Caused by: failed to fetch branch or tag `2.0.0`
Caused by: process didn't exit successfully: `git fetch [...]` (exit code: 128)
--- stderr
fatal: couldn't find remote ref refs/tags/2.0.0
"###);
}
/// Install a package from a public GitHub repository at a ref that does not exist
#[test]
#[cfg(feature = "git")]
fn install_git_public_https_missing_commit() {
let context = TestContext::new("3.8");
let mut filters = context.filters();
// Windows does not style the command the same as Unix, so we must omit it from the snapshot
filters.push(("`git fetch .*`", "`git fetch [...]`"));
filters.push(("exit status", "exit code"));
uv_snapshot!(filters, command(&context)
// 2.0.0 does not exist
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b")
, @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to download and build: uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b
Caused by: Git operation failed
Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/8dab139913c4b566
Caused by: failed to fetch commit `79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b`
Caused by: process didn't exit successfully: `git fetch [...]` (exit code: 128)
--- stderr
fatal: remote error: upload-pack: not our ref 79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b
"###); "###);
} }