radicle_surf/
branch.rs

1use std::{
2    convert::TryFrom,
3    str::{self, FromStr},
4};
5
6use git_ext::ref_format::{component, lit, Component, Qualified, RefStr, RefString};
7
8use crate::refs::refstr_join;
9
10/// A `Branch` represents any git branch. It can be [`Local`] or [`Remote`].
11///
12/// Note that if a `Branch` is created from a [`git2::Reference`] then
13/// any `refs/namespaces` will be stripped.
14#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub enum Branch {
17    Local(Local),
18    Remote(Remote),
19}
20
21impl Branch {
22    /// Construct a [`Local`] branch.
23    pub fn local<R>(name: R) -> Self
24    where
25        R: AsRef<RefStr>,
26    {
27        Self::Local(Local::new(name))
28    }
29
30    /// Construct a [`Remote`] branch.
31    /// The `remote` is the remote name of the reference name while
32    /// the `name` is the suffix, i.e. `refs/remotes/<remote>/<name>`.
33    pub fn remote<R>(remote: Component<'_>, name: R) -> Self
34    where
35        R: AsRef<RefStr>,
36    {
37        Self::Remote(Remote::new(remote, name))
38    }
39
40    /// Return the short `Branch` refname,
41    /// e.g. `fix/ref-format`.
42    pub fn short_name(&self) -> &RefString {
43        match self {
44            Branch::Local(local) => local.short_name(),
45            Branch::Remote(remote) => remote.short_name(),
46        }
47    }
48
49    /// Give back the fully qualified `Branch` refname,
50    /// e.g. `refs/remotes/origin/fix/ref-format`,
51    /// `refs/heads/fix/ref-format`.
52    pub fn refname(&self) -> Qualified {
53        match self {
54            Branch::Local(local) => local.refname(),
55            Branch::Remote(remote) => remote.refname(),
56        }
57    }
58}
59
60impl TryFrom<&git2::Reference<'_>> for Branch {
61    type Error = error::Branch;
62
63    fn try_from(reference: &git2::Reference<'_>) -> Result<Self, Self::Error> {
64        let name = str::from_utf8(reference.name_bytes())?;
65        Self::from_str(name)
66    }
67}
68
69impl TryFrom<&str> for Branch {
70    type Error = error::Branch;
71
72    fn try_from(name: &str) -> Result<Self, Self::Error> {
73        Self::from_str(name)
74    }
75}
76
77impl FromStr for Branch {
78    type Err = error::Branch;
79
80    fn from_str(name: &str) -> Result<Self, Self::Err> {
81        let name = RefStr::try_from_str(name)?;
82        let name = match name.to_namespaced() {
83            None => name
84                .qualified()
85                .ok_or_else(|| error::Branch::NotQualified(name.to_string()))?,
86            Some(name) => name.strip_namespace_recursive(),
87        };
88
89        let (_ref, category, c, cs) = name.non_empty_components();
90
91        if category == component::HEADS {
92            Ok(Self::Local(Local::new(refstr_join(c, cs))))
93        } else if category == component::REMOTES {
94            Ok(Self::Remote(Remote::new(c, cs.collect::<RefString>())))
95        } else {
96            Err(error::Branch::InvalidName(name.into()))
97        }
98    }
99}
100
101/// A `Local` represents a local branch, i.e. it is a reference under
102/// `refs/heads`.
103///
104/// Note that if a `Local` is created from a [`git2::Reference`] then
105/// any `refs/namespaces` will be stripped.
106#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct Local {
109    name: RefString,
110}
111
112impl Local {
113    /// Construct a new `Local` with the given `name`.
114    ///
115    /// If the name is qualified with `refs/heads`, this will be
116    /// shortened to the suffix. To get the `Qualified` name again,
117    /// use [`Local::refname`].
118    pub(crate) fn new<R>(name: R) -> Self
119    where
120        R: AsRef<RefStr>,
121    {
122        match name.as_ref().qualified() {
123            None => Self {
124                name: name.as_ref().to_ref_string(),
125            },
126            Some(qualified) => {
127                let (_refs, heads, c, cs) = qualified.non_empty_components();
128                if heads == component::HEADS {
129                    Self {
130                        name: refstr_join(c, cs),
131                    }
132                } else {
133                    Self {
134                        name: name.as_ref().to_ref_string(),
135                    }
136                }
137            }
138        }
139    }
140
141    /// Return the short `Local` refname,
142    /// e.g. `fix/ref-format`.
143    pub fn short_name(&self) -> &RefString {
144        &self.name
145    }
146
147    /// Return the fully qualified `Local` refname,
148    /// e.g. `refs/heads/fix/ref-format`.
149    pub fn refname(&self) -> Qualified {
150        lit::refs_heads(&self.name).into()
151    }
152}
153
154impl TryFrom<&git2::Reference<'_>> for Local {
155    type Error = error::Local;
156
157    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
158        let name = str::from_utf8(reference.name_bytes())?;
159        Self::from_str(name)
160    }
161}
162
163impl TryFrom<&str> for Local {
164    type Error = error::Local;
165
166    fn try_from(name: &str) -> Result<Self, Self::Error> {
167        Self::from_str(name)
168    }
169}
170
171impl FromStr for Local {
172    type Err = error::Local;
173
174    fn from_str(name: &str) -> Result<Self, Self::Err> {
175        let name = RefStr::try_from_str(name)?;
176        let name = match name.to_namespaced() {
177            None => name
178                .qualified()
179                .ok_or_else(|| error::Local::NotQualified(name.to_string()))?,
180            Some(name) => name.strip_namespace_recursive(),
181        };
182
183        let (_ref, heads, c, cs) = name.non_empty_components();
184        if heads == component::HEADS {
185            Ok(Self::new(refstr_join(c, cs)))
186        } else {
187            Err(error::Local::NotHeads(name.into()))
188        }
189    }
190}
191
192/// A `Remote` represents a remote branch, i.e. it is a reference under
193/// `refs/remotes`.
194///
195/// Note that if a `Remote` is created from a [`git2::Reference`] then
196/// any `refs/namespaces` will be stripped.
197#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199pub struct Remote {
200    remote: RefString,
201    name: RefString,
202}
203
204impl Remote {
205    /// Construct a new `Remote` with the given `name` and `remote`.
206    ///
207    /// ## Note
208    /// `name` is expected to be in short form, i.e. not begin with
209    /// `refs`.
210    ///
211    /// If you are creating a `Remote` with a name that begins with
212    /// `refs/remotes`, use [`Remote::from_refs_remotes`] instead.
213    ///
214    /// To get the `Qualified` name, use [`Remote::refname`].
215    pub(crate) fn new<R>(remote: Component, name: R) -> Self
216    where
217        R: AsRef<RefStr>,
218    {
219        Self {
220            name: name.as_ref().to_ref_string(),
221            remote: remote.to_ref_string(),
222        }
223    }
224
225    /// Parse the `name` from the form `refs/remotes/<remote>/<rest>`.
226    ///
227    /// If the `name` is not of this form, then `None` is returned.
228    pub fn from_refs_remotes<R>(name: R) -> Option<Self>
229    where
230        R: AsRef<RefStr>,
231    {
232        let qualified = name.as_ref().qualified()?;
233        let (_refs, remotes, remote, cs) = qualified.non_empty_components();
234        (remotes == component::REMOTES).then_some(Self {
235            name: cs.collect(),
236            remote: remote.to_ref_string(),
237        })
238    }
239
240    /// Return the short `Remote` refname,
241    /// e.g. `fix/ref-format`.
242    pub fn short_name(&self) -> &RefString {
243        &self.name
244    }
245
246    /// Return the remote of the `Remote`'s refname,
247    /// e.g. `origin`.
248    pub fn remote(&self) -> &RefString {
249        &self.remote
250    }
251
252    /// Give back the fully qualified `Remote` refname,
253    /// e.g. `refs/remotes/origin/fix/ref-format`.
254    pub fn refname(&self) -> Qualified {
255        lit::refs_remotes(self.remote.join(&self.name)).into()
256    }
257}
258
259impl TryFrom<&git2::Reference<'_>> for Remote {
260    type Error = error::Remote;
261
262    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
263        let name = str::from_utf8(reference.name_bytes())?;
264        Self::from_str(name)
265    }
266}
267
268impl TryFrom<&str> for Remote {
269    type Error = error::Remote;
270
271    fn try_from(name: &str) -> Result<Self, Self::Error> {
272        Self::from_str(name)
273    }
274}
275
276impl FromStr for Remote {
277    type Err = error::Remote;
278
279    fn from_str(name: &str) -> Result<Self, Self::Err> {
280        let name = RefStr::try_from_str(name)?;
281        let name = match name.to_namespaced() {
282            None => name
283                .qualified()
284                .ok_or_else(|| error::Remote::NotQualified(name.to_string()))?,
285            Some(name) => name.strip_namespace_recursive(),
286        };
287
288        let (_ref, remotes, remote, cs) = name.non_empty_components();
289        if remotes == component::REMOTES {
290            Ok(Self::new(remote, cs.collect::<RefString>()))
291        } else {
292            Err(error::Remote::NotRemotes(name.into()))
293        }
294    }
295}
296
297pub mod error {
298    use radicle_git_ext::ref_format::{self, RefString};
299    use thiserror::Error;
300
301    #[derive(Debug, Error)]
302    pub enum Branch {
303        #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
304        InvalidName(RefString),
305        #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
306        NotQualified(String),
307        #[error(transparent)]
308        RefFormat(#[from] ref_format::Error),
309        #[error(transparent)]
310        Utf8(#[from] std::str::Utf8Error),
311    }
312
313    #[derive(Debug, Error)]
314    pub enum Local {
315        #[error("the refname '{0}' did not begin with 'refs/heads'")]
316        NotHeads(RefString),
317        #[error("the refname '{0}' did not begin with 'refs/heads'")]
318        NotQualified(String),
319        #[error(transparent)]
320        RefFormat(#[from] ref_format::Error),
321        #[error(transparent)]
322        Utf8(#[from] std::str::Utf8Error),
323    }
324
325    #[derive(Debug, Error)]
326    pub enum Remote {
327        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
328        NotQualified(String),
329        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
330        NotRemotes(RefString),
331        #[error(transparent)]
332        RefFormat(#[from] ref_format::Error),
333        #[error(transparent)]
334        Utf8(#[from] std::str::Utf8Error),
335    }
336}