Skip to main content

uv_git_types/
reference.rs

1use std::borrow::Cow;
2use std::fmt::Display;
3use std::str;
4
5use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
6
7use crate::GitOid;
8
9/// Percent-encode Git revisions for use after the `@` in VCS URLs.
10///
11/// This follows Python's `urllib.parse.quote(rev, safe="/")`.
12/// See: <https://docs.python.org/3/library/urllib.parse.html#urllib.parse.quote>
13const GIT_REFERENCE_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
14    .remove(b'/')
15    .remove(b'-')
16    .remove(b'.')
17    .remove(b'_')
18    .remove(b'~');
19
20/// A reference to commit or commit-ish.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub enum GitReference {
23    /// A specific commit, identified by its full object ID.
24    Commit(GitOid),
25    /// A specific branch.
26    Branch(String),
27    /// A specific tag.
28    Tag(String),
29    /// From a reference that's ambiguously a branch or tag.
30    BranchOrTag(String),
31    /// An ambiguous reference, including abbreviated commits and hexadecimal branch or tag names.
32    BranchOrTagOrCommit(String),
33    /// From a named reference, like `refs/pull/493/head`.
34    NamedRef(String),
35    /// The default branch of the repository, the reference named `HEAD`.
36    DefaultBranch,
37}
38
39impl GitReference {
40    /// Creates a [`GitReference`] from an arbitrary revision string, which could represent a
41    /// branch, tag, commit, or named ref.
42    pub fn from_rev(rev: String) -> Self {
43        if rev.starts_with("refs/") {
44            Self::NamedRef(rev)
45        } else if let Ok(commit) = rev.parse() {
46            Self::Commit(commit)
47        } else if looks_like_commit_hash(&rev) {
48            Self::BranchOrTagOrCommit(rev)
49        } else {
50            Self::BranchOrTag(rev)
51        }
52    }
53
54    /// Converts the [`GitReference`] to a `str`.
55    pub fn as_str(&self) -> Option<&str> {
56        match self {
57            Self::Commit(commit) => Some(commit.as_str()),
58            Self::Tag(rev) => Some(rev),
59            Self::Branch(rev) => Some(rev),
60            Self::BranchOrTag(rev) => Some(rev),
61            Self::BranchOrTagOrCommit(rev) => Some(rev),
62            Self::NamedRef(rev) => Some(rev),
63            Self::DefaultBranch => None,
64        }
65    }
66
67    /// Converts the [`GitReference`] to a `str` that can be used as a revision.
68    pub fn as_rev(&self) -> &str {
69        match self {
70            Self::Commit(commit) => commit.as_str(),
71            Self::Tag(rev) => rev,
72            Self::Branch(rev) => rev,
73            Self::BranchOrTag(rev) => rev,
74            Self::BranchOrTagOrCommit(rev) => rev,
75            Self::NamedRef(rev) => rev,
76            Self::DefaultBranch => "HEAD",
77        }
78    }
79
80    /// Converts the [`GitReference`] to a percent-encoded revision string for use in a URL.
81    pub fn as_url_rev(&self) -> Option<Cow<'_, str>> {
82        self.as_str().map(Self::encode_rev)
83    }
84
85    /// Return the commit identified directly by this reference, without resolving named references.
86    pub fn as_commit(&self) -> Option<GitOid> {
87        match self {
88            Self::Commit(commit) => Some(*commit),
89            Self::Branch(_)
90            | Self::Tag(_)
91            | Self::BranchOrTag(_)
92            | Self::BranchOrTagOrCommit(_)
93            | Self::NamedRef(_)
94            | Self::DefaultBranch => None,
95        }
96    }
97
98    /// Percent-encode a revision string for use in a URL.
99    fn encode_rev(rev: &str) -> Cow<'_, str> {
100        utf8_percent_encode(rev, GIT_REFERENCE_ENCODE_SET).into()
101    }
102
103    /// Returns the kind of this reference.
104    pub fn kind_str(&self) -> &str {
105        match self {
106            Self::Commit(_) => "commit",
107            Self::Branch(_) => "branch",
108            Self::Tag(_) => "tag",
109            Self::BranchOrTag(_) => "branch or tag",
110            Self::BranchOrTagOrCommit(_) => "branch, tag, or commit",
111            Self::NamedRef(_) => "ref",
112            Self::DefaultBranch => "default branch",
113        }
114    }
115}
116
117impl Display for GitReference {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        write!(f, "{}", self.as_str().unwrap_or("HEAD"))
120    }
121}
122
123/// Whether a `rev` looks like a commit hash (ASCII hex digits).
124fn looks_like_commit_hash(rev: &str) -> bool {
125    rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
126}