Respect pip-like Git branch, tag, and commit references (#297)

We need to parse revisions out from URLs like `MyProject @
git+https://git.example.com/MyProject.git@v1.0`, per [VCS
Support](https://pip.pypa.io/en/stable/topics/vcs-support/). Cargo has
the advantage that it uses a TOML table in its configuration, so the
user has to specify whether they're fetching a commit, a tag, a branch,
etc. We have to instead assume that anything that isn't clearly a commit
is _either_ a branch or a tag.

Closes https://github.com/astral-sh/puffin/issues/296.
This commit is contained in:
Charlie Marsh
2023-11-02 12:10:02 -07:00
committed by GitHub
parent a4002fe132
commit e47d3f1f66
8 changed files with 370 additions and 29 deletions
+56 -1
View File
@@ -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/") {