Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error message when git ref cannot be fetched #1826

Merged
merged 7 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 52 additions & 25 deletions crates/uv-git/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, str};

use anyhow::{anyhow, Context as _, Result};
use anyhow::{anyhow, Context, Result};
use cargo_util::{paths, ProcessBuilder};
use git2::{self, ErrorClass, ObjectType};
use reqwest::Client;
use reqwest::StatusCode;
use tracing::{debug, error, warn};
use tracing::{debug, warn};
use url::Url;
use uv_fs::Normalized;

Expand Down Expand Up @@ -78,6 +78,18 @@ impl GitReference {
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.
Expand Down Expand Up @@ -245,6 +257,7 @@ impl GitDatabase {
impl GitReference {
/// Resolves self to an object ID with objects the `repo` currently has.
pub(crate) fn resolve(&self, repo: &git2::Repository) -> Result<git2::Oid> {
let refkind = self.kind_str();
let id = match self {
// Note that we resolve the named tag here in sync with where it's
// fetched into via `fetch` below.
Expand All @@ -255,18 +268,18 @@ impl GitReference {
let obj = obj.peel(ObjectType::Commit)?;
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
// `fetch` below.
GitReference::Branch(s) => {
let name = format!("origin/{s}");
let b = repo
.find_branch(&name, git2::BranchType::Remote)
.with_context(|| format!("failed to find branch `{s}`"))?;
.with_context(|| format!("failed to find {refkind} `{s}`"))?;
b.get()
.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.
Expand All @@ -283,7 +296,7 @@ impl GitReference {
let obj = obj.peel(ObjectType::Commit).ok()?;
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
Expand Down Expand Up @@ -980,36 +993,50 @@ pub(crate) fn fetch(
debug!("Performing a Git fetch for: {remote_url}");
match strategy {
FetchStrategy::Cli => {
match refspec_strategy {
let result = match refspec_strategy {
RefspecStrategy::All => fetch_with_cli(repo, remote_url, refspecs.as_slice(), tags),
RefspecStrategy::First => {
let num_refspecs = refspecs.len();

// Try each refspec
let errors = refspecs
.into_iter()
.map(|refspec| {
(
refspec.clone(),
fetch_with_cli(repo, remote_url, &[refspec], tags),
)
let mut errors = refspecs
.iter()
.map_while(|refspec| {
let fetch_result =
fetch_with_cli(repo, remote_url, &[refspec.clone()], 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<_>>();

if errors.len() == num_refspecs {
// If all of the fetches failed, report to the user
for (refspec, err) in errors {
if let Err(err) = err {
error!("failed to fetch refspec `{refspec}`: {err}");
}
if errors.len() == refspecs.len() {
if let Some(result) = errors.pop() {
// Use the last error for the message
result
} else {
// Can only occur if there were no refspecs to fetch
Ok(())
}
Err(anyhow!("failed to fetch all refspecs"))
} else {
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 => {
Expand Down
16 changes: 8 additions & 8 deletions crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)> {
zanieb marked this conversation as resolved.
Show resolved Hide resolved
let mut filters = INSTA_FILTERS.to_vec();

for (pattern, replacement) in &self.filters {
filters.push((pattern, replacement));
}

filters
// Put test context snapshots before the default filters
// This ensures we don't replace other patterns inside paths from the test context first
self.filters
.iter()
.map(|(p, r)| (p.as_str(), r.as_str()))
.chain(INSTA_FILTERS.iter().copied())
.collect()
}
}

Expand Down
46 changes: 43 additions & 3 deletions crates/uv/tests/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,10 +815,15 @@ fn install_git_public_https() {
/// 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_ref() {
fn install_git_public_https_missing_branch_or_tag() {
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
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0")
, @r###"
Expand All @@ -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
Caused by: Git operation failed
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

"###);
}

Expand Down