radicle_surf/
namespace.rs

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