Skip to main content

uv_git_types/
reference.rs

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