git_prole/git/refs/
name.rs

1use std::fmt::Debug;
2use std::fmt::Display;
3use std::str::FromStr;
4
5use miette::miette;
6use winnow::combinator::rest;
7use winnow::token::take_till;
8use winnow::PResult;
9use winnow::Parser;
10
11/// A Git reference.
12///
13/// For branches, see:
14/// - [`super::LocalBranchRef`] for `refs/heads/*`.
15/// - [`super::RemoteBranchRef`] for `refs/remotes/*`.
16/// - [`super::BranchRef`] to combine the above types.
17#[derive(Clone, Hash, PartialEq, Eq)]
18pub struct Ref {
19    /// The ref kind; usually `heads`, `remotes`, or `tags`.
20    ///
21    /// Other kinds:
22    /// - `stash`
23    /// - `bisect`
24    kind: String,
25    /// The ref name; everything after the kind.
26    name: String,
27}
28
29impl Debug for Ref {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{:?}", self.to_string())
32    }
33}
34
35impl Ref {
36    /// The `kind` indicating a branch reference.
37    pub const HEADS: &str = "heads";
38    /// The `kind` indicating a remote-tracking branch reference.
39    pub const REMOTES: &str = "remotes";
40    /// The `kind` indicating a tag reference.
41    pub const TAGS: &str = "tags";
42
43    pub fn new(kind: String, name: String) -> Self {
44        Self { kind, name }
45    }
46
47    pub fn name(&self) -> &str {
48        &self.name
49    }
50
51    pub fn kind(&self) -> &str {
52        &self.kind
53    }
54
55    /// Determine if this is a remote branch, i.e. its kind is [`Self::REMOTES`].
56    pub fn is_remote_branch(&self) -> bool {
57        self.kind == Self::REMOTES
58    }
59
60    /// Determine if this is a local branch, i.e. its kind is [`Self::HEADS`].
61    pub fn is_local_branch(&self) -> bool {
62        self.kind == Self::HEADS
63    }
64
65    /// Determine if this is a tag, i.e. its kind is [`Self::TAGS`].
66    #[expect(dead_code)]
67    pub(crate) fn is_tag(&self) -> bool {
68        self.kind == Self::TAGS
69    }
70
71    /// Parse a ref name like `refs/puppy/doggy`.
72    ///
73    /// Needs at least one slash after `refs/`; this does not treat `refs/puppy` as a valid ref
74    /// name.
75    pub fn parser(input: &mut &str) -> PResult<Self> {
76        let _refs_prefix = "refs/".parse_next(input)?;
77
78        let kind = take_till(1.., '/').parse_next(input)?;
79        let _ = '/'.parse_next(input)?;
80        let name = rest.parse_next(input)?;
81
82        Ok(Self {
83            kind: kind.to_owned(),
84            name: name.to_owned(),
85        })
86    }
87}
88
89impl FromStr for Ref {
90    type Err = miette::Report;
91
92    fn from_str(input: &str) -> Result<Self, Self::Err> {
93        Self::parser.parse(input).map_err(|err| miette!("{err}"))
94    }
95}
96
97impl Display for Ref {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        if f.alternate() {
100            write!(f, "refs/{}/{}", self.kind, self.name)
101        } else {
102            write!(f, "{}", self.name)
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_ref_parse_no_slash() {
113        assert!(Ref::from_str("refs/puppy").is_err());
114    }
115
116    #[test]
117    fn test_ref_parse_simple() {
118        assert_eq!(
119            Ref::from_str("refs/puppy/doggy").unwrap(),
120            Ref {
121                kind: "puppy".into(),
122                name: "doggy".into()
123            }
124        );
125    }
126
127    #[test]
128    fn test_ref_parse_multiple_slashes() {
129        assert_eq!(
130            Ref::from_str("refs/puppy/doggy/softie/cutie").unwrap(),
131            Ref {
132                kind: "puppy".into(),
133                name: "doggy/softie/cutie".into()
134            }
135        );
136    }
137}