Skip to main content

radicle_git_ref_format/
deriv.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display},
4    ops::Deref,
5};
6
7use crate::{
8    Component, RefStr, RefString, lit, name,
9    refspec::{PatternStr, QualifiedPattern},
10};
11
12/// A fully-qualified refname.
13///
14/// A refname is qualified _iff_ it starts with "refs/" and has at least three
15/// components. This implies that a [`Qualified`] ref has a category, such as
16/// "refs/heads/main".
17#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
18pub struct Qualified<'a>(pub(crate) Cow<'a, RefStr>);
19
20impl<'a> Qualified<'a> {
21    /// Infallibly create a [`Qualified`] from components.
22    ///
23    /// Note that the "refs/" prefix is implicitly added, so `a` is the second
24    /// [`Component`]. Mirroring [`Self::non_empty_components`], providing
25    /// two [`Component`]s guarantees well-formedness of the [`Qualified`].
26    /// `tail` may be empty.
27    ///
28    /// # Example
29    ///
30    /// ```
31    /// use radicle_git_ref_format::{name::component, Qualified};
32    ///
33    /// assert_eq!(
34    ///     "refs/heads/main",
35    ///     Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
36    /// )
37    /// ```
38    pub fn from_components<'b, 'c, 'd, A, B, C>(a: A, b: B, tail: C) -> Self
39    where
40        A: Into<Component<'b>>,
41        B: Into<Component<'c>>,
42        C: IntoIterator<Item = Component<'d>>,
43    {
44        let mut inner = name::REFS.join(a.into()).and(b.into());
45        inner.extend(tail);
46
47        Self(inner.into())
48    }
49
50    pub fn from_refstr(r: impl Into<Cow<'a, RefStr>>) -> Option<Self> {
51        Self::_from_refstr(r.into())
52    }
53
54    fn _from_refstr(r: Cow<'a, RefStr>) -> Option<Self> {
55        let mut iter = r.iter();
56        match (iter.next()?, iter.next()?, iter.next()?) {
57            ("refs", _, _) => Some(Qualified(r)),
58            _ => None,
59        }
60    }
61
62    #[inline]
63    pub fn as_str(&self) -> &str {
64        self.as_ref()
65    }
66
67    #[inline]
68    pub fn join<'b, R>(&self, other: R) -> Qualified<'b>
69    where
70        R: AsRef<RefStr>,
71    {
72        Qualified(self.0.join(other).into())
73    }
74
75    pub fn to_pattern<P>(&'a self, pattern: P) -> QualifiedPattern<'a>
76    where
77        P: AsRef<PatternStr>,
78    {
79        QualifiedPattern(Cow::Owned(RefStr::to_pattern(self, pattern.as_ref())))
80    }
81
82    #[inline]
83    pub fn to_namespaced(&'a self) -> Option<Namespaced<'a>> {
84        self.0.as_ref().into()
85    }
86
87    /// Add a namespace.
88    ///
89    /// Creates a new [`Namespaced`] by prefxing `self` with
90    /// `refs/namespaces/<ns>`.
91    pub fn with_namespace<'b>(&self, ns: Component<'b>) -> Namespaced<'a> {
92        Namespaced(Cow::Owned(
93            IntoIterator::into_iter([lit::Refs.into(), lit::Namespaces.into(), ns])
94                .chain(self.0.components())
95                .collect(),
96        ))
97    }
98
99    /// Like [`Self::non_empty_components`], but with string slices.
100    pub fn non_empty_iter(&'a self) -> (&'a str, &'a str, &'a str, name::Iter<'a>) {
101        let mut iter = self.iter();
102        (
103            iter.next().unwrap(),
104            iter.next().unwrap(),
105            iter.next().unwrap(),
106            iter,
107        )
108    }
109
110    /// Return the first three [`Component`]s, and a possibly empty iterator
111    /// over the remaining ones.
112    ///
113    /// A qualified ref is guaranteed to have at least three components, which
114    /// this method provides a witness of. This is useful eg. for pattern
115    /// matching on the prefix.
116    pub fn non_empty_components(
117        &'a self,
118    ) -> (
119        Component<'a>,
120        Component<'a>,
121        Component<'a>,
122        name::Components<'a>,
123    ) {
124        let mut cs = self.components();
125        (
126            cs.next().unwrap(),
127            cs.next().unwrap(),
128            cs.next().unwrap(),
129            cs,
130        )
131    }
132
133    #[inline]
134    pub fn to_owned<'b>(&self) -> Qualified<'b> {
135        Qualified(Cow::Owned(self.0.clone().into_owned()))
136    }
137
138    #[inline]
139    pub fn into_owned<'b>(self) -> Qualified<'b> {
140        Qualified(Cow::Owned(self.0.into_owned()))
141    }
142
143    #[inline]
144    pub fn into_refstring(self) -> RefString {
145        self.into()
146    }
147}
148
149impl Deref for Qualified<'_> {
150    type Target = RefStr;
151
152    #[inline]
153    fn deref(&self) -> &Self::Target {
154        &self.0
155    }
156}
157
158impl AsRef<RefStr> for Qualified<'_> {
159    #[inline]
160    fn as_ref(&self) -> &RefStr {
161        self
162    }
163}
164
165impl AsRef<str> for Qualified<'_> {
166    #[inline]
167    fn as_ref(&self) -> &str {
168        self.0.as_str()
169    }
170}
171
172impl AsRef<Self> for Qualified<'_> {
173    #[inline]
174    fn as_ref(&self) -> &Self {
175        self
176    }
177}
178
179impl<'a> From<Qualified<'a>> for Cow<'a, RefStr> {
180    #[inline]
181    fn from(q: Qualified<'a>) -> Self {
182        q.0
183    }
184}
185
186impl From<Qualified<'_>> for RefString {
187    #[inline]
188    fn from(q: Qualified) -> Self {
189        q.0.into_owned()
190    }
191}
192
193impl<T, U> From<(lit::Refs, T, U)> for Qualified<'_>
194where
195    T: AsRef<RefStr>,
196    U: AsRef<RefStr>,
197{
198    #[inline]
199    fn from((refs, cat, name): (lit::Refs, T, U)) -> Self {
200        let refs: &RefStr = refs.into();
201        Self(Cow::Owned(refs.join(cat).and(name)))
202    }
203}
204
205impl<T> From<lit::RefsHeads<T>> for Qualified<'_>
206where
207    T: AsRef<RefStr>,
208{
209    #[inline]
210    fn from((refs, heads, name): lit::RefsHeads<T>) -> Self {
211        Self(Cow::Owned(
212            IntoIterator::into_iter([Component::from(refs), heads.into()])
213                .collect::<RefString>()
214                .and(name),
215        ))
216    }
217}
218
219impl<T> From<lit::RefsTags<T>> for Qualified<'_>
220where
221    T: AsRef<RefStr>,
222{
223    #[inline]
224    fn from((refs, tags, name): lit::RefsTags<T>) -> Self {
225        Self(Cow::Owned(
226            IntoIterator::into_iter([Component::from(refs), tags.into()])
227                .collect::<RefString>()
228                .and(name),
229        ))
230    }
231}
232
233impl<T> From<lit::RefsNotes<T>> for Qualified<'_>
234where
235    T: AsRef<RefStr>,
236{
237    #[inline]
238    fn from((refs, notes, name): lit::RefsNotes<T>) -> Self {
239        Self(Cow::Owned(
240            IntoIterator::into_iter([Component::from(refs), notes.into()])
241                .collect::<RefString>()
242                .and(name),
243        ))
244    }
245}
246
247impl<T> From<lit::RefsRemotes<T>> for Qualified<'_>
248where
249    T: AsRef<RefStr>,
250{
251    #[inline]
252    fn from((refs, remotes, name): lit::RefsRemotes<T>) -> Self {
253        Self(Cow::Owned(
254            IntoIterator::into_iter([Component::from(refs), remotes.into()])
255                .collect::<RefString>()
256                .and(name),
257        ))
258    }
259}
260
261impl Display for Qualified<'_> {
262    #[inline]
263    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
264        self.0.fmt(f)
265    }
266}
267
268/// A [`Qualified`] ref under a git namespace.
269///
270/// A ref is namespaced if it starts with "refs/namespaces/", another path
271/// component, and "refs" or "HEAD".
272/// For example "/namespaces/xyz/refs/heads/main"
273/// or "refs/namespaces/xyz/HEAD".
274///
275/// Note that namespaces can be nested, so the result of
276/// [`Namespaced::strip_namespace`] may be convertible to a [`Namespaced`]
277/// again. For example:
278///
279/// ```
280/// use radicle_git_ref_format::RefString;
281///
282/// let full =
283///     RefString::try_from("refs/namespaces/a/refs/namespaces/b/refs/heads/main").unwrap();
284/// let namespaced = full.to_namespaced().unwrap();
285/// let strip_first = namespaced.strip_namespace();
286/// let nested = strip_first.to_namespaced().unwrap();
287/// let strip_second = nested.strip_namespace();
288///
289/// assert_eq!("a", namespaced.namespace().as_str());
290/// assert_eq!("b", nested.namespace().as_str());
291/// assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
292/// assert_eq!("refs/heads/main", strip_second.as_str());
293/// ```
294#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
295pub struct Namespaced<'a>(Cow<'a, RefStr>);
296
297impl<'a> Namespaced<'a> {
298    pub fn namespace(&'a self) -> Component<'a> {
299        self.components().nth(2).unwrap()
300    }
301
302    pub fn strip_namespace<'b>(&self) -> Qualified<'b> {
303        const REFS_NAMESPACES: &RefStr = RefStr::from_str("refs/namespaces");
304
305        Qualified(Cow::Owned(
306            self.strip_prefix(REFS_NAMESPACES)
307                .unwrap()
308                .components()
309                .skip(1)
310                .collect(),
311        ))
312    }
313
314    pub fn strip_namespace_recursive<'b>(&self) -> Qualified<'b> {
315        let mut strip = self.strip_namespace();
316        while let Some(ns) = strip.to_namespaced() {
317            strip = ns.strip_namespace();
318        }
319        strip
320    }
321
322    #[inline]
323    pub fn to_owned<'b>(&self) -> Namespaced<'b> {
324        Namespaced(Cow::Owned(self.0.clone().into_owned()))
325    }
326
327    #[inline]
328    pub fn into_owned<'b>(self) -> Namespaced<'b> {
329        Namespaced(Cow::Owned(self.0.into_owned()))
330    }
331
332    #[inline]
333    pub fn into_qualified(self) -> Qualified<'a> {
334        self.into()
335    }
336}
337
338impl Deref for Namespaced<'_> {
339    type Target = RefStr;
340
341    #[inline]
342    fn deref(&self) -> &Self::Target {
343        &self.0
344    }
345}
346
347impl AsRef<RefStr> for Namespaced<'_> {
348    #[inline]
349    fn as_ref(&self) -> &RefStr {
350        self
351    }
352}
353
354impl AsRef<str> for Namespaced<'_> {
355    #[inline]
356    fn as_ref(&self) -> &str {
357        self.0.as_str()
358    }
359}
360
361impl<'a> From<Namespaced<'a>> for Qualified<'a> {
362    #[inline]
363    fn from(ns: Namespaced<'a>) -> Self {
364        Self(ns.0)
365    }
366}
367
368impl<'a> From<&'a RefStr> for Option<Namespaced<'a>> {
369    fn from(rs: &'a RefStr) -> Self {
370        let mut cs = rs.iter();
371        match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) {
372            ("refs", "namespaces", _, "refs" | "HEAD") => Some(Namespaced(Cow::from(rs))),
373
374            _ => None,
375        }
376    }
377}
378
379impl<'a, T> From<lit::RefsNamespaces<'_, T>> for Namespaced<'static>
380where
381    T: Into<Component<'a>>,
382{
383    #[inline]
384    fn from((refs, namespaces, namespace, name): lit::RefsNamespaces<T>) -> Self {
385        Self(Cow::Owned(
386            IntoIterator::into_iter([refs.into(), namespaces.into(), namespace.into()])
387                .collect::<RefString>()
388                .and(name),
389        ))
390    }
391}
392
393impl Display for Namespaced<'_> {
394    #[inline]
395    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
396        self.0.fmt(f)
397    }
398}