mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-11-03 05:03:46 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									f441f8fa9b
								
							
						
					
					
						commit
						10be62e9d3
					
				
					 3 changed files with 103 additions and 36 deletions
				
			
		| 
						 | 
					@ -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 => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    "###);
 | 
					    "###);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue