git_prole/git/refs/
name.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use std::fmt::Debug;
use std::fmt::Display;
use std::str::FromStr;

use miette::miette;
use winnow::combinator::rest;
use winnow::token::take_till;
use winnow::PResult;
use winnow::Parser;

/// A Git reference.
///
/// For branches, see:
/// - [`super::LocalBranchRef`] for `refs/heads/*`.
/// - [`super::RemoteBranchRef`] for `refs/remotes/*`.
/// - [`super::BranchRef`] to combine the above types.
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct Ref {
    /// The ref kind; usually `heads`, `remotes`, or `tags`.
    ///
    /// Other kinds:
    /// - `stash`
    /// - `bisect`
    kind: String,
    /// The ref name; everything after the kind.
    name: String,
}

impl Debug for Ref {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self.to_string())
    }
}

impl Ref {
    /// The `kind` indicating a branch reference.
    pub const HEADS: &str = "heads";
    /// The `kind` indicating a remote-tracking branch reference.
    pub const REMOTES: &str = "remotes";
    /// The `kind` indicating a tag reference.
    pub const TAGS: &str = "tags";

    pub fn new(kind: String, name: String) -> Self {
        Self { kind, name }
    }

    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn kind(&self) -> &str {
        &self.kind
    }

    /// Determine if this is a remote branch, i.e. its kind is [`Self::REMOTES`].
    pub fn is_remote_branch(&self) -> bool {
        self.kind == Self::REMOTES
    }

    /// Determine if this is a local branch, i.e. its kind is [`Self::HEADS`].
    pub fn is_local_branch(&self) -> bool {
        self.kind == Self::HEADS
    }

    /// Determine if this is a tag, i.e. its kind is [`Self::TAGS`].
    #[expect(dead_code)]
    pub(crate) fn is_tag(&self) -> bool {
        self.kind == Self::TAGS
    }

    /// Parse a ref name like `refs/puppy/doggy`.
    ///
    /// Needs at least one slash after `refs/`; this does not treat `refs/puppy` as a valid ref
    /// name.
    pub fn parser(input: &mut &str) -> PResult<Self> {
        let _refs_prefix = "refs/".parse_next(input)?;

        let kind = take_till(1.., '/').parse_next(input)?;
        let _ = '/'.parse_next(input)?;
        let name = rest.parse_next(input)?;

        Ok(Self {
            kind: kind.to_owned(),
            name: name.to_owned(),
        })
    }
}

impl FromStr for Ref {
    type Err = miette::Report;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Self::parser.parse(input).map_err(|err| miette!("{err}"))
    }
}

impl Display for Ref {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if f.alternate() {
            write!(f, "refs/{}/{}", self.kind, self.name)
        } else {
            write!(f, "{}", self.name)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ref_parse_no_slash() {
        assert!(Ref::from_str("refs/puppy").is_err());
    }

    #[test]
    fn test_ref_parse_simple() {
        assert_eq!(
            Ref::from_str("refs/puppy/doggy").unwrap(),
            Ref {
                kind: "puppy".into(),
                name: "doggy".into()
            }
        );
    }

    #[test]
    fn test_ref_parse_multiple_slashes() {
        assert_eq!(
            Ref::from_str("refs/puppy/doggy/softie/cutie").unwrap(),
            Ref {
                kind: "puppy".into(),
                name: "doggy/softie/cutie".into()
            }
        );
    }
}