Skip to main content

radicle_surf/
namespace.rs

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