Repository: astral-sh/uv (Rust)

Issue:
Add support for Git revision references in VCS URLs
See: https://pip.pypa.io/en/stable/topics/vcs-support. We need to support, e.g., `MyProject @ git+https://git.example.com/MyProject.git@v1.0`. I didn't realize that pip expects a different format here than Cargo, which uses query parameters.



Fix (patch):
diff --git a/crates/puffin-git/src/git.rs b/crates/puffin-git/src/git.rs
index ebc43b3d3014b..a2d2426c04220 100644
--- a/crates/puffin-git/src/git.rs
+++ b/crates/puffin-git/src/git.rs
@@ -15,12 +15,40 @@ use tracing::{debug, info, warn};
 use url::Url;
 
 use crate::util::retry;
-use crate::{FetchStrategy, GitReference};
+use crate::FetchStrategy;
 
 /// A file indicates that if present, `git reset` has been done and a repo
 /// checkout is ready to go. See [`GitCheckout::reset`] for why we need this.
 const CHECKOUT_READY_LOCK: &str = ".ok";
 
+/// A reference to commit or commit-ish.
+#[derive(Debug, Clone)]
+pub(crate) enum GitReference {
+    /// From a branch.
+    #[allow(unused)]
+    Branch(String),
+    /// From a tag.
+    #[allow(unused)]
+    Tag(String),
+    /// From a reference that's ambiguously a branch or tag.
+    BranchOrTag(String),
+    /// From a specific revision. Can be a commit hash (either short or full),
+    /// or a named reference like `refs/pull/493/head`.
+    Rev(String),
+    /// The default branch of the repository, the reference named `HEAD`.
+    DefaultBranch,
+}
+
+impl GitReference {
+    pub(crate) fn from_rev(rev: &str) -> Self {
+        if looks_like_commit_hash(rev) || rev.starts_with("refs/") {
+            Self::Rev(rev.to_owned())
+        } else {
+            Self::BranchOrTag(rev.to_owned())
+        }
+    }
+}
+
 /// A short abbreviated OID.
 ///
 /// Exists for avoiding extra allocations in [`GitDatabase::to_short_id`].
@@ -210,6 +238,23 @@ impl GitReference {
                     .ok_or_else(|| anyhow::format_err!("branch `{s}` did not have a target"))?
             }
 
+            // Attempt to resolve the branch, then the tag.
+            GitReference::BranchOrTag(s) => {
+                let name = format!("origin/{s}");
+
+                repo.find_branch(&name, git2::BranchType::Remote)
+                    .ok()
+                    .and_then(|b| b.get().target())
+                    .or_else(|| {
+                        let refname = format!("refs/remotes/origin/tags/{s}");
+                        let id = repo.refname_to_id(&refname).ok()?;
+                        let obj = repo.find_object(id, None).ok()?;
+                        let obj = obj.peel(ObjectType::Commit).ok()?;
+                        Some(obj.id())
+                    })
+                    .ok_or_else(|| anyhow::format_err!("failed to find branch or tag `{s}`"))?
+            }
+
             // We'll be using the HEAD commit
             GitReference::DefaultBranch => {
                 let head_id = repo.refname_to_id("refs/remotes/origin/HEAD")?;
@@ -859,6 +904,15 @@ pub(crate) fn fetch(
             refspecs.push(format!("+refs/tags/{tag}:refs/remotes/origin/tags/{tag}"));
         }
 
+        GitReference::BranchOrTag(branch_or_tag) => {
+            refspecs.push(format!(
+                "+refs/heads/{branch_or_tag}:refs/remotes/origin/{branch_or_tag}"
+            ));
+            refspecs.push(format!(
+                "+refs/tags/{branch_or_tag}:refs/remotes/origin/tags/{branch_or_tag}"
+            ));
+        }
+
         GitReference::DefaultBranch => {
             refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
         }
@@ -1158,6 +1212,7 @@ fn github_fast_path(
     let github_branch_name = match reference {
         GitReference::Branch(branch) => branch,
         GitReference::Tag(tag) => tag,
+        GitReference::BranchOrTag(branch_or_tag) => branch_or_tag,
         GitReference::DefaultBranch => "HEAD",
         GitReference::Rev(rev) => {
             if rev.starts_with("refs/") {
diff --git a/crates/puffin-git/src/lib.rs b/crates/puffin-git/src/lib.rs
index a075923960222..07ab1f262f162 100644
--- a/crates/puffin-git/src/lib.rs
+++ b/crates/puffin-git/src/lib.rs
@@ -1,5 +1,7 @@
 use url::Url;
 
+use crate::git::GitReference;
+
 pub use self::source::GitSource;
 
 mod git;
@@ -22,24 +24,22 @@ impl TryFrom<Url> for Git {
 
     /// Initialize a [`Git`] source from a URL.
     fn try_from(mut url: Url) -> Result<Self, Self::Error> {
-        let mut reference = GitReference::DefaultBranch;
-        for (k, v) in url.query_pairs() {
-            match &k[..] {
-                // Map older 'ref' to branch.
-                "branch" | "ref" => reference = GitReference::Branch(v.into_owned()),
-                "rev" => reference = GitReference::Rev(v.into_owned()),
-                "tag" => reference = GitReference::Tag(v.into_owned()),
-                _ => {}
-            }
-        }
-        let precise = url.fragment().map(git2::Oid::from_str).transpose()?;
+        // Remove any query parameters and fragments.
         url.set_fragment(None);
         url.set_query(None);
 
+        // If the URL ends with a reference, like `https://git.example.com/MyProject.git@v1.0`,
+        // extract it.
+        let mut reference = GitReference::DefaultBranch;
+        if let Some((prefix, rev)) = url.as_str().rsplit_once('@') {
+            reference = GitReference::from_rev(rev);
+            url = Url::parse(prefix)?;
+        }
+
         Ok(Self {
             url,
             reference,
-            precise,
+            precise: None,
         })
     }
 }
@@ -50,20 +50,6 @@ impl std::fmt::Display for Git {
     }
 }
 
-/// Information to find a specific commit in a Git repository.
-#[derive(Debug, Clone)]
-pub enum GitReference {
-    /// From a tag.
-    Tag(String),
-    /// From a branch.
-    Branch(String),
-    /// From a specific revision. Can be a commit hash (either short or full),
-    /// or a named reference like `refs/pull/493/head`.
-    Rev(String),
-    /// The default branch of the repository, the reference named `HEAD`.
-    DefaultBranch,
-}
-
 #[derive(Debug, Clone, Copy)]
 pub enum FetchStrategy {
     /// Fetch Git repositories using libgit2.
diff --git a/crates/puffin-git/src/source.rs b/crates/puffin-git/src/source.rs
index 4f8ae3328b34d..412492674c743 100644
--- a/crates/puffin-git/src/source.rs
+++ b/crates/puffin-git/src/source.rs
@@ -9,8 +9,8 @@ use tracing::debug;
 
 use puffin_cache::{digest, CanonicalUrl};
 
-use crate::git::GitRemote;
-use crate::{FetchStrategy, Git, GitReference};
+use crate::git::{GitReference, GitRemote};
+use crate::{FetchStrategy, Git};
 
 /// A remote Git source that can be checked out locally.
 pub struct GitSource {