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