radicle_surf/
namespace.rs

1use std::{
2    convert::TryFrom,
3    fmt,
4    str::{self, FromStr},
5};
6
7use git_ext::ref_format::{
8    self,
9    refspec::{NamespacedPattern, PatternString, QualifiedPattern},
10    Component, Namespaced, Qualified, RefStr, RefString,
11};
12use nonempty::NonEmpty;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum Error {
17    /// When parsing a namespace we may come across one that was an empty
18    /// string.
19    #[error("namespaces must not be empty")]
20    EmptyNamespace,
21    #[error(transparent)]
22    RefFormat(#[from] ref_format::Error),
23    #[error(transparent)]
24    Utf8(#[from] str::Utf8Error),
25}
26
27/// A `Namespace` value allows us to switch the git namespace of
28/// a repo.
29///
30/// A `Namespace` is one or more name components separated by `/`, e.g. `surf`,
31/// `surf/git`.
32///
33/// For each `Namespace`, the reference name will add a single `refs/namespaces`
34/// prefix, e.g. `refs/namespaces/surf`,
35/// `refs/namespaces/surf/refs/namespaces/git`.
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct Namespace {
38    // XXX: we rely on RefString being non-empty here, which
39    // git-ref-format ensures that there's no way to construct one.
40    pub(super) namespaces: RefString,
41}
42
43impl Namespace {
44    /// Take a `Qualified` reference name and convert it to a `Namespaced` using
45    /// this `Namespace`.
46    ///
47    /// # Example
48    ///
49    /// ```no_run
50    /// let ns = "surf/git".parse::<Namespace>();
51    /// let name = ns.to_namespaced(qualified!("refs/heads/main"));
52    /// assert_eq!(
53    ///     name.as_str(),
54    ///     "refs/namespaces/surf/refs/namespaces/git/refs/heads/main"
55    /// );
56    /// ```
57    pub(crate) fn to_namespaced<'a>(&self, name: &Qualified<'a>) -> Namespaced<'a> {
58        let mut components = self.namespaces.components().rev();
59        let mut namespaced = name.with_namespace(
60            components
61                .next()
62                .expect("BUG: 'namespaces' cannot be empty"),
63        );
64        for ns in components {
65            let qualified = namespaced.into_qualified();
66            namespaced = qualified.with_namespace(ns);
67        }
68        namespaced
69    }
70
71    /// Take a `QualifiedPattern` reference name and convert it to a
72    /// `NamespacedPattern` using this `Namespace`.
73    ///
74    /// # Example
75    ///
76    /// ```no_run
77    /// let ns = "surf/git".parse::<Namespace>();
78    /// let name = ns.to_namespaced(pattern!("refs/heads/*").to_qualified().unwrap());
79    /// assert_eq!(
80    ///     name.as_str(),
81    ///     "refs/namespaces/surf/refs/namespaces/git/refs/heads/*"
82    /// );
83    /// ```
84    pub(crate) fn to_namespaced_pattern<'a>(
85        &self,
86        pat: &QualifiedPattern<'a>,
87    ) -> NamespacedPattern<'a> {
88        let pattern = PatternString::from(self.namespaces.clone());
89        let mut components = pattern.components().rev();
90        let mut namespaced = pat
91            .with_namespace(
92                components
93                    .next()
94                    .expect("BUG: 'namespaces' cannot be empty"),
95            )
96            .expect("BUG: 'namespace' cannot have globs");
97        for ns in components {
98            let qualified = namespaced.into_qualified();
99            namespaced = qualified
100                .with_namespace(ns)
101                .expect("BUG: 'namespaces' cannot have globs");
102        }
103        namespaced
104    }
105}
106
107impl fmt::Display for Namespace {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}", self.namespaces)
110    }
111}
112
113impl<'a> From<NonEmpty<Component<'a>>> for Namespace {
114    fn from(cs: NonEmpty<Component<'a>>) -> Self {
115        Self {
116            namespaces: cs.into_iter().collect::<RefString>(),
117        }
118    }
119}
120
121impl TryFrom<&str> for Namespace {
122    type Error = Error;
123
124    fn try_from(name: &str) -> Result<Self, Self::Error> {
125        Self::from_str(name)
126    }
127}
128
129impl TryFrom<&[u8]> for Namespace {
130    type Error = Error;
131
132    fn try_from(namespace: &[u8]) -> Result<Self, Self::Error> {
133        str::from_utf8(namespace)
134            .map_err(Error::from)
135            .and_then(Self::from_str)
136    }
137}
138
139impl FromStr for Namespace {
140    type Err = Error;
141
142    fn from_str(name: &str) -> Result<Self, Self::Err> {
143        let namespaces = RefStr::try_from_str(name)?.to_ref_string();
144        Ok(Self { namespaces })
145    }
146}
147
148impl From<Namespaced<'_>> for Namespace {
149    fn from(namespaced: Namespaced<'_>) -> Self {
150        let mut namespaces = namespaced.namespace().to_ref_string();
151        let mut qualified = namespaced.strip_namespace();
152        while let Some(namespaced) = qualified.to_namespaced() {
153            namespaces.push(namespaced.namespace());
154            qualified = namespaced.strip_namespace();
155        }
156        Self { namespaces }
157    }
158}
159
160impl TryFrom<&git2::Reference<'_>> for Namespace {
161    type Error = Error;
162
163    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
164        let name = RefStr::try_from_str(str::from_utf8(reference.name_bytes())?)?;
165        name.to_namespaced()
166            .ok_or(Error::EmptyNamespace)
167            .map(Self::from)
168    }
169}