Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'origin/merge-git-ref-format'
Fintan Halpenny committed 3 years ago
commit a887e542f660b9d8d1c0e5d9320ca25a30e65d8c
parent f06bc38
85 files changed +3497 -3504
modified Cargo.toml
@@ -1,6 +1,5 @@
[workspace]
members = [
-
  "git-ref-format",
  "git-commit",
  "git-storage",
  "git-trailers",
deleted git-ref-format/Cargo.toml
@@ -1,31 +0,0 @@
-
[package]
-
name = "git-ref-format"
-
version = "0.2.0"
-
authors = [
-
  "Kim Altintop <kim@eagain.st>",
-
  "Fintan Halpenny <fintan.halpenny@gmail.com>",
-
]
-
edition = "2021"
-
license = "GPL-3.0-or-later"
-
description = "Everything you never knew you wanted for handling git ref names."
-
keywords = ["git", "references"]
-

-
[lib]
-
doctest = false
-
test = false
-

-
[features]
-
bstr = ["git-ref-format-core/bstr"]
-
macro = ["git-ref-format-macro"]
-
minicbor = ["git-ref-format-core/minicbor"]
-
percent-encoding = ["git-ref-format-core/percent-encoding"]
-
serde = ["git-ref-format-core/serde"]
-

-
[dependencies.git-ref-format-core]
-
version = "0.2.0"
-
path = "./core"
-

-
[dependencies.git-ref-format-macro]
-
version = "0.2.0"
-
path = "./macro"
-
optional = true
deleted git-ref-format/core/CHANGELOG.md
@@ -1,11 +0,0 @@
-
# CHANGELOG
-

-
## Version 0.2.0
-

-
* 2022-12-16 Update to rust-1.66 [8d451e3](https://github.com/radicle-dev/radicle-git/commit/8d451e3d9d6c0121b0ed3e4ac6cbf710625e9f9d)
-
* 2022-12-08 Fix to deserialization [db7f2d4](https://github.com/radicle-dev/radicle-git/commit/db7f2d40e32ef0fac7425054c4c49e22cb8bc70f)
-
* 2022-11-18 Remove link-literals feature [d45d63b](https://github.com/radicle-dev/radicle-git/commit/d45d63b7ceb1d03b80f6c47749b4b3e3ebb99e70)
-
* 2022-11-16 Add well-known refspec components [2b5a34a](https://github.com/radicle-dev/radicle-git/commit/2b5a34a703c182ff48d6c83ff3fa4256b4f37624)
-
* 2022-11-15 DoubleEndedIterator for Components [cf1594c](https://github.com/radicle-dev/radicle-git/commit/cf1594cf43e23c9fc8f74cf69e22f81227e82f2b)
-
* 2022-11-11 Add QualifiedPattern and NamespacedPattern [6479ba0](https://github.com/radicle-dev/radicle-git/commit/6479ba091fa3ee3b16fba61138cfd8c6ec6d985d)
-
* 2022-11-16 Component::from_refstr  [d6c7bb2](https://github.com/radicle-dev/radicle-git/commit/d6c7bb29abc0f0050e2efe8de47397eaa4564f6f)
deleted git-ref-format/core/Cargo.toml
@@ -1,33 +0,0 @@
-
[package]
-
name = "git-ref-format-core"
-
version = "0.2.0"
-
authors = ["Kim Altintop <kim@eagain.st>"]
-
edition = "2018"
-
license = "GPL-3.0-or-later"
-
description = "Core types for the git-ref-format crate"
-
keywords = ["git", "references"]
-

-
[lib]
-
doctest = false
-
test = false
-

-
[dependencies]
-
thiserror = "1.0"
-

-
[dependencies.bstr]
-
version = "0.2"
-
optional = true
-

-
[dependencies.minicbor]
-
version = "0.13"
-
features = ["std"]
-
optional = true
-

-
[dependencies.percent-encoding]
-
version = "2.1.0"
-
optional = true
-

-
[dependencies.serde]
-
version = "1.0"
-
features = ["derive"]
-
optional = true
deleted git-ref-format/core/src/cbor.rs
@@ -1,117 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use std::convert::TryFrom;
-

-
use minicbor::{
-
    decode,
-
    encode::{self, Write},
-
    Decode,
-
    Decoder,
-
    Encode,
-
    Encoder,
-
};
-

-
use crate::{
-
    refspec::{PatternStr, PatternString},
-
    Namespaced,
-
    Qualified,
-
    RefStr,
-
    RefString,
-
};
-

-
impl<'de: 'a, 'a> Decode<'de> for &'a RefStr {
-
    #[inline]
-
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
-
        d.str()
-
            .and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
-
    }
-
}
-

-
impl<'a> Encode for &'a RefStr {
-
    #[inline]
-
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
        e.str(self.as_str())?;
-
        Ok(())
-
    }
-
}
-

-
impl<'de> Decode<'de> for RefString {
-
    #[inline]
-
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
-
        Decode::decode(d).map(|s: &RefStr| s.to_owned())
-
    }
-
}
-

-
impl Encode for RefString {
-
    #[inline]
-
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
        self.as_refstr().encode(e)
-
    }
-
}
-

-
impl<'de: 'a, 'a> Decode<'de> for &'a PatternStr {
-
    #[inline]
-
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
-
        d.str()
-
            .and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
-
    }
-
}
-

-
impl<'a> Encode for &'a PatternStr {
-
    #[inline]
-
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
        e.str(self.as_str())?;
-
        Ok(())
-
    }
-
}
-

-
impl<'de> Decode<'de> for PatternString {
-
    #[inline]
-
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
-
        Decode::decode(d).map(|s: &PatternStr| s.to_owned())
-
    }
-
}
-

-
impl Encode for PatternString {
-
    #[inline]
-
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
        self.as_pattern_str().encode(e)
-
    }
-
}
-

-
impl<'de: 'a, 'a> Decode<'de> for Qualified<'a> {
-
    #[inline]
-
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
-
        Decode::decode(d).and_then(|s: &RefStr| {
-
            s.qualified()
-
                .ok_or(decode::Error::Message("not a qualified ref"))
-
        })
-
    }
-
}
-

-
impl<'a> Encode for Qualified<'a> {
-
    #[inline]
-
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
        self.as_str().encode(e)
-
    }
-
}
-

-
impl<'de: 'a, 'a> Decode<'de> for Namespaced<'a> {
-
    #[inline]
-
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
-
        Decode::decode(d).and_then(|s: &RefStr| {
-
            s.to_namespaced()
-
                .ok_or(decode::Error::Message("not a namespaced ref"))
-
        })
-
    }
-
}
-

-
impl<'a> Encode for Namespaced<'a> {
-
    #[inline]
-
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
        self.as_str().encode(e)
-
    }
-
}
deleted git-ref-format/core/src/check.rs
@@ -1,106 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use thiserror::Error;
-

-
pub struct Options {
-
    /// If `false`, the refname must contain at least one `/`.
-
    pub allow_onelevel: bool,
-
    /// If `true`, the refname may contain exactly one `*` character.
-
    pub allow_pattern: bool,
-
}
-

-
#[derive(Debug, PartialEq, Eq, Error)]
-
#[non_exhaustive]
-
pub enum Error {
-
    #[error("empty input")]
-
    Empty,
-
    #[error("lone '@' character")]
-
    LoneAt,
-
    #[error("consecutive or trailing slash")]
-
    Slash,
-
    #[error("ends with '.lock'")]
-
    DotLock,
-
    #[error("consecutive dots ('..')")]
-
    DotDot,
-
    #[error("at-open-brace ('@{{')")]
-
    AtOpenBrace,
-
    #[error("invalid character {0:?}")]
-
    InvalidChar(char),
-
    #[error("component starts with '.'")]
-
    StartsDot,
-
    #[error("component ends with '.'")]
-
    EndsDot,
-
    #[error("control character")]
-
    Control,
-
    #[error("whitespace")]
-
    Space,
-
    #[error("must contain at most one '*'")]
-
    Pattern,
-
    #[error("must contain at least one '/'")]
-
    OneLevel,
-
}
-

-
/// Validate that a string slice is a valid refname.
-
pub fn ref_format(opts: Options, s: &str) -> Result<(), Error> {
-
    match s {
-
        "" => Err(Error::Empty),
-
        "@" => Err(Error::LoneAt),
-
        "." => Err(Error::StartsDot),
-
        _ => {
-
            let mut globs = 0usize;
-
            let mut parts = 0usize;
-

-
            for x in s.split('/') {
-
                if x.is_empty() {
-
                    return Err(Error::Slash);
-
                }
-

-
                parts += 1;
-

-
                if x.ends_with(".lock") {
-
                    return Err(Error::DotLock);
-
                }
-

-
                let last_char = x.chars().count() - 1;
-
                for (i, y) in x.chars().zip(x.chars().cycle().skip(1)).enumerate() {
-
                    match y {
-
                        ('.', '.') => return Err(Error::DotDot),
-
                        ('@', '{') => return Err(Error::AtOpenBrace),
-

-
                        ('\0', _) => return Err(Error::InvalidChar('\0')),
-
                        ('\\', _) => return Err(Error::InvalidChar('\\')),
-
                        ('~', _) => return Err(Error::InvalidChar('~')),
-
                        ('^', _) => return Err(Error::InvalidChar('^')),
-
                        (':', _) => return Err(Error::InvalidChar(':')),
-
                        ('?', _) => return Err(Error::InvalidChar('?')),
-
                        ('[', _) => return Err(Error::InvalidChar('[')),
-

-
                        ('*', _) => globs += 1,
-

-
                        ('.', _) if i == 0 => return Err(Error::StartsDot),
-
                        ('.', _) if i == last_char => return Err(Error::EndsDot),
-

-
                        (' ', _) => return Err(Error::Space),
-

-
                        (z, _) if z.is_ascii_control() => return Err(Error::Control),
-

-
                        _ => continue,
-
                    }
-
                }
-
            }
-

-
            if parts < 2 && !opts.allow_onelevel {
-
                Err(Error::OneLevel)
-
            } else if globs > 1 && opts.allow_pattern {
-
                Err(Error::Pattern)
-
            } else if globs > 0 && !opts.allow_pattern {
-
                Err(Error::InvalidChar('*'))
-
            } else {
-
                Ok(())
-
            }
-
        },
-
    }
-
}
deleted git-ref-format/core/src/deriv.rs
@@ -1,397 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use std::{
-
    borrow::Cow,
-
    fmt::{self, Display},
-
    ops::Deref,
-
};
-

-
use crate::{
-
    lit,
-
    name,
-
    refspec::{PatternStr, QualifiedPattern},
-
    Component,
-
    RefStr,
-
    RefString,
-
};
-

-
/// A fully-qualified refname.
-
///
-
/// A refname is qualified _iff_ it starts with "refs/" and has at least three
-
/// components. This implies that a [`Qualified`] ref has a category, such as
-
/// "refs/heads/main".
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Qualified<'a>(pub(crate) Cow<'a, RefStr>);
-

-
impl<'a> Qualified<'a> {
-
    /// Infallibly create a [`Qualified`] from components.
-
    ///
-
    /// Note that the "refs/" prefix is implicitly added, so `a` is the second
-
    /// [`Component`]. Mirroring [`Self::non_empty_components`], providing
-
    /// two [`Component`]s guarantees well-formedness of the [`Qualified`].
-
    /// `tail` may be empty.
-
    ///
-
    /// # Example
-
    ///
-
    /// ```no_run
-
    /// use git_ref_format::{component, Qualified};
-
    ///
-
    /// assert_eq!(
-
    ///     "refs/heads/main",
-
    ///     Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
-
    /// )
-
    /// ```
-
    pub fn from_components<'b, 'c, 'd, A, B, C>(a: A, b: B, tail: C) -> Self
-
    where
-
        A: Into<Component<'b>>,
-
        B: Into<Component<'c>>,
-
        C: IntoIterator<Item = Component<'d>>,
-
    {
-
        let mut inner = name::REFS.join(a.into()).and(b.into());
-
        inner.extend(tail);
-

-
        Self(inner.into())
-
    }
-

-
    pub fn from_refstr(r: impl Into<Cow<'a, RefStr>>) -> Option<Self> {
-
        Self::_from_refstr(r.into())
-
    }
-

-
    fn _from_refstr(r: Cow<'a, RefStr>) -> Option<Self> {
-
        let mut iter = r.iter();
-
        match (iter.next()?, iter.next()?, iter.next()?) {
-
            ("refs", _, _) => Some(Qualified(r)),
-
            _ => None,
-
        }
-
    }
-

-
    #[inline]
-
    pub fn as_str(&self) -> &str {
-
        self.as_ref()
-
    }
-

-
    #[inline]
-
    pub fn join<'b, R>(&self, other: R) -> Qualified<'b>
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        Qualified(self.0.join(other).into())
-
    }
-

-
    pub fn to_pattern<P>(&self, pattern: P) -> QualifiedPattern
-
    where
-
        P: AsRef<PatternStr>,
-
    {
-
        QualifiedPattern(Cow::Owned(RefStr::to_pattern(self, pattern.as_ref())))
-
    }
-

-
    #[inline]
-
    pub fn to_namespaced(&self) -> Option<Namespaced> {
-
        self.0.as_ref().into()
-
    }
-

-
    /// Add a namespace.
-
    ///
-
    /// Creates a new [`Namespaced`] by prefxing `self` with
-
    /// `refs/namespaces/<ns>`.
-
    pub fn with_namespace<'b>(&self, ns: Component<'b>) -> Namespaced<'a> {
-
        Namespaced(Cow::Owned(
-
            IntoIterator::into_iter([lit::Refs.into(), lit::Namespaces.into(), ns])
-
                .chain(self.0.components())
-
                .collect(),
-
        ))
-
    }
-

-
    /// Like [`Self::non_empty_components`], but with string slices.
-
    pub fn non_empty_iter(&self) -> (&str, &str, &str, name::Iter) {
-
        let mut iter = self.iter();
-
        (
-
            iter.next().unwrap(),
-
            iter.next().unwrap(),
-
            iter.next().unwrap(),
-
            iter,
-
        )
-
    }
-

-
    /// Return the first three [`Component`]s, and a possibly empty iterator
-
    /// over the remaining ones.
-
    ///
-
    /// A qualified ref is guaranteed to have at least three components, which
-
    /// this method provides a witness of. This is useful eg. for pattern
-
    /// matching on the prefix.
-
    pub fn non_empty_components(&self) -> (Component, Component, Component, name::Components) {
-
        let mut cs = self.components();
-
        (
-
            cs.next().unwrap(),
-
            cs.next().unwrap(),
-
            cs.next().unwrap(),
-
            cs,
-
        )
-
    }
-

-
    #[inline]
-
    pub fn to_owned<'b>(&self) -> Qualified<'b> {
-
        Qualified(Cow::Owned(self.0.clone().into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_owned<'b>(self) -> Qualified<'b> {
-
        Qualified(Cow::Owned(self.0.into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_refstring(self) -> RefString {
-
        self.into()
-
    }
-
}
-

-
impl Deref for Qualified<'_> {
-
    type Target = RefStr;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<RefStr> for Qualified<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &RefStr {
-
        self
-
    }
-
}
-

-
impl AsRef<str> for Qualified<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self.0.as_str()
-
    }
-
}
-

-
impl AsRef<Self> for Qualified<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &Self {
-
        self
-
    }
-
}
-

-
impl<'a> From<Qualified<'a>> for Cow<'a, RefStr> {
-
    #[inline]
-
    fn from(q: Qualified<'a>) -> Self {
-
        q.0
-
    }
-
}
-

-
impl From<Qualified<'_>> for RefString {
-
    #[inline]
-
    fn from(q: Qualified) -> Self {
-
        q.0.into_owned()
-
    }
-
}
-

-
impl<T, U> From<(lit::Refs, T, U)> for Qualified<'_>
-
where
-
    T: AsRef<RefStr>,
-
    U: AsRef<RefStr>,
-
{
-
    #[inline]
-
    fn from((refs, cat, name): (lit::Refs, T, U)) -> Self {
-
        let refs: &RefStr = refs.into();
-
        Self(Cow::Owned(refs.join(cat).and(name)))
-
    }
-
}
-

-
impl<T> From<lit::RefsHeads<T>> for Qualified<'_>
-
where
-
    T: AsRef<RefStr>,
-
{
-
    #[inline]
-
    fn from((refs, heads, name): lit::RefsHeads<T>) -> Self {
-
        Self(Cow::Owned(
-
            IntoIterator::into_iter([Component::from(refs), heads.into()])
-
                .collect::<RefString>()
-
                .and(name),
-
        ))
-
    }
-
}
-

-
impl<T> From<lit::RefsTags<T>> for Qualified<'_>
-
where
-
    T: AsRef<RefStr>,
-
{
-
    #[inline]
-
    fn from((refs, tags, name): lit::RefsTags<T>) -> Self {
-
        Self(Cow::Owned(
-
            IntoIterator::into_iter([Component::from(refs), tags.into()])
-
                .collect::<RefString>()
-
                .and(name),
-
        ))
-
    }
-
}
-

-
impl<T> From<lit::RefsNotes<T>> for Qualified<'_>
-
where
-
    T: AsRef<RefStr>,
-
{
-
    #[inline]
-
    fn from((refs, notes, name): lit::RefsNotes<T>) -> Self {
-
        Self(Cow::Owned(
-
            IntoIterator::into_iter([Component::from(refs), notes.into()])
-
                .collect::<RefString>()
-
                .and(name),
-
        ))
-
    }
-
}
-

-
impl<T> From<lit::RefsRemotes<T>> for Qualified<'_>
-
where
-
    T: AsRef<RefStr>,
-
{
-
    #[inline]
-
    fn from((refs, remotes, name): lit::RefsRemotes<T>) -> Self {
-
        Self(Cow::Owned(
-
            IntoIterator::into_iter([Component::from(refs), remotes.into()])
-
                .collect::<RefString>()
-
                .and(name),
-
        ))
-
    }
-
}
-

-
impl Display for Qualified<'_> {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        self.0.fmt(f)
-
    }
-
}
-

-
/// A [`Qualified`] ref under a git namespace.
-
///
-
/// A ref is namespaced if it starts with "refs/namespaces/", another path
-
/// component, and "refs/". Eg.
-
///
-
///     refs/namespaces/xyz/refs/heads/main
-
///
-
/// Note that namespaces can be nested, so the result of
-
/// [`Namespaced::strip_namespace`] may be convertible to a [`Namespaced`]
-
/// again. For example:
-
///
-
/// ```no_run
-
/// let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
-
/// let namespaced = full.namespaced().unwrap();
-
/// let strip_first = namespaced.strip_namespace();
-
/// let nested = strip_first.namespaced().unwrap();
-
/// let strip_second = nested.strip_namespace();
-
///
-
/// assert_eq!("a", namespaced.namespace().as_str());
-
/// assert_eq!("b", nested.namespace().as_str());
-
/// assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
-
/// assert_eq!("refs/heads/main", strip_second.as_str());
-
/// ```
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Namespaced<'a>(Cow<'a, RefStr>);
-

-
impl<'a> Namespaced<'a> {
-
    pub fn namespace(&self) -> Component {
-
        self.components().nth(2).unwrap()
-
    }
-

-
    pub fn strip_namespace<'b>(&self) -> Qualified<'b> {
-
        const REFS_NAMESPACES: &RefStr = RefStr::from_str("refs/namespaces");
-

-
        Qualified(Cow::Owned(
-
            self.strip_prefix(REFS_NAMESPACES)
-
                .unwrap()
-
                .components()
-
                .skip(1)
-
                .collect(),
-
        ))
-
    }
-

-
    pub fn strip_namespace_recursive<'b>(&self) -> Qualified<'b> {
-
        let mut strip = self.strip_namespace();
-
        while let Some(ns) = strip.to_namespaced() {
-
            strip = ns.strip_namespace();
-
        }
-
        strip
-
    }
-

-
    #[inline]
-
    pub fn to_owned<'b>(&self) -> Namespaced<'b> {
-
        Namespaced(Cow::Owned(self.0.clone().into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_owned<'b>(self) -> Namespaced<'b> {
-
        Namespaced(Cow::Owned(self.0.into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_qualified(self) -> Qualified<'a> {
-
        self.into()
-
    }
-
}
-

-
impl Deref for Namespaced<'_> {
-
    type Target = RefStr;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<RefStr> for Namespaced<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &RefStr {
-
        self
-
    }
-
}
-

-
impl AsRef<str> for Namespaced<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self.0.as_str()
-
    }
-
}
-

-
impl<'a> From<Namespaced<'a>> for Qualified<'a> {
-
    #[inline]
-
    fn from(ns: Namespaced<'a>) -> Self {
-
        Self(ns.0)
-
    }
-
}
-

-
impl<'a> From<&'a RefStr> for Option<Namespaced<'a>> {
-
    fn from(rs: &'a RefStr) -> Self {
-
        let mut cs = rs.iter();
-
        match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) {
-
            ("refs", "namespaces", _, "refs") => Some(Namespaced(Cow::from(rs))),
-

-
            _ => None,
-
        }
-
    }
-
}
-

-
impl<'a, T> From<lit::RefsNamespaces<'_, T>> for Namespaced<'static>
-
where
-
    T: Into<Component<'a>>,
-
{
-
    #[inline]
-
    fn from((refs, namespaces, namespace, name): lit::RefsNamespaces<T>) -> Self {
-
        Self(Cow::Owned(
-
            IntoIterator::into_iter([refs.into(), namespaces.into(), namespace.into()])
-
                .collect::<RefString>()
-
                .and(name),
-
        ))
-
    }
-
}
-

-
impl Display for Namespaced<'_> {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        self.0.fmt(f)
-
    }
-
}
deleted git-ref-format/core/src/lib.rs
@@ -1,25 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
mod check;
-
pub use check::{ref_format as check_ref_format, Error, Options};
-

-
mod deriv;
-
pub use deriv::{Namespaced, Qualified};
-

-
pub mod lit;
-

-
pub mod name;
-
#[cfg(feature = "percent-encoding")]
-
pub use name::PercentEncode;
-
pub use name::{Component, RefStr, RefString};
-

-
pub mod refspec;
-
pub use refspec::DuplicateGlob;
-

-
#[cfg(feature = "minicbor")]
-
mod cbor;
-
#[cfg(feature = "serde")]
-
mod serde;
deleted git-ref-format/core/src/lit.rs
@@ -1,197 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use crate::{name, Qualified, RefStr};
-

-
/// A literal [`RefStr`].
-
///
-
/// Types implementing [`Lit`] must be [`name::Component`]s, and provide a
-
/// conversion from a component _iff_ the component's [`RefStr`] representation
-
/// is equal to [`Lit::NAME`]. Because these morphisms can only be guaranteed
-
/// axiomatically, the trait can not currently be implemented by types outside
-
/// of this crate.
-
///
-
/// [`Lit`] types are useful for efficiently creating known-valid [`Qualified`]
-
/// refs, and sometimes for pattern matching.
-
pub trait Lit: Sized + sealed::Sealed {
-
    const SELF: Self;
-
    const NAME: &'static RefStr;
-

-
    #[inline]
-
    fn from_component(c: &name::Component) -> Option<Self> {
-
        (c.as_ref() == Self::NAME).then_some(Self::SELF)
-
    }
-
}
-

-
impl<T: Lit> From<T> for &'static RefStr {
-
    #[inline]
-
    fn from(_: T) -> Self {
-
        T::NAME
-
    }
-
}
-

-
mod sealed {
-
    pub trait Sealed {}
-
}
-

-
/// All known literal [`RefStr`]s.
-
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub enum KnownLit {
-
    Refs,
-
    Heads,
-
    Namespaces,
-
    Remotes,
-
    Tags,
-
    Notes,
-
}
-

-
impl KnownLit {
-
    #[inline]
-
    pub fn from_component(c: &name::Component) -> Option<Self> {
-
        let c: &RefStr = c.as_ref();
-
        if c == Refs::NAME {
-
            Some(Self::Refs)
-
        } else if c == Heads::NAME {
-
            Some(Self::Heads)
-
        } else if c == Namespaces::NAME {
-
            Some(Self::Namespaces)
-
        } else if c == Remotes::NAME {
-
            Some(Self::Remotes)
-
        } else if c == Tags::NAME {
-
            Some(Self::Tags)
-
        } else if c == Notes::NAME {
-
            Some(Self::Notes)
-
        } else {
-
            None
-
        }
-
    }
-
}
-

-
impl From<KnownLit> for name::Component<'_> {
-
    #[inline]
-
    fn from(k: KnownLit) -> Self {
-
        match k {
-
            KnownLit::Refs => Refs.into(),
-
            KnownLit::Heads => Heads.into(),
-
            KnownLit::Namespaces => Namespaces.into(),
-
            KnownLit::Remotes => Remotes.into(),
-
            KnownLit::Tags => Tags.into(),
-
            KnownLit::Notes => Notes.into(),
-
        }
-
    }
-
}
-

-
/// Either a [`KnownLit`] or a [`name::Component`]
-
pub enum SomeLit<'a> {
-
    Known(KnownLit),
-
    Any(name::Component<'a>),
-
}
-

-
impl SomeLit<'_> {
-
    pub fn known(self) -> Option<KnownLit> {
-
        match self {
-
            Self::Known(k) => Some(k),
-
            _ => None,
-
        }
-
    }
-
}
-

-
impl<'a> From<name::Component<'a>> for SomeLit<'a> {
-
    #[inline]
-
    fn from(c: name::Component<'a>) -> Self {
-
        match KnownLit::from_component(&c) {
-
            Some(k) => Self::Known(k),
-
            None => Self::Any(c),
-
        }
-
    }
-
}
-

-
pub type RefsHeads<T> = (Refs, Heads, T);
-
pub type RefsTags<T> = (Refs, Tags, T);
-
pub type RefsNotes<T> = (Refs, Notes, T);
-
pub type RefsRemotes<T> = (Refs, Remotes, T);
-
pub type RefsNamespaces<'a, T> = (Refs, Namespaces, T, Qualified<'a>);
-

-
#[inline]
-
pub fn refs_heads<T: AsRef<RefStr>>(name: T) -> RefsHeads<T> {
-
    (Refs, Heads, name)
-
}
-

-
#[inline]
-
pub fn refs_tags<T: AsRef<RefStr>>(name: T) -> RefsTags<T> {
-
    (Refs, Tags, name)
-
}
-

-
#[inline]
-
pub fn refs_notes<T: AsRef<RefStr>>(name: T) -> RefsNotes<T> {
-
    (Refs, Notes, name)
-
}
-

-
#[inline]
-
pub fn refs_remotes<T: AsRef<RefStr>>(name: T) -> RefsRemotes<T> {
-
    (Refs, Remotes, name)
-
}
-

-
#[inline]
-
pub fn refs_namespaces<'a, 'b, T>(namespace: T, name: Qualified<'b>) -> RefsNamespaces<'b, T>
-
where
-
    T: Into<name::Component<'a>>,
-
{
-
    (Refs, Namespaces, namespace, name)
-
}
-

-
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Refs;
-

-
impl Lit for Refs {
-
    const SELF: Self = Self;
-
    const NAME: &'static RefStr = name::REFS;
-
}
-
impl sealed::Sealed for Refs {}
-

-
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Heads;
-

-
impl Lit for Heads {
-
    const SELF: Self = Self;
-
    const NAME: &'static RefStr = name::HEADS;
-
}
-
impl sealed::Sealed for Heads {}
-

-
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Namespaces;
-

-
impl Lit for Namespaces {
-
    const SELF: Self = Self;
-
    const NAME: &'static RefStr = name::NAMESPACES;
-
}
-
impl sealed::Sealed for Namespaces {}
-

-
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Remotes;
-

-
impl Lit for Remotes {
-
    const SELF: Self = Self;
-
    const NAME: &'static RefStr = name::REMOTES;
-
}
-
impl sealed::Sealed for Remotes {}
-

-
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Tags;
-

-
impl Lit for Tags {
-
    const SELF: Self = Self;
-
    const NAME: &'static RefStr = name::TAGS;
-
}
-
impl sealed::Sealed for Tags {}
-

-
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Notes;
-

-
impl Lit for Notes {
-
    const SELF: Self = Self;
-
    const NAME: &'static RefStr = name::NOTES;
-
}
-
impl sealed::Sealed for Notes {}
deleted git-ref-format/core/src/name.rs
@@ -1,470 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use std::{
-
    borrow::{Borrow, Cow},
-
    convert::TryFrom,
-
    fmt::{self, Display},
-
    iter::{Extend, FromIterator},
-
    ops::Deref,
-
};
-

-
use crate::{
-
    check,
-
    refspec::{PatternStr, PatternString},
-
    Namespaced,
-
    Qualified,
-
};
-

-
mod iter;
-
pub use iter::{component, Component, Components, Iter};
-

-
#[cfg(feature = "percent-encoding")]
-
pub use percent_encoding::PercentEncode;
-

-
pub const HEADS: &RefStr = RefStr::from_str(str::HEADS);
-
pub const MAIN: &RefStr = RefStr::from_str(str::MAIN);
-
pub const MASTER: &RefStr = RefStr::from_str(str::MASTER);
-
pub const NAMESPACES: &RefStr = RefStr::from_str(str::NAMESPACES);
-
pub const NOTES: &RefStr = RefStr::from_str(str::NOTES);
-
pub const ORIGIN: &RefStr = RefStr::from_str(str::ORIGIN);
-
pub const REFS: &RefStr = RefStr::from_str(str::REFS);
-
pub const REMOTES: &RefStr = RefStr::from_str(str::REMOTES);
-
pub const TAGS: &RefStr = RefStr::from_str(str::TAGS);
-

-
pub const REFS_HEADS_MAIN: &RefStr = RefStr::from_str(str::REFS_HEADS_MAIN);
-
pub const REFS_HEADS_MASTER: &RefStr = RefStr::from_str(str::REFS_HEADS_MASTER);
-

-
pub mod str {
-
    pub const HEADS: &str = "heads";
-
    pub const MAIN: &str = "main";
-
    pub const MASTER: &str = "master";
-
    pub const NAMESPACES: &str = "namespaces";
-
    pub const NOTES: &str = "notes";
-
    pub const ORIGIN: &str = "origin";
-
    pub const REFS: &str = "refs";
-
    pub const REMOTES: &str = "remotes";
-
    pub const TAGS: &str = "tags";
-

-
    pub const REFS_HEADS_MAIN: &str = "refs/heads/main";
-
    pub const REFS_HEADS_MASTER: &str = "refs/heads/master";
-
}
-

-
pub mod bytes {
-
    use super::str;
-

-
    pub const HEADS: &[u8] = str::HEADS.as_bytes();
-
    pub const MAIN: &[u8] = str::MAIN.as_bytes();
-
    pub const MASTER: &[u8] = str::MASTER.as_bytes();
-
    pub const NAMESPACES: &[u8] = str::NAMESPACES.as_bytes();
-
    pub const NOTES: &[u8] = str::NOTES.as_bytes();
-
    pub const ORIGIN: &[u8] = str::ORIGIN.as_bytes();
-
    pub const REFS: &[u8] = str::REFS.as_bytes();
-
    pub const REMOTES: &[u8] = str::REMOTES.as_bytes();
-
    pub const TAGS: &[u8] = str::TAGS.as_bytes();
-

-
    pub const REFS_HEADS_MAIN: &[u8] = str::REFS_HEADS_MAIN.as_bytes();
-
    pub const REFS_HEADS_MASTER: &[u8] = str::REFS_HEADS_MASTER.as_bytes();
-
}
-

-
const CHECK_OPTS: check::Options = check::Options {
-
    allow_pattern: false,
-
    allow_onelevel: true,
-
};
-

-
#[repr(transparent)]
-
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct RefStr(str);
-

-
impl RefStr {
-
    pub fn try_from_str(s: &str) -> Result<&RefStr, check::Error> {
-
        TryFrom::try_from(s)
-
    }
-

-
    #[inline]
-
    pub fn as_str(&self) -> &str {
-
        self
-
    }
-

-
    #[inline]
-
    pub fn to_ref_string(&self) -> RefString {
-
        self.to_owned()
-
    }
-

-
    pub fn strip_prefix<P>(&self, base: P) -> Option<&RefStr>
-
    where
-
        P: AsRef<RefStr>,
-
    {
-
        self._strip_prefix(base.as_ref())
-
    }
-

-
    fn _strip_prefix(&self, base: &RefStr) -> Option<&RefStr> {
-
        self.0
-
            .strip_prefix(base.as_str())
-
            .and_then(|s| s.strip_prefix('/'))
-
            .map(Self::from_str)
-
    }
-

-
    /// Join `other` onto `self`, yielding a new [`RefString`].
-
    ///
-
    /// Consider to use [`RefString::and`] when chaining multiple fragments
-
    /// together, and the intermediate values are not needed.
-
    pub fn join<R>(&self, other: R) -> RefString
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self._join(other.as_ref())
-
    }
-

-
    fn _join(&self, other: &RefStr) -> RefString {
-
        let mut buf = self.to_ref_string();
-
        buf.push(other);
-
        buf
-
    }
-

-
    pub fn to_pattern<P>(&self, pattern: P) -> PatternString
-
    where
-
        P: AsRef<PatternStr>,
-
    {
-
        self._to_pattern(pattern.as_ref())
-
    }
-

-
    fn _to_pattern(&self, pattern: &PatternStr) -> PatternString {
-
        self.to_owned().with_pattern(pattern)
-
    }
-

-
    #[inline]
-
    pub fn qualified(&self) -> Option<Qualified> {
-
        Qualified::from_refstr(self)
-
    }
-

-
    #[inline]
-
    pub fn to_namespaced(&self) -> Option<Namespaced> {
-
        self.into()
-
    }
-

-
    pub fn iter(&self) -> Iter {
-
        self.0.split('/')
-
    }
-

-
    pub fn components(&self) -> Components {
-
        Components::from(self)
-
    }
-

-
    pub fn head(&self) -> Component {
-
        self.components().next().expect("`RefStr` cannot be empty")
-
    }
-

-
    #[cfg(feature = "percent-encoding")]
-
    pub fn percent_encode(&self) -> PercentEncode {
-
        /// https://url.spec.whatwg.org/#fragment-percent-encode-set
-
        const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
-
            &percent_encoding::CONTROLS
-
                .add(b' ')
-
                .add(b'"')
-
                .add(b'<')
-
                .add(b'>')
-
                .add(b'`');
-

-
        /// https://url.spec.whatwg.org/#path-percent-encode-set
-
        const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
-
            .add(b'#')
-
            .add(b'?')
-
            .add(b'{')
-
            .add(b'}');
-

-
        percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
-
    }
-

-
    #[cfg(feature = "bstr")]
-
    #[inline]
-
    pub fn as_bstr(&self) -> &bstr::BStr {
-
        self.as_ref()
-
    }
-

-
    pub(crate) const fn from_str(s: &str) -> &RefStr {
-
        unsafe { &*(s as *const str as *const RefStr) }
-
    }
-
}
-

-
impl Deref for RefStr {
-
    type Target = str;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<str> for RefStr {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self
-
    }
-
}
-

-
#[cfg(feature = "bstr")]
-
impl AsRef<bstr::BStr> for RefStr {
-
    #[inline]
-
    fn as_ref(&self) -> &bstr::BStr {
-
        use bstr::ByteSlice as _;
-
        self.as_str().as_bytes().as_bstr()
-
    }
-
}
-

-
impl<'a> AsRef<RefStr> for &'a RefStr {
-
    #[inline]
-
    fn as_ref(&self) -> &RefStr {
-
        self
-
    }
-
}
-

-
impl<'a> TryFrom<&'a str> for &'a RefStr {
-
    type Error = check::Error;
-

-
    #[inline]
-
    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
-
        check::ref_format(CHECK_OPTS, s).map(|()| RefStr::from_str(s))
-
    }
-
}
-

-
impl<'a> From<&'a RefStr> for Cow<'a, RefStr> {
-
    #[inline]
-
    fn from(rs: &'a RefStr) -> Cow<'a, RefStr> {
-
        Cow::Borrowed(rs)
-
    }
-
}
-

-
impl Display for RefStr {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(self)
-
    }
-
}
-

-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct RefString(String);
-

-
impl RefString {
-
    #[inline]
-
    pub fn as_refstr(&self) -> &RefStr {
-
        self
-
    }
-

-
    /// Join `other` onto `self` in place.
-
    ///
-
    /// This is a consuming version of [`RefString::push`] which can be chained.
-
    /// Prefer this over chaining calls to [`RefStr::join`] if the
-
    /// intermediate values are not needed.
-
    pub fn and<R>(self, other: R) -> Self
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self._and(other.as_ref())
-
    }
-

-
    fn _and(mut self, other: &RefStr) -> Self {
-
        self.push(other);
-
        self
-
    }
-

-
    pub fn push<R>(&mut self, other: R)
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self.0.push('/');
-
        self.0.push_str(other.as_ref().as_str());
-
    }
-

-
    #[inline]
-
    pub fn pop(&mut self) -> bool {
-
        match self.0.rfind('/') {
-
            None => false,
-
            Some(idx) => {
-
                self.0.truncate(idx);
-
                true
-
            },
-
        }
-
    }
-

-
    /// Append a [`PatternStr`], turning self into a new [`PatternString`].
-
    pub fn with_pattern<P>(self, pattern: P) -> PatternString
-
    where
-
        P: AsRef<PatternStr>,
-
    {
-
        self._with_pattern(pattern.as_ref())
-
    }
-

-
    fn _with_pattern(self, pattern: &PatternStr) -> PatternString {
-
        let mut buf = self.0;
-
        buf.push('/');
-
        buf.push_str(pattern.as_str());
-

-
        PatternString(buf)
-
    }
-

-
    #[inline]
-
    pub fn into_qualified<'a>(self) -> Option<Qualified<'a>> {
-
        Qualified::from_refstr(self)
-
    }
-

-
    #[inline]
-
    pub fn reserve(&mut self, additional: usize) {
-
        self.0.reserve(additional)
-
    }
-

-
    #[inline]
-
    pub fn shrink_to_fit(&mut self) {
-
        self.0.shrink_to_fit()
-
    }
-

-
    #[cfg(feature = "bstr")]
-
    #[inline]
-
    pub fn into_bstring(self) -> bstr::BString {
-
        self.into()
-
    }
-

-
    #[cfg(feature = "bstr")]
-
    #[inline]
-
    pub fn as_bstr(&self) -> &bstr::BStr {
-
        self.as_ref()
-
    }
-
}
-

-
impl Deref for RefString {
-
    type Target = RefStr;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        self.borrow()
-
    }
-
}
-

-
impl AsRef<RefStr> for RefString {
-
    #[inline]
-
    fn as_ref(&self) -> &RefStr {
-
        self
-
    }
-
}
-

-
impl AsRef<str> for RefString {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self.0.as_str()
-
    }
-
}
-

-
#[cfg(feature = "bstr")]
-
impl AsRef<bstr::BStr> for RefString {
-
    #[inline]
-
    fn as_ref(&self) -> &bstr::BStr {
-
        use bstr::ByteSlice as _;
-
        self.as_str().as_bytes().as_bstr()
-
    }
-
}
-

-
impl Borrow<RefStr> for RefString {
-
    #[inline]
-
    fn borrow(&self) -> &RefStr {
-
        RefStr::from_str(self.0.as_str())
-
    }
-
}
-

-
impl ToOwned for RefStr {
-
    type Owned = RefString;
-

-
    #[inline]
-
    fn to_owned(&self) -> Self::Owned {
-
        RefString(self.0.to_owned())
-
    }
-
}
-

-
impl TryFrom<&str> for RefString {
-
    type Error = check::Error;
-

-
    #[inline]
-
    fn try_from(s: &str) -> Result<Self, Self::Error> {
-
        RefStr::try_from_str(s).map(ToOwned::to_owned)
-
    }
-
}
-

-
impl TryFrom<String> for RefString {
-
    type Error = check::Error;
-

-
    #[inline]
-
    fn try_from(s: String) -> Result<Self, Self::Error> {
-
        check::ref_format(CHECK_OPTS, s.as_str()).map(|()| RefString(s))
-
    }
-
}
-

-
impl<'a> From<&'a RefString> for Cow<'a, RefStr> {
-
    #[inline]
-
    fn from(rs: &'a RefString) -> Cow<'a, RefStr> {
-
        Cow::Borrowed(rs.as_refstr())
-
    }
-
}
-

-
impl<'a> From<RefString> for Cow<'a, RefStr> {
-
    #[inline]
-
    fn from(rs: RefString) -> Cow<'a, RefStr> {
-
        Cow::Owned(rs)
-
    }
-
}
-

-
impl From<RefString> for String {
-
    #[inline]
-
    fn from(rs: RefString) -> Self {
-
        rs.0
-
    }
-
}
-

-
#[cfg(feature = "bstr")]
-
impl From<RefString> for bstr::BString {
-
    #[inline]
-
    fn from(rs: RefString) -> Self {
-
        bstr::BString::from(rs.0.into_bytes())
-
    }
-
}
-

-
impl Display for RefString {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(&self.0)
-
    }
-
}
-

-
impl<A> FromIterator<A> for RefString
-
where
-
    A: AsRef<RefStr>,
-
{
-
    fn from_iter<T>(iter: T) -> Self
-
    where
-
        T: IntoIterator<Item = A>,
-
    {
-
        let mut buf = String::new();
-
        for c in iter {
-
            buf.push_str(c.as_ref().as_str());
-
            buf.push('/');
-
        }
-
        assert!(!buf.is_empty(), "empty iterator");
-
        buf.truncate(buf.len() - 1);
-

-
        Self(buf)
-
    }
-
}
-

-
impl<A> Extend<A> for RefString
-
where
-
    A: AsRef<RefStr>,
-
{
-
    fn extend<T>(&mut self, iter: T)
-
    where
-
        T: IntoIterator<Item = A>,
-
    {
-
        for x in iter {
-
            self.push(x)
-
        }
-
    }
-
}
deleted git-ref-format/core/src/name/iter.rs
@@ -1,160 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use std::{
-
    borrow::Cow,
-
    fmt::{self, Display},
-
    ops::Deref,
-
};
-

-
use super::RefStr;
-
use crate::lit;
-

-
pub type Iter<'a> = std::str::Split<'a, char>;
-

-
/// Iterator created by the [`RefStr::iter`] method.
-
#[must_use = "iterators are lazy and do nothing unless consumed"]
-
#[derive(Clone)]
-
pub struct Components<'a> {
-
    inner: std::str::Split<'a, char>,
-
}
-

-
impl<'a> Iterator for Components<'a> {
-
    type Item = Component<'a>;
-

-
    #[inline]
-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.inner
-
            .next()
-
            .map(RefStr::from_str)
-
            .map(Cow::from)
-
            .map(Component)
-
    }
-
}
-

-
impl<'a> DoubleEndedIterator for Components<'a> {
-
    #[inline]
-
    fn next_back(&mut self) -> Option<Self::Item> {
-
        self.inner
-
            .next_back()
-
            .map(RefStr::from_str)
-
            .map(Cow::from)
-
            .map(Component)
-
    }
-
}
-

-
impl<'a> From<&'a RefStr> for Components<'a> {
-
    #[inline]
-
    fn from(rs: &'a RefStr) -> Self {
-
        Self {
-
            inner: rs.as_str().split('/'),
-
        }
-
    }
-
}
-

-
/// A path component of a [`RefStr`].
-
///
-
/// A [`Component`] is a valid [`RefStr`] which does not contain any '/'
-
/// separators.
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct Component<'a>(Cow<'a, RefStr>);
-

-
impl<'a> Component<'a> {
-
    #[inline]
-
    pub fn from_refstr(r: impl Into<Cow<'a, RefStr>>) -> Option<Self> {
-
        let r = r.into();
-
        if !r.contains('/') {
-
            Some(Self(r))
-
        } else {
-
            None
-
        }
-
    }
-

-
    #[inline]
-
    pub fn as_lit<T: lit::Lit>(&self) -> Option<T> {
-
        T::from_component(self)
-
    }
-

-
    #[inline]
-
    pub fn into_inner(self) -> Cow<'a, RefStr> {
-
        self.0
-
    }
-
}
-

-
impl<'a> Deref for Component<'a> {
-
    type Target = RefStr;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<RefStr> for Component<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &RefStr {
-
        self
-
    }
-
}
-

-
impl<'a> From<&'a RefStr> for Option<Component<'a>> {
-
    #[inline]
-
    fn from(r: &'a RefStr) -> Self {
-
        if !r.contains('/') {
-
            Some(Component(Cow::from(r)))
-
        } else {
-
            None
-
        }
-
    }
-
}
-

-
impl<'a> From<Component<'a>> for Cow<'a, RefStr> {
-
    #[inline]
-
    fn from(c: Component<'a>) -> Self {
-
        c.0
-
    }
-
}
-

-
impl<T: lit::Lit> From<T> for Component<'static> {
-
    #[inline]
-
    fn from(_: T) -> Self {
-
        Component(Cow::from(T::NAME))
-
    }
-
}
-

-
impl<'a> From<lit::SomeLit<'a>> for Component<'a> {
-
    #[inline]
-
    fn from(s: lit::SomeLit<'a>) -> Self {
-
        use lit::SomeLit::*;
-

-
        match s {
-
            Known(k) => k.into(),
-
            Any(c) => c,
-
        }
-
    }
-
}
-

-
impl Display for Component<'_> {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(self.0.as_str())
-
    }
-
}
-

-
pub mod component {
-
    use super::Component;
-
    use crate::name;
-
    use std::borrow::Cow;
-

-
    pub const HEADS: Component = Component(Cow::Borrowed(name::HEADS));
-
    pub const MAIN: Component = Component(Cow::Borrowed(name::MAIN));
-
    pub const MASTER: Component = Component(Cow::Borrowed(name::MASTER));
-
    pub const NAMESPACES: Component = Component(Cow::Borrowed(name::NAMESPACES));
-
    pub const NOTES: Component = Component(Cow::Borrowed(name::NOTES));
-
    pub const ORIGIN: Component = Component(Cow::Borrowed(name::ORIGIN));
-
    pub const REFS: Component = Component(Cow::Borrowed(name::REFS));
-
    pub const REMOTES: Component = Component(Cow::Borrowed(name::REMOTES));
-
    pub const TAGS: Component = Component(Cow::Borrowed(name::TAGS));
-
}
deleted git-ref-format/core/src/refspec.rs
@@ -1,560 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use std::{
-
    borrow::{Borrow, Cow},
-
    convert::TryFrom,
-
    fmt::{self, Display},
-
    iter::FromIterator,
-
    ops::Deref,
-
};
-

-
use thiserror::Error;
-

-
use crate::{check, lit, RefStr, RefString};
-

-
mod iter;
-
pub use iter::{component, Component, Components, Iter};
-

-
pub const STAR: &PatternStr = PatternStr::from_str("*");
-

-
const CHECK_OPTS: check::Options = check::Options {
-
    allow_onelevel: true,
-
    allow_pattern: true,
-
};
-

-
#[repr(transparent)]
-
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct PatternStr(str);
-

-
impl PatternStr {
-
    #[inline]
-
    pub fn try_from_str(s: &str) -> Result<&Self, check::Error> {
-
        TryFrom::try_from(s)
-
    }
-

-
    #[inline]
-
    pub fn as_str(&self) -> &str {
-
        self
-
    }
-

-
    pub fn join<R>(&self, other: R) -> PatternString
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self._join(other.as_ref())
-
    }
-

-
    fn _join(&self, other: &RefStr) -> PatternString {
-
        let mut buf = self.to_owned();
-
        buf.push(other);
-
        buf
-
    }
-

-
    #[inline]
-
    pub fn qualified(&self) -> Option<QualifiedPattern> {
-
        QualifiedPattern::from_patternstr(self)
-
    }
-

-
    #[inline]
-
    pub fn to_namespaced(&self) -> Option<NamespacedPattern> {
-
        self.into()
-
    }
-

-
    #[inline]
-
    pub fn iter(&self) -> Iter {
-
        self.0.split('/')
-
    }
-

-
    #[inline]
-
    pub fn components(&self) -> Components {
-
        Components::from(self)
-
    }
-

-
    pub(crate) const fn from_str(s: &str) -> &PatternStr {
-
        unsafe { &*(s as *const str as *const PatternStr) }
-
    }
-
}
-

-
impl Deref for PatternStr {
-
    type Target = str;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<str> for PatternStr {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self
-
    }
-
}
-

-
impl AsRef<Self> for PatternStr {
-
    #[inline]
-
    fn as_ref(&self) -> &Self {
-
        self
-
    }
-
}
-

-
impl<'a> TryFrom<&'a str> for &'a PatternStr {
-
    type Error = check::Error;
-

-
    #[inline]
-
    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
-
        check::ref_format(CHECK_OPTS, s).map(|()| PatternStr::from_str(s))
-
    }
-
}
-

-
impl<'a> From<&'a RefStr> for &'a PatternStr {
-
    #[inline]
-
    fn from(rs: &'a RefStr) -> Self {
-
        PatternStr::from_str(rs.as_str())
-
    }
-
}
-

-
impl<'a> From<&'a PatternStr> for Cow<'a, PatternStr> {
-
    #[inline]
-
    fn from(p: &'a PatternStr) -> Cow<'a, PatternStr> {
-
        Cow::Borrowed(p)
-
    }
-
}
-

-
impl Display for PatternStr {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(self)
-
    }
-
}
-

-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct PatternString(pub(crate) String);
-

-
impl PatternString {
-
    #[inline]
-
    pub fn as_str(&self) -> &str {
-
        self.as_ref()
-
    }
-

-
    #[inline]
-
    pub fn as_pattern_str(&self) -> &PatternStr {
-
        self.as_ref()
-
    }
-

-
    #[inline]
-
    pub fn from_components<'a, T>(iter: T) -> Result<Self, DuplicateGlob>
-
    where
-
        T: IntoIterator<Item = Component<'a>>,
-
    {
-
        iter.into_iter().collect()
-
    }
-

-
    #[inline]
-
    pub fn and<R>(mut self, other: R) -> Self
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self._push(other.as_ref());
-
        self
-
    }
-

-
    #[inline]
-
    pub fn push<R>(&mut self, other: R)
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self._push(other.as_ref())
-
    }
-

-
    fn _push(&mut self, other: &RefStr) {
-
        self.0.push('/');
-
        self.0.push_str(other.as_str());
-
    }
-

-
    #[inline]
-
    pub fn pop(&mut self) -> bool {
-
        match self.0.rfind('/') {
-
            None => false,
-
            Some(idx) => {
-
                self.0.truncate(idx);
-
                true
-
            },
-
        }
-
    }
-
}
-

-
impl Deref for PatternString {
-
    type Target = PatternStr;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        self.borrow()
-
    }
-
}
-

-
impl AsRef<PatternStr> for PatternString {
-
    #[inline]
-
    fn as_ref(&self) -> &PatternStr {
-
        self
-
    }
-
}
-

-
impl AsRef<str> for PatternString {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self.0.as_str()
-
    }
-
}
-

-
impl Borrow<PatternStr> for PatternString {
-
    #[inline]
-
    fn borrow(&self) -> &PatternStr {
-
        PatternStr::from_str(self.0.as_str())
-
    }
-
}
-

-
impl ToOwned for PatternStr {
-
    type Owned = PatternString;
-

-
    #[inline]
-
    fn to_owned(&self) -> Self::Owned {
-
        PatternString(self.0.to_owned())
-
    }
-
}
-

-
impl From<RefString> for PatternString {
-
    #[inline]
-
    fn from(rs: RefString) -> Self {
-
        Self(rs.into())
-
    }
-
}
-

-
impl<'a> From<&'a PatternString> for Cow<'a, PatternStr> {
-
    #[inline]
-
    fn from(p: &'a PatternString) -> Cow<'a, PatternStr> {
-
        Cow::Borrowed(p.as_ref())
-
    }
-
}
-

-
impl From<PatternString> for String {
-
    #[inline]
-
    fn from(p: PatternString) -> Self {
-
        p.0
-
    }
-
}
-

-
impl TryFrom<&str> for PatternString {
-
    type Error = check::Error;
-

-
    #[inline]
-
    fn try_from(s: &str) -> Result<Self, Self::Error> {
-
        PatternStr::try_from_str(s).map(ToOwned::to_owned)
-
    }
-
}
-

-
impl TryFrom<String> for PatternString {
-
    type Error = check::Error;
-

-
    #[inline]
-
    fn try_from(s: String) -> Result<Self, Self::Error> {
-
        check::ref_format(CHECK_OPTS, s.as_str()).map(|()| PatternString(s))
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[error("more than one '*' encountered")]
-
pub struct DuplicateGlob;
-

-
impl<'a> FromIterator<Component<'a>> for Result<PatternString, DuplicateGlob> {
-
    fn from_iter<T>(iter: T) -> Self
-
    where
-
        T: IntoIterator<Item = Component<'a>>,
-
    {
-
        use Component::*;
-

-
        let mut buf = String::new();
-
        let mut seen_glob = false;
-
        for c in iter {
-
            if let Glob(_) = c {
-
                if seen_glob {
-
                    return Err(DuplicateGlob);
-
                }
-

-
                seen_glob = true;
-
            }
-

-
            buf.push_str(c.as_str());
-
            buf.push('/');
-
        }
-
        buf.truncate(buf.len() - 1);
-

-
        Ok(PatternString(buf))
-
    }
-
}
-

-
impl Display for PatternString {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(&self.0)
-
    }
-
}
-

-
/// A fully-qualified refspec.
-
///
-
/// A refspec is qualified _iff_ it starts with "refs/" and has at least three
-
/// components. This implies that a [`QualifiedPattern`] ref has a category,
-
/// such as "refs/heads/*".
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct QualifiedPattern<'a>(pub(crate) Cow<'a, PatternStr>);
-

-
impl<'a> QualifiedPattern<'a> {
-
    pub fn from_patternstr(r: impl Into<Cow<'a, PatternStr>>) -> Option<Self> {
-
        Self::_from_patternstr(r.into())
-
    }
-

-
    fn _from_patternstr(r: Cow<'a, PatternStr>) -> Option<Self> {
-
        let mut iter = r.iter();
-
        match (iter.next()?, iter.next()?, iter.next()?) {
-
            ("refs", _, _) => Some(QualifiedPattern(r)),
-
            _ => None,
-
        }
-
    }
-

-
    #[inline]
-
    pub fn as_str(&self) -> &str {
-
        self.as_ref()
-
    }
-

-
    #[inline]
-
    pub fn join<'b, R>(&self, other: R) -> QualifiedPattern<'b>
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        QualifiedPattern(Cow::Owned(self.0.join(other)))
-
    }
-

-
    #[inline]
-
    pub fn to_namespaced(&self) -> Option<NamespacedPattern> {
-
        self.0.as_ref().into()
-
    }
-

-
    /// Add a namespace.
-
    ///
-
    /// Creates a new [`NamespacedPattern`] by prefxing `self` with
-
    /// `refs/namespaces/<ns>`.
-
    pub fn with_namespace<'b>(
-
        &self,
-
        ns: Component<'b>,
-
    ) -> Result<NamespacedPattern<'a>, DuplicateGlob> {
-
        PatternString::from_components(
-
            IntoIterator::into_iter([lit::Refs.into(), lit::Namespaces.into(), ns])
-
                .chain(self.components()),
-
        )
-
        .map(|pat| NamespacedPattern(Cow::Owned(pat)))
-
    }
-

-
    /// Like [`Self::non_empty_components`], but with string slices.
-
    pub fn non_empty_iter(&self) -> (&str, &str, &str, Iter) {
-
        let mut iter = self.iter();
-
        (
-
            iter.next().unwrap(),
-
            iter.next().unwrap(),
-
            iter.next().unwrap(),
-
            iter,
-
        )
-
    }
-

-
    /// Return the first three [`Component`]s, and a possibly empty iterator
-
    /// over the remaining ones.
-
    ///
-
    /// A qualified ref is guaranteed to have at least three components, which
-
    /// this method provides a witness of. This is useful eg. for pattern
-
    /// matching on the prefix.
-
    pub fn non_empty_components(&self) -> (Component, Component, Component, Components) {
-
        let mut cs = self.components();
-
        (
-
            cs.next().unwrap(),
-
            cs.next().unwrap(),
-
            cs.next().unwrap(),
-
            cs,
-
        )
-
    }
-

-
    #[inline]
-
    pub fn to_owned<'b>(&self) -> QualifiedPattern<'b> {
-
        QualifiedPattern(Cow::Owned(self.0.clone().into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_owned<'b>(self) -> QualifiedPattern<'b> {
-
        QualifiedPattern(Cow::Owned(self.0.into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_patternstring(self) -> PatternString {
-
        self.into()
-
    }
-
}
-

-
impl Deref for QualifiedPattern<'_> {
-
    type Target = PatternStr;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<PatternStr> for QualifiedPattern<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &PatternStr {
-
        self
-
    }
-
}
-

-
impl AsRef<str> for QualifiedPattern<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self.0.as_str()
-
    }
-
}
-

-
impl AsRef<Self> for QualifiedPattern<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &Self {
-
        self
-
    }
-
}
-

-
impl<'a> From<QualifiedPattern<'a>> for Cow<'a, PatternStr> {
-
    #[inline]
-
    fn from(q: QualifiedPattern<'a>) -> Self {
-
        q.0
-
    }
-
}
-

-
impl From<QualifiedPattern<'_>> for PatternString {
-
    #[inline]
-
    fn from(q: QualifiedPattern) -> Self {
-
        q.0.into_owned()
-
    }
-
}
-

-
/// A [`PatternString`] ref under a git namespace.
-
///
-
/// A ref is namespaced if it starts with "refs/namespaces/", another path
-
/// component, and "refs/". Eg.
-
///
-
///     refs/namespaces/xyz/refs/heads/main
-
///
-
/// Note that namespaces can be nested, so the result of
-
/// [`NamespacedPattern::strip_namespace`] may be convertible to a
-
/// [`NamespacedPattern`] again. For example:
-
///
-
/// ```no_run
-
/// let full = pattern!("refs/namespaces/a/refs/namespaces/b/refs/heads/*");
-
/// let namespaced = full.to_namespaced().unwrap();
-
/// let strip_first = namespaced.strip_namespace();
-
/// let nested = strip_first.namespaced().unwrap();
-
/// let strip_second = nested.strip_namespace();
-
///
-
/// assert_eq!("a", namespaced.namespace().as_str());
-
/// assert_eq!("b", nested.namespace().as_str());
-
/// assert_eq!("refs/namespaces/b/refs/heads/*", strip_first.as_str());
-
/// assert_eq!("refs/heads/*", strip_second.as_str());
-
/// ```
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
pub struct NamespacedPattern<'a>(Cow<'a, PatternStr>);
-

-
impl<'a> NamespacedPattern<'a> {
-
    pub fn namespace(&self) -> Component {
-
        self.components().nth(2).unwrap()
-
    }
-

-
    pub fn strip_namespace(&self) -> PatternString {
-
        PatternString::from_components(self.components().skip(3))
-
            .expect("BUG: NamespacedPattern was constructed with a duplicate glob")
-
    }
-

-
    pub fn strip_namespace_recursive(&self) -> PatternString {
-
        let mut strip = self.strip_namespace();
-
        while let Some(ns) = strip.to_namespaced() {
-
            strip = ns.strip_namespace();
-
        }
-
        strip
-
    }
-

-
    #[inline]
-
    pub fn to_owned<'b>(&self) -> NamespacedPattern<'b> {
-
        NamespacedPattern(Cow::Owned(self.0.clone().into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_owned<'b>(self) -> NamespacedPattern<'b> {
-
        NamespacedPattern(Cow::Owned(self.0.into_owned()))
-
    }
-

-
    #[inline]
-
    pub fn into_qualified(self) -> QualifiedPattern<'a> {
-
        self.into()
-
    }
-
}
-

-
impl Deref for NamespacedPattern<'_> {
-
    type Target = PatternStr;
-

-
    #[inline]
-
    fn deref(&self) -> &Self::Target {
-
        self.borrow()
-
    }
-
}
-

-
impl AsRef<PatternStr> for NamespacedPattern<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &PatternStr {
-
        self.0.as_ref()
-
    }
-
}
-

-
impl AsRef<str> for NamespacedPattern<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        self.0.as_str()
-
    }
-
}
-

-
impl Borrow<PatternStr> for NamespacedPattern<'_> {
-
    #[inline]
-
    fn borrow(&self) -> &PatternStr {
-
        PatternStr::from_str(self.0.as_str())
-
    }
-
}
-

-
impl<'a> From<NamespacedPattern<'a>> for QualifiedPattern<'a> {
-
    #[inline]
-
    fn from(ns: NamespacedPattern<'a>) -> Self {
-
        Self(ns.0)
-
    }
-
}
-

-
impl<'a> From<&'a PatternStr> for Option<NamespacedPattern<'a>> {
-
    fn from(rs: &'a PatternStr) -> Self {
-
        let mut cs = rs.iter();
-
        match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) {
-
            ("refs", "namespaces", _, "refs") => Some(NamespacedPattern(Cow::from(rs))),
-

-
            _ => None,
-
        }
-
    }
-
}
-

-
impl Display for NamespacedPattern<'_> {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        self.0.fmt(f)
-
    }
-
}
deleted git-ref-format/core/src/refspec/iter.rs
@@ -1,103 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use std::fmt::{self, Display};
-

-
use super::PatternStr;
-
use crate::{lit, RefStr};
-

-
pub type Iter<'a> = std::str::Split<'a, char>;
-

-
pub enum Component<'a> {
-
    Glob(Option<&'a PatternStr>),
-
    Normal(&'a RefStr),
-
}
-

-
impl Component<'_> {
-
    #[inline]
-
    pub fn as_str(&self) -> &str {
-
        self.as_ref()
-
    }
-
}
-

-
impl AsRef<str> for Component<'_> {
-
    #[inline]
-
    fn as_ref(&self) -> &str {
-
        match self {
-
            Self::Glob(None) => "*",
-
            Self::Glob(Some(x)) => x.as_str(),
-
            Self::Normal(x) => x.as_str(),
-
        }
-
    }
-
}
-

-
impl Display for Component<'_> {
-
    #[inline]
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(self.as_str())
-
    }
-
}
-

-
impl<T: lit::Lit> From<T> for Component<'static> {
-
    #[inline]
-
    fn from(_: T) -> Self {
-
        Self::Normal(T::NAME)
-
    }
-
}
-

-
#[must_use = "iterators are lazy and do nothing unless consumed"]
-
#[derive(Clone)]
-
pub struct Components<'a> {
-
    inner: Iter<'a>,
-
}
-

-
impl<'a> Iterator for Components<'a> {
-
    type Item = Component<'a>;
-

-
    #[inline]
-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.inner.next().map(|next| match next {
-
            "*" => Component::Glob(None),
-
            x if x.contains('*') => Component::Glob(Some(PatternStr::from_str(x))),
-
            x => Component::Normal(RefStr::from_str(x)),
-
        })
-
    }
-
}
-

-
impl<'a> DoubleEndedIterator for Components<'a> {
-
    #[inline]
-
    fn next_back(&mut self) -> Option<Self::Item> {
-
        self.inner.next_back().map(|next| match next {
-
            "*" => Component::Glob(None),
-
            x if x.contains('*') => Component::Glob(Some(PatternStr::from_str(x))),
-
            x => Component::Normal(RefStr::from_str(x)),
-
        })
-
    }
-
}
-

-
impl<'a> From<&'a PatternStr> for Components<'a> {
-
    #[inline]
-
    fn from(p: &'a PatternStr) -> Self {
-
        Self {
-
            inner: p.as_str().split('/'),
-
        }
-
    }
-
}
-

-
pub mod component {
-
    use super::Component;
-
    use crate::name;
-

-
    pub const STAR: Component = Component::Glob(None);
-
    pub const HEADS: Component = Component::Normal(name::HEADS);
-
    pub const MAIN: Component = Component::Normal(name::MAIN);
-
    pub const MASTER: Component = Component::Normal(name::MASTER);
-
    pub const NAMESPACES: Component = Component::Normal(name::NAMESPACES);
-
    pub const NOTES: Component = Component::Normal(name::NOTES);
-
    pub const ORIGIN: Component = Component::Normal(name::ORIGIN);
-
    pub const REFS: Component = Component::Normal(name::REFS);
-
    pub const REMOTES: Component = Component::Normal(name::REMOTES);
-
    pub const TAGS: Component = Component::Normal(name::TAGS);
-
}
deleted git-ref-format/core/src/serde.rs
@@ -1,150 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
use std::convert::TryFrom;
-

-
use ::serde::{de, Deserialize, Deserializer, Serialize, Serializer};
-

-
use crate::{
-
    refspec::{PatternStr, PatternString},
-
    Namespaced,
-
    Qualified,
-
    RefStr,
-
    RefString,
-
};
-

-
impl<'de: 'a, 'a> Deserialize<'de> for &'a RefStr {
-
    #[inline]
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        Deserialize::deserialize(deserializer)
-
            .and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
-
    }
-
}
-

-
impl<'a> Serialize for &'a RefStr {
-
    #[inline]
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(self.as_str())
-
    }
-
}
-

-
impl<'de> Deserialize<'de> for RefString {
-
    #[inline]
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        Deserialize::deserialize(deserializer)
-
            .and_then(|x: String| Self::try_from(x).map_err(de::Error::custom))
-
    }
-
}
-

-
impl Serialize for RefString {
-
    #[inline]
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(self.as_str())
-
    }
-
}
-

-
impl<'de: 'a, 'a> Deserialize<'de> for &'a PatternStr {
-
    #[inline]
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        Deserialize::deserialize(deserializer)
-
            .and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
-
    }
-
}
-

-
impl<'a> Serialize for &'a PatternStr {
-
    #[inline]
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(self.as_str())
-
    }
-
}
-

-
impl<'de> Deserialize<'de> for PatternString {
-
    #[inline]
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        Deserialize::deserialize(deserializer)
-
            .and_then(|x: String| Self::try_from(x).map_err(de::Error::custom))
-
    }
-
}
-

-
impl Serialize for PatternString {
-
    #[inline]
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(self.as_str())
-
    }
-
}
-

-
impl<'de> Deserialize<'de> for Qualified<'static> {
-
    #[inline]
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        Deserialize::deserialize(deserializer).and_then(|s: String| {
-
            let s = RefString::try_from(s).map_err(de::Error::custom)?;
-
            s.qualified()
-
                .ok_or_else(|| de::Error::custom("not a qualified ref"))
-
                .map(|q| q.into_owned())
-
        })
-
    }
-
}
-

-
impl Serialize for Qualified<'_> {
-
    #[inline]
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(self.as_str())
-
    }
-
}
-

-
impl<'de> Deserialize<'de> for Namespaced<'static> {
-
    #[inline]
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        Deserialize::deserialize(deserializer).and_then(|s: String| {
-
            let s = RefString::try_from(s).map_err(de::Error::custom)?;
-
            s.to_namespaced()
-
                .ok_or_else(|| de::Error::custom("not a namespaced ref"))
-
                .map(|ns| ns.into_owned())
-
        })
-
    }
-
}
-

-
impl Serialize for Namespaced<'_> {
-
    #[inline]
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(self.as_str())
-
    }
-
}
deleted git-ref-format/macro/CHANGELOG.md
@@ -1,5 +0,0 @@
-
# CHANGELOG
-

-
## Version 0.2.0
-

-
* 2022-11-16 Import Qualified within macro [629191f](https://github.com/radicle-dev/radicle-git/commit/629191f55afe77c00808cfe3d6f35d4cff1f4f73)
deleted git-ref-format/macro/Cargo.toml
@@ -1,22 +0,0 @@
-
[package]
-
name = "git-ref-format-macro"
-
version = "0.2.0"
-
authors = ["Kim Altintop <kim@eagain.st>"]
-
edition = "2018"
-
license = "GPL-3.0-or-later"
-
description = "Macros for the git-ref-format crate"
-
keywords = ["git", "references"]
-

-
[lib]
-
doctest = false
-
proc-macro = true
-
test = false
-

-
[dependencies]
-
proc-macro-error = "1.0.4"
-
quote = "1"
-
syn = "1"
-

-
[dependencies.git-ref-format-core]
-
version = "0.2.0"
-
path = "../core"
deleted git-ref-format/macro/src/lib.rs
@@ -1,168 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
#[macro_use]
-
extern crate proc_macro_error;
-

-
use std::convert::TryInto;
-

-
use proc_macro::TokenStream;
-
use proc_macro_error::abort;
-
use quote::quote;
-
use syn::{parse_macro_input, LitStr};
-

-
use git_ref_format_core::{refspec::PatternStr, Component, Error, Qualified, RefStr};
-

-
/// Create a [`git_ref_format_core::RefString`] from a string literal.
-
///
-
/// The string is validated at compile time, and an unsafe conversion is
-
/// emitted.
-
#[proc_macro_error]
-
#[proc_macro]
-
pub fn refname(input: TokenStream) -> TokenStream {
-
    let lit = parse_macro_input!(input as LitStr);
-
    let val = lit.value();
-

-
    let parsed: Result<&RefStr, Error> = val.as_str().try_into();
-
    match parsed {
-
        Ok(safe) => {
-
            let safe: &str = safe.as_str();
-
            let expand = quote! {
-
                unsafe {
-
                    use ::std::mem::transmute;
-
                    use ::git_ref_format::RefString;
-

-
                    transmute::<_, RefString>(#safe.to_owned())
-
                }
-
            };
-
            TokenStream::from(expand)
-
        },
-

-
        Err(e) => {
-
            abort!(lit.span(), "invalid refname literal: {}", e);
-
        },
-
    }
-
}
-

-
/// Create a [`git_ref_format_core::Qualified`] from a string literal.
-
///
-
/// The string is validated at compile time, and an unsafe conversion is
-
/// emitted.
-
#[proc_macro_error]
-
#[proc_macro]
-
pub fn qualified(input: TokenStream) -> TokenStream {
-
    let lit = parse_macro_input!(input as LitStr);
-
    let val = lit.value();
-

-
    let parsed: Result<&RefStr, Error> = val.as_str().try_into();
-
    match parsed {
-
        Ok(name) => {
-
            let qualified: Option<Qualified> = Qualified::from_refstr(name);
-
            match qualified {
-
                Some(safe) => {
-
                    let safe: &str = safe.as_str();
-
                    let expand = quote! {
-
                        unsafe {
-
                            use ::std::{borrow::Cow, mem::transmute};
-
                            use ::git_ref_format::{Component, RefStr, RefString, Qualified};
-

-
                            let inner: RefString = transmute(#safe.to_owned());
-
                            let cow: Cow<'static, RefStr> = Cow::Owned(inner);
-
                            transmute::<_, Qualified>(cow)
-
                        }
-
                    };
-

-
                    TokenStream::from(expand)
-
                },
-

-
                None => {
-
                    abort!(
-
                        lit.span(),
-
                        "refname is not of the form 'refs/<category>/<name>'"
-
                    );
-
                },
-
            }
-
        },
-

-
        Err(e) => {
-
            abort!(lit.span(), "invalid refname literal: {}", e);
-
        },
-
    }
-
}
-

-
/// Create a [`git_ref_format_core::Component`] from a string literal.
-
///
-
/// The string is validated at compile time, and an unsafe conversion is
-
/// emitted.
-
#[proc_macro_error]
-
#[proc_macro]
-
pub fn component(input: TokenStream) -> TokenStream {
-
    let lit = parse_macro_input!(input as LitStr);
-
    let val = lit.value();
-

-
    let name: Result<&RefStr, Error> = val.as_str().try_into();
-
    match name {
-
        Ok(name) => {
-
            let comp: Option<Component> = name.into();
-
            match comp {
-
                Some(safe) => {
-
                    let safe: &str = safe.as_ref().as_str();
-
                    let expand = quote! {
-
                        unsafe {
-
                            use ::std::{borrow::Cow, mem::transmute};
-
                            use ::git_ref_format::{Component, RefStr, RefString};
-

-
                            let inner: RefString = transmute(#safe.to_owned());
-
                            let cow: Cow<'static, RefStr> = Cow::Owned(inner);
-
                            transmute::<_, Component>(cow)
-
                        }
-
                    };
-

-
                    TokenStream::from(expand)
-
                },
-

-
                None => {
-
                    abort!(lit.span(), "component contains a '/'");
-
                },
-
            }
-
        },
-

-
        Err(e) => {
-
            abort!(lit.span(), "invalid refname literal: {}", e);
-
        },
-
    }
-
}
-

-
/// Create a [`git_ref_format_core::refspec::PatternString`] from a string
-
/// literal.
-
///
-
/// The string is validated at compile time, and an unsafe conversion is
-
/// emitted.
-
#[proc_macro_error]
-
#[proc_macro]
-
pub fn pattern(input: TokenStream) -> TokenStream {
-
    let lit = parse_macro_input!(input as LitStr);
-
    let val = lit.value();
-

-
    let parsed: Result<&PatternStr, Error> = val.as_str().try_into();
-
    match parsed {
-
        Ok(safe) => {
-
            let safe: &str = safe.as_str();
-
            let expand = quote! {
-
                unsafe {
-
                    use ::std::mem::transmute;
-
                    use ::git_ref_format::refspec::PatternString;
-

-
                    transmute::<_, PatternString>(#safe.to_owned())
-
                }
-
            };
-
            TokenStream::from(expand)
-
        },
-

-
        Err(e) => {
-
            abort!(lit.span(), "invalid refspec pattern literal: {}", e);
-
        },
-
    }
-
}
deleted git-ref-format/src/lib.rs
@@ -1,157 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
//! Everything you never knew you wanted for handling git ref names.
-
//!
-
//! # Overview
-
//!
-
//! This crate provides a number of types which allow to validate git ref names,
-
//! create new ones which are valid by construction, make assertions about their
-
//! structure, and deconstruct them into their components.
-
//!
-
//! ## Basic Types
-
//!
-
//! The basic types are:
-
//!
-
//! * [`RefStr`]
-
//! * [`RefString`]
-
//!
-
//! They are wrappers around [`str`] and [`String`] respectively, with the
-
//! additional guarantee that they are also valid ref names as per
-
//! [`git-check-ref-format`] (which is also exposed directly as
-
//! [`check_ref_format`]). Both types are referred to as "ref strings".
-
//!
-
//! Note that this implies that ref names must be valid UTF-8, which git itself
-
//! doesn't require.
-
//!
-
//! Ref strings can be iterated over, either yielding `&str` or [`Component`]. A
-
//! [`Component`] is guaranteed to not contain a '/' separator, and can thus
-
//! also be used to conveniently construct known-valid ref strings. The [`lit`]
-
//! module contains a number of types (and `const` values thereof) which can be
-
//! coerced into [`Component`], and thus can be used to construct known-valid
-
//! ref strings.
-
//!
-
//! The [`name`] module also provides a number of constant values of commonly
-
//! used ref strings / components, which are useful for pattern matching.
-
//!
-
//! The `"macro"` feature enables the `refstring!` and `component!` macros,
-
//! which can be convenient to construct compile-time validated [`RefString`]s
-
//! respectively [`Component`]s.
-
//!
-
//! ## Refspec Patterns
-
//!
-
//! The types
-
//!
-
//! * [`refspec::PatternStr`]
-
//! * [`refspec::PatternString`]
-
//!
-
//! guarantee that their values are valid ref strings but additionally _may_
-
//! contain at most one "*" character. It is thus possible to convert a ref
-
//! string to a refspec pattern, but not the other way round. Refspec patterns
-
//! are commonly used for mapping remote to local refs (cf. [`git-fetch`]).
-
//!
-
//! The `"macro"` feature enables the `refspec::pattern!` macro, which
-
//! constructs a compile-time validated [`refspec::PatternString`].
-
//!
-
//! ## Structured Ref Strings
-
//!
-
//! Ref strings may be [`Qualified`], which essentially means that they start
-
//! with "refs/". [`Qualified`] ref string also require at least three
-
//! components (eg. "refs/heads/main"), which makes it easier to deal with
-
//! common naming conventions.
-
//!
-
//! [`Qualified`] refs may be [`Namespaced`], or can be given a namespace
-
//! (namespaces can be nested). [`Namespaced`] refs are also [`Qualified`], and
-
//! can have their namespace(s) stripped.
-
//!
-
//! # On Git Ref Name Conventions
-
//!
-
//! Git references are essentially path names pointing to their traditional
-
//! storage location in a the repository (`$GIT_DIR/refs`). Unlike (UNIX) file
-
//! paths, they are subject to a few restrictions, as described in
-
//! [`git-check-ref-format`].
-
//!
-
//! On top of that, there are a number of conventions around the hierarchical
-
//! naming, _some_ of which are treated specially by tools such as the `git`
-
//! CLI. For example:
-
//!
-
//! * `refs/heads/..` are also called "branches".
-
//!
-
//!   Omitting the "refs/heads/" prefix is typically accepted. Such a branch
-
//!   name is also referred to as a "shorthand" ref.
-
//!
-
//! * `refs/tags/..` are assumed to contain tags.
-
//!
-
//!   `git` treats tags specially, specifically it insists that they be globally
-
//!   unique across all   copies of the repository.
-
//!
-
//! * `refs/remotes/../..` is where "remote tracking branches" are stored.
-
//!
-
//!   In `git`, the first element after "remotes" is considered the name of the
-
//!   [remote][git-remote] (as it appears in the config file), while everything
-
//!   after that is considered a shorthand branch. Note, however, that the
-
//!   remote name may itself contain '/' separators, so it is not generally
-
//!   possible to extract  the branch name without access to the config.
-
//!
-
//! * `refs/namespaces/..` is hidden unless [`gitnamespaces`] are in effect.
-
//!
-
//!   The structure of namespaces is recursive: they contain full refs, which
-
//!   can themselves be namespaces (eg.
-
//!   `refs/namespaces/a/refs/namespaces/b/refs/heads/branch`). Note that,
-
//!   unlike remote names,  namespace names can **not** contain forward slashes
-
//!   but there is no tooling which would enforce that.
-
//!
-
//! There are also other such ref hierachies `git` knows about, and this crate
-
//! doesn't attempt to cover all of them. More importantly, `git` does not
-
//! impose any restrictions on ref hierarchies: as long as they don't collide
-
//! with convential ones, applications can introduce any hierchies they want.
-
//!
-
//! This restricts the transformations between conventional refs which can be
-
//! made without additional information besides the ref name: for example, it is
-
//! not generally possible to turn a remote tracking branch into a branch (or a
-
//! shorthand) without knowning about all possible remote names.
-
//!
-
//! Therefore, this crate doesn't attempt to interpret all possible semantics
-
//! associated with refs, and instead tries to make it easy for library
-
//! consumers to do so.
-
//!
-
//! [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
-
//! [`git-fetch`]: https://git-scm.com/docs/git-fetch
-
//! [git-remote]: https://git-scm.com/docs/git-remote
-
//! [`gitnamespaces`]: https://git-scm.com/docs/gitnamespaces
-
#[cfg(feature = "percent-encoding")]
-
pub use git_ref_format_core::PercentEncode;
-
pub use git_ref_format_core::{
-
    check_ref_format,
-
    lit,
-
    name::component,
-
    Component,
-
    DuplicateGlob,
-
    Error,
-
    Namespaced,
-
    Options,
-
    Qualified,
-
    RefStr,
-
    RefString,
-
};
-

-
pub mod name {
-
    pub use git_ref_format_core::name::*;
-

-
    #[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
-
    pub use git_ref_format_macro::component;
-
}
-

-
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
-
pub use git_ref_format_macro::qualified;
-
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
-
pub use git_ref_format_macro::refname;
-

-
pub mod refspec {
-
    pub use git_ref_format_core::refspec::*;
-

-
    #[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
-
    pub use git_ref_format_macro::pattern;
-
}
deleted git-ref-format/t/Cargo.toml
@@ -1,29 +0,0 @@
-
[package]
-
name = "git-ref-format-test"
-
version = "0.1.0"
-
edition = "2021"
-
license = "GPL-3.0-or-later"
-

-
publish = false
-

-
[lib]
-
doctest = false
-
test = true
-
doc = false
-

-
[features]
-
test = []
-

-
[dependencies]
-
proptest = "1"
-

-
[dev-dependencies]
-
assert_matches = "1.5"
-
serde_json = "1"
-

-
[dev-dependencies.git-ref-format]
-
path = ".."
-
features = ["macro", "minicbor", "serde"]
-

-
[dev-dependencies.test-helpers]
-
path = "../../test/test-helpers"
deleted git-ref-format/t/src/gen.rs
@@ -1,101 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
use proptest::prelude::*;
-

-
/// Any unicode "word" is trivially a valid refname.
-
pub fn trivial() -> impl Strategy<Value = String> {
-
    "\\w+"
-
}
-

-
pub fn valid() -> impl Strategy<Value = String> {
-
    prop::collection::vec(trivial(), 1..20).prop_map(|xs| xs.join("/"))
-
}
-

-
pub fn invalid_char() -> impl Strategy<Value = char> {
-
    prop_oneof![
-
        Just('\0'),
-
        Just('\\'),
-
        Just('~'),
-
        Just('^'),
-
        Just(':'),
-
        Just('?'),
-
        Just('[')
-
    ]
-
}
-

-
pub fn with_invalid_char() -> impl Strategy<Value = String> {
-
    ("\\w*", invalid_char(), "\\w*").prop_map(|(mut pre, invalid, suf)| {
-
        pre.push(invalid);
-
        pre.push_str(&suf);
-
        pre
-
    })
-
}
-

-
pub fn ends_with_dot_lock() -> impl Strategy<Value = String> {
-
    "\\w*\\.lock"
-
}
-

-
pub fn with_double_dot() -> impl Strategy<Value = String> {
-
    "\\w*\\.\\.\\w*"
-
}
-

-
pub fn starts_with_dot() -> impl Strategy<Value = String> {
-
    "\\.\\w*"
-
}
-

-
pub fn ends_with_dot() -> impl Strategy<Value = String> {
-
    "\\w+\\."
-
}
-

-
pub fn with_control_char() -> impl Strategy<Value = String> {
-
    "\\w*[\x01-\x1F\x7F]+\\w*"
-
}
-

-
pub fn with_space() -> impl Strategy<Value = String> {
-
    "\\w* +\\w*"
-
}
-

-
pub fn with_consecutive_slashes() -> impl Strategy<Value = String> {
-
    "\\w*//\\w*"
-
}
-

-
pub fn with_glob() -> impl Strategy<Value = String> {
-
    "\\w*\\*\\w*"
-
}
-

-
pub fn multi_glob() -> impl Strategy<Value = String> {
-
    (
-
        prop::collection::vec(with_glob(), 2..5),
-
        prop::collection::vec(trivial(), 0..5),
-
    )
-
        .prop_map(|(mut globs, mut valids)| {
-
            globs.append(&mut valids);
-
            globs
-
        })
-
        .prop_shuffle()
-
        .prop_map(|xs| xs.join("/"))
-
}
-

-
pub fn invalid() -> impl Strategy<Value = String> {
-
    fn path(s: impl Strategy<Value = String>) -> impl Strategy<Value = String> {
-
        prop::collection::vec(s, 1..20).prop_map(|xs| xs.join("/"))
-
    }
-

-
    prop_oneof![
-
        Just(String::from("")),
-
        Just(String::from("@")),
-
        path(with_invalid_char()),
-
        path(ends_with_dot_lock()),
-
        path(with_double_dot()),
-
        path(starts_with_dot()),
-
        path(ends_with_dot()),
-
        path(with_control_char()),
-
        path(with_space()),
-
        path(with_consecutive_slashes()),
-
        path(trivial()).prop_map(|mut p| {
-
            p.push('/');
-
            p
-
        }),
-
    ]
-
}
deleted git-ref-format/t/src/lib.rs
@@ -1,13 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
#[cfg(test)]
-
#[macro_use]
-
extern crate assert_matches;
-

-
#[cfg(any(test, feature = "test"))]
-
pub mod gen;
-
#[cfg(test)]
-
mod properties;
-
#[cfg(test)]
-
mod tests;
deleted git-ref-format/t/src/properties.rs
@@ -1,25 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
use git_ref_format::{check_ref_format, Error, Options};
-
use proptest::prelude::*;
-

-
use crate::gen;
-

-
mod name;
-
mod pattern;
-

-
proptest! {
-
    #[test]
-
    fn disallow_onelevel(input in gen::trivial(), allow_pattern in any::<bool>()) {
-
        assert_matches!(
-
            check_ref_format(Options {
-
                    allow_onelevel: false,
-
                    allow_pattern,
-
                },
-
                &input
-
            ),
-
            Err(Error::OneLevel)
-
        )
-
    }
-
}
deleted git-ref-format/t/src/properties/name.rs
@@ -1,100 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
use std::convert::TryFrom;
-

-
use git_ref_format::{name, refname, Error, RefStr, RefString};
-
use proptest::prelude::*;
-
use test_helpers::roundtrip;
-

-
use crate::gen;
-

-
proptest! {
-
    #[test]
-
    fn valid(input in gen::valid()) {
-
        assert_eq!(input.as_str(), RefStr::try_from_str(&input).unwrap().as_str())
-
    }
-

-
    #[test]
-
    fn invalid_char(input in gen::with_invalid_char()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::InvalidChar(_)))
-
    }
-

-
    #[test]
-
    fn dot_lock(input in gen::ends_with_dot_lock()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::DotLock))
-
    }
-

-
    #[test]
-
    fn double_dot(input in gen::with_double_dot()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::DotDot))
-
    }
-

-
    #[test]
-
    fn starts_dot(input in gen::starts_with_dot()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::StartsDot))
-
    }
-

-
    #[test]
-
    fn ends_dot(input in gen::ends_with_dot()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::EndsDot))
-
    }
-

-
    #[test]
-
    fn control_char(input in gen::with_control_char()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::Control))
-
    }
-

-
    #[test]
-
    fn space(input in gen::with_space()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::Space))
-
    }
-

-
    #[test]
-
    fn consecutive_slashes(input in gen::with_consecutive_slashes()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::Slash))
-
    }
-

-
    #[test]
-
    fn glob(input in gen::with_glob()) {
-
        assert_matches!(RefString::try_from(input), Err(Error::InvalidChar('*')))
-
    }
-

-
    #[test]
-
    fn invalid(input in gen::invalid()) {
-
        assert_matches!(RefString::try_from(input), Err(_))
-
    }
-

-
    #[test]
-
    fn roundtrip_components(input in gen::valid()) {
-
        assert_eq!(
-
            input.as_str(),
-
            RefStr::try_from_str(&input).unwrap().components().collect::<RefString>().as_str()
-
        )
-
    }
-

-
    #[test]
-
    fn json(input in gen::valid()) {
-
        let input = RefString::try_from(input).unwrap();
-
        roundtrip::json(input.clone());
-
        let qualified = refname!("refs/heads").and(input).qualified().unwrap().into_owned();
-
        roundtrip::json(qualified.clone());
-
        let namespaced = qualified.with_namespace(name::component!("foo"));
-
        roundtrip::json(namespaced);
-
    }
-

-
    #[test]
-
    fn json_value(input in gen::valid()) {
-
        let input = RefString::try_from(input).unwrap();
-
        roundtrip::json_value(input.clone());
-
        let qualified = refname!("refs/heads").and(input).qualified().unwrap().into_owned();
-
        roundtrip::json_value(qualified.clone());
-
        let namespaced = qualified.with_namespace(name::component!("foo"));
-
        roundtrip::json_value(namespaced);
-
    }
-

-
    #[test]
-
    fn cbor(input in gen::valid()) {
-
        roundtrip::cbor(RefString::try_from(input).unwrap())
-
    }
-
}
deleted git-ref-format/t/src/properties/pattern.rs
@@ -1,60 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
use std::convert::TryFrom;
-

-
use git_ref_format::{refspec, Error};
-
use proptest::prelude::*;
-
use test_helpers::roundtrip;
-

-
use crate::gen;
-

-
proptest! {
-
    #[test]
-
    fn valid(input in gen::with_glob()) {
-
        assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
-
    }
-

-
    #[test]
-
    fn refname_is_pattern(input in gen::valid()) {
-
        assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
-
    }
-

-
    #[test]
-
    fn no_more_than_one_star(input in gen::multi_glob()) {
-
        assert_matches!(refspec::PatternString::try_from(input), Err(Error::Pattern))
-
    }
-

-
    #[test]
-
    fn invalid_refname_is_invalid_pattern(input in gen::invalid()) {
-
        assert_matches!(refspec::PatternString::try_from(input), Err(_))
-
    }
-

-
    #[test]
-
    fn roundtrip_components(input in gen::with_glob()) {
-
        assert_eq!(
-
            input.as_str(),
-
            refspec::PatternStr::try_from_str(&input)
-
                .unwrap()
-
                .components()
-
                .collect::<Result<refspec::PatternString, _>>()
-
                .unwrap()
-
                .as_str()
-
        )
-
    }
-

-
    #[test]
-
    fn json(input in gen::with_glob()) {
-
        roundtrip::json(refspec::PatternString::try_from(input).unwrap())
-
    }
-

-
    #[test]
-
    fn json_value(input in gen::with_glob()) {
-
        roundtrip::json_value(refspec::PatternString::try_from(input).unwrap())
-
    }
-

-
    #[test]
-
    fn cbor(input in gen::with_glob()) {
-
        roundtrip::cbor(refspec::PatternString::try_from(input).unwrap())
-
    }
-
}
deleted git-ref-format/t/src/tests.rs
@@ -1,372 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
use git_ref_format::{
-
    component,
-
    name,
-
    qualified,
-
    refname,
-
    refspec,
-
    Error,
-
    Qualified,
-
    RefStr,
-
    RefString,
-
};
-

-
#[test]
-
fn refname_macro_works() {
-
    assert_eq!("refs/heads/main", refname!("refs/heads/main").as_str())
-
}
-

-
#[test]
-
fn qualified_macro_works() {
-
    assert_eq!("refs/heads/main", qualified!("refs/heads/main").as_str())
-
}
-

-
#[test]
-
fn component_macro_works() {
-
    assert_eq!("self", name::component!("self").as_str())
-
}
-

-
#[test]
-
fn pattern_macro_works() {
-
    assert_eq!("refs/heads/*", refspec::pattern!("refs/heads/*").as_str())
-
}
-

-
#[test]
-
fn empty() {
-
    assert_matches!(RefStr::try_from_str(""), Err(Error::Empty));
-
    assert_matches!(RefString::try_from("".to_owned()), Err(Error::Empty));
-
}
-

-
#[test]
-
fn join() {
-
    let s = name::REFS.join(name::HEADS);
-
    let t = s.join(name::MAIN);
-
    assert_eq!("refs/heads", s.as_str());
-
    assert_eq!("refs/heads/main", t.as_str());
-
}
-

-
#[test]
-
fn join_and() {
-
    assert_eq!(
-
        "refs/heads/this/that",
-
        name::REFS
-
            .join(name::HEADS)
-
            .and(refname!("this"))
-
            .and(refname!("that"))
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn strip_prefix() {
-
    assert_eq!(
-
        "main",
-
        name::REFS_HEADS_MAIN
-
            .strip_prefix(refname!("refs/heads"))
-
            .unwrap()
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn strip_prefix_not_prefix() {
-
    assert!(name::REFS_HEADS_MAIN
-
        .strip_prefix(refname!("refs/tags"))
-
        .is_none())
-
}
-

-
#[test]
-
fn qualified() {
-
    assert_eq!(
-
        "refs/heads/main",
-
        name::REFS_HEADS_MAIN.qualified().unwrap().as_str()
-
    )
-
}
-

-
#[test]
-
fn qualified_tag() {
-
    assert_eq!(
-
        "refs/tags/v1",
-
        refname!("refs/tags/v1").qualified().unwrap().as_str()
-
    )
-
}
-

-
#[test]
-
fn qualified_remote_tracking() {
-
    assert_eq!(
-
        "refs/remotes/origin/master",
-
        refname!("refs/remotes/origin/master")
-
            .qualified()
-
            .unwrap()
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn not_qualified() {
-
    assert!(name::MAIN.qualified().is_none())
-
}
-

-
#[test]
-
fn qualified_from_components() {
-
    assert_eq!(
-
        "refs/heads/main",
-
        Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
-
    )
-
}
-

-
#[test]
-
fn qualified_from_components_with_iter() {
-
    assert_eq!(
-
        "refs/heads/foo/bar/baz",
-
        Qualified::from_components(
-
            component::HEADS,
-
            name::component!("foo"),
-
            [name::component!("bar"), name::component!("baz")]
-
        )
-
        .as_str()
-
    )
-
}
-

-
#[test]
-
fn qualified_from_components_non_empty_iter() {
-
    let q = Qualified::from_components(component::HEADS, component::MAIN, None);
-
    let (refs, heads, main, mut empty) = q.non_empty_iter();
-
    assert!(empty.next().is_none());
-
    assert_eq!(("refs", "heads", "main"), (refs, heads, main))
-
}
-

-
#[test]
-
fn qualified_from_components_non_empty_components() {
-
    let q = Qualified::from_components(component::HEADS, component::MAIN, Some(component::MASTER));
-
    let (refs, heads, main, mut master) = q.non_empty_components();
-
    assert_eq!(
-
        (
-
            component::REFS,
-
            component::HEADS,
-
            component::MAIN,
-
            component::MASTER
-
        ),
-
        (refs, heads, main, master.next().unwrap())
-
    )
-
}
-

-
#[test]
-
fn namespaced() {
-
    assert_eq!(
-
        "refs/namespaces/foo/refs/heads/main",
-
        refname!("refs/namespaces/foo/refs/heads/main")
-
            .to_namespaced()
-
            .unwrap()
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn not_namespaced() {
-
    assert!(name::REFS_HEADS_MAIN.to_namespaced().is_none())
-
}
-

-
#[test]
-
fn not_namespaced_because_not_qualified() {
-
    assert!(refname!("refs/namespaces/foo/banana")
-
        .to_namespaced()
-
        .is_none())
-
}
-

-
#[test]
-
fn strip_namespace() {
-
    assert_eq!(
-
        "refs/rad/id",
-
        refname!("refs/namespaces/xyz/refs/rad/id")
-
            .to_namespaced()
-
            .unwrap()
-
            .strip_namespace()
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn strip_nested_namespaces() {
-
    let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
-
    let namespaced = full.to_namespaced().unwrap();
-
    let strip_first = namespaced.strip_namespace();
-
    let nested = strip_first.to_namespaced().unwrap();
-
    let strip_second = nested.strip_namespace();
-

-
    assert_eq!("a", namespaced.namespace().as_str());
-
    assert_eq!("b", nested.namespace().as_str());
-
    assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
-
    assert_eq!("refs/heads/main", strip_second.as_str());
-
}
-

-
#[test]
-
fn with_namespace() {
-
    assert_eq!(
-
        "refs/namespaces/foo/refs/heads/main",
-
        name::REFS_HEADS_MAIN
-
            .qualified()
-
            .unwrap()
-
            .with_namespace(refname!("foo").head())
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn iter() {
-
    assert_eq!(
-
        vec!["refs", "heads", "main"],
-
        name::REFS_HEADS_MAIN.iter().collect::<Vec<_>>()
-
    )
-
}
-

-
#[test]
-
fn push_pop() {
-
    let mut s = name::REFS.to_owned();
-
    s.push(name::HEADS);
-
    s.push(name::MAIN);
-

-
    assert_eq!("refs/heads/main", s.as_str());
-
    assert!(s.pop());
-
    assert!(s.pop());
-
    assert_eq!("refs", s.as_str());
-
    assert!(!s.pop());
-
    assert_eq!("refs", s.as_str());
-
}
-

-
#[test]
-
fn to_pattern() {
-
    assert_eq!(
-
        "refs/heads/*",
-
        refname!("refs/heads")
-
            .to_pattern(refspec::pattern!("*"))
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn with_pattern() {
-
    assert_eq!(
-
        "refs/heads/*",
-
        refname!("refs/heads").with_pattern(refspec::STAR).as_str()
-
    )
-
}
-

-
#[test]
-
fn with_pattern_and() {
-
    assert_eq!(
-
        "refs/*/heads",
-
        refname!("refs")
-
            .with_pattern(refspec::STAR)
-
            .and(name::HEADS)
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn pattern_is_qualified() {
-
    assert!(refspec::pattern!("refs/heads/*").qualified().is_some())
-
}
-

-
#[test]
-
fn pattern_is_not_qualified() {
-
    assert!(refspec::pattern!("heads/*").qualified().is_none())
-
}
-

-
#[test]
-
fn pattern_with_namespaced() {
-
    assert_eq!(
-
        "refs/namespaces/a/refs/heads/*",
-
        refspec::pattern!("refs/heads/*")
-
            .qualified()
-
            .unwrap()
-
            .with_namespace(refspec::Component::Normal(refname!("a").as_ref()))
-
            .unwrap()
-
            .as_str(),
-
    )
-
}
-

-
#[test]
-
fn pattern_not_namespaced_because_not_qualified() {
-
    assert!(refspec::pattern!("refs/namespaces/foo/*")
-
        .to_namespaced()
-
        .is_none())
-
}
-

-
#[test]
-
fn pattern_namespaced() {
-
    assert!(refspec::pattern!("refs/namespaces/a/refs/foo/*")
-
        .to_namespaced()
-
        .is_some())
-
}
-

-
#[test]
-
fn pattern_strip_namespace() {
-
    assert_eq!(
-
        "refs/rad/*",
-
        refspec::pattern!("refs/namespaces/xyz/refs/rad/*")
-
            .to_namespaced()
-
            .unwrap()
-
            .strip_namespace()
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn pattern_strip_nested_namespaces() {
-
    let full = refspec::pattern!("refs/namespaces/a/refs/namespaces/b/refs/heads/*");
-
    let namespaced = full.to_namespaced().unwrap();
-
    let strip_first = namespaced.strip_namespace();
-
    let nested = strip_first.to_namespaced().unwrap();
-
    let strip_second = nested.strip_namespace();
-

-
    assert_eq!("a", namespaced.namespace().as_str());
-
    assert_eq!("b", nested.namespace().as_str());
-
    assert_eq!("refs/namespaces/b/refs/heads/*", strip_first.as_str());
-
    assert_eq!("refs/heads/*", strip_second.as_str());
-
}
-

-
#[test]
-
fn pattern_qualified_with_namespace() {
-
    assert_eq!(
-
        "refs/namespaces/a/refs/heads/*",
-
        refspec::pattern!("refs/heads/*")
-
            .qualified()
-
            .unwrap()
-
            .with_namespace(refspec::Component::Normal(refname!("a").as_ref()))
-
            .unwrap()
-
            .as_str(),
-
    )
-
}
-

-
#[test]
-
fn collect() {
-
    assert_eq!(
-
        "refs/heads/main",
-
        IntoIterator::into_iter([name::REFS, name::HEADS, name::MAIN])
-
            .collect::<RefString>()
-
            .as_str()
-
    )
-
}
-

-
#[test]
-
fn collect_components() {
-
    let a = name::REFS_HEADS_MAIN.to_owned();
-
    let b = a.components().collect();
-
    assert_eq!(a, b)
-
}
-

-
#[test]
-
fn collect_pattern_duplicate_glob() {
-
    assert_matches!(
-
        IntoIterator::into_iter([
-
            refspec::Component::Normal(name::REFS),
-
            refspec::Component::Glob(None),
-
            refspec::Component::Glob(Some(refspec::pattern!("fo*").as_ref()))
-
        ])
-
        .collect::<Result<_, _>>(),
-
        Err(refspec::DuplicateGlob)
-
    )
-
}
modified git-storage/Cargo.toml
@@ -17,9 +17,6 @@ version = "0.16.1"
default-features = false
features = ["vendored-libgit2"]

-
[dependencies.git-ref-format]
-
path = "../git-ref-format"
-

[dependencies.libgit2-sys]
version = ">= 0.14.2"
default-features = false
modified git-storage/src/backend/read.rs
@@ -5,7 +5,7 @@

use std::{fmt, path::Path};

-
use git_ext::{error::is_not_found_err, Oid};
+
use git_ext::{error::is_not_found_err, ref_format, Oid};
use std_ext::result::ResultExt as _;

use crate::{
@@ -89,7 +89,7 @@ impl<'a> refdb::Read for &'a Read {

    fn find_reference<Ref>(&self, reference: Ref) -> Result<Option<Reference>, Self::FindRef>
    where
-
        Ref: AsRef<git_ref_format::RefStr>,
+
        Ref: AsRef<ref_format::RefStr>,
    {
        let reference = self
            .raw
@@ -101,7 +101,7 @@ impl<'a> refdb::Read for &'a Read {

    fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
    where
-
        Pat: AsRef<git_ref_format::refspec::PatternStr>,
+
        Pat: AsRef<ref_format::refspec::PatternStr>,
    {
        Ok(References {
            inner: ReferencesGlob {
@@ -113,7 +113,7 @@ impl<'a> refdb::Read for &'a Read {

    fn find_reference_oid<Ref>(&self, reference: Ref) -> Result<Option<Oid>, Self::FindRefOid>
    where
-
        Ref: AsRef<git_ref_format::RefStr>,
+
        Ref: AsRef<ref_format::RefStr>,
    {
        self.raw
            .refname_to_id(reference.as_ref().as_str())
modified git-storage/src/backend/write.rs
@@ -7,8 +7,11 @@ use std::path::Path;

use either::Either;

-
use git_ext::{error::is_not_found_err, Oid};
-
use git_ref_format::{Qualified, RefStr, RefString};
+
use git_ext::{
+
    error::is_not_found_err,
+
    ref_format::{self, Qualified, RefStr, RefString},
+
    Oid,
+
};

use crate::{
    odb,
@@ -103,21 +106,21 @@ impl<'a> refdb::Read for &'a Write {

    fn find_reference<Ref>(&self, reference: Ref) -> Result<Option<Reference>, Self::FindRef>
    where
-
        Ref: AsRef<git_ref_format::RefStr>,
+
        Ref: AsRef<ref_format::RefStr>,
    {
        self.read_only().find_reference(reference)
    }

    fn find_references<Pat>(&self, reference: Pat) -> Result<Self::References, Self::FindRefs>
    where
-
        Pat: AsRef<git_ref_format::refspec::PatternStr>,
+
        Pat: AsRef<ref_format::refspec::PatternStr>,
    {
        self.read_only().find_references(reference)
    }

    fn find_reference_oid<Ref>(&self, reference: Ref) -> Result<Option<Oid>, Self::FindRefOid>
    where
-
        Ref: AsRef<git_ref_format::RefStr>,
+
        Ref: AsRef<ref_format::RefStr>,
    {
        self.read_only().find_reference_oid(reference)
    }
modified git-storage/src/glob.rs
@@ -5,7 +5,7 @@

use std::path::Path;

-
use git_ref_format::refspec::PatternString;
+
use git_ext::ref_format::refspec::PatternString;

pub trait Pattern {
    fn matches<P: AsRef<Path>>(&self, path: P) -> bool;
modified git-storage/src/odb/write.rs
@@ -3,8 +3,7 @@

use std::error::Error;

-
use git_ext::Oid;
-
use git_ref_format::RefStr;
+
use git_ext::{ref_format::RefStr, Oid};

use super::{Commit, Object, Read, Tree, TreeBuilder};

modified git-storage/src/refdb.rs
@@ -19,8 +19,7 @@

use std::fmt::Debug;

-
use git_ext::Oid;
-
use git_ref_format::RefString;
+
use git_ext::{ref_format::RefString, Oid};

pub mod iter;
pub use iter::{References, ReferencesGlob};
@@ -104,7 +103,7 @@ pub mod error {
        #[error("reference name did contain valid UTF-8 bytes")]
        InvalidUtf8,
        #[error(transparent)]
-
        RefString(#[from] git_ref_format::Error),
+
        RefString(#[from] radicle_git_ext::ref_format::Error),
    }

    #[derive(Debug, Error)]
modified git-storage/src/refdb/read.rs
@@ -3,8 +3,10 @@

use std::error::Error;

-
use git_ext::Oid;
-
use git_ref_format::{refspec, RefStr};
+
use git_ext::{
+
    ref_format::{refspec, RefStr},
+
    Oid,
+
};

use super::Reference;

modified git-storage/src/refdb/write.rs
@@ -3,8 +3,10 @@

use std::error::Error;

-
use git_ext::Oid;
-
use git_ref_format::{Qualified, RefString};
+
use git_ext::{
+
    ref_format::{Qualified, RefString},
+
    Oid,
+
};

use super::read::Read;

modified git-storage/t/Cargo.toml
@@ -38,10 +38,3 @@ features = ["v4"]
[dev-dependencies.radicle-git-ext-test]
path = "../../radicle-git-ext/t"
features = ["test"]
-

-
[dev-dependencies.git-ref-format]
-
path = "../../git-ref-format"
-

-
[dev-dependencies.git-ref-format-test]
-
path = "../../git-ref-format/t"
-
features = ["test"]
modified git-storage/t/src/properties/odb.rs
@@ -3,12 +3,12 @@

use git2::ObjectType;

-
use git_ref_format::RefString;
-
use git_ref_format_test::gen::valid;
use git_storage::{
    odb::{Read as _, Write as _},
    signature::UserInfo,
};
+
use radicle_git_ext::ref_format::RefString;
+
use radicle_git_ext_test::git_ref_format::gen::valid;

use proptest::prelude::*;

modified radicle-git-ext/Cargo.toml
@@ -25,7 +25,8 @@ features = ["vendored-libgit2"]

[dependencies.git-ref-format]
version = "0.2.0"
-
path = "../git-ref-format"
+
path = "./git-ref-format"
+
features = ["macro", "serde", "minicbor"]

[dependencies.serde]
version = "1"
added radicle-git-ext/git-ref-format/Cargo.toml
@@ -0,0 +1,31 @@
+
[package]
+
name = "git-ref-format"
+
version = "0.2.0"
+
authors = [
+
  "Kim Altintop <kim@eagain.st>",
+
  "Fintan Halpenny <fintan.halpenny@gmail.com>",
+
]
+
edition = "2021"
+
license = "GPL-3.0-or-later"
+
description = "Everything you never knew you wanted for handling git ref names."
+
keywords = ["git", "references"]
+

+
[lib]
+
doctest = false
+
test = false
+

+
[features]
+
bstr = ["git-ref-format-core/bstr"]
+
macro = ["git-ref-format-macro"]
+
minicbor = ["git-ref-format-core/minicbor"]
+
percent-encoding = ["git-ref-format-core/percent-encoding"]
+
serde = ["git-ref-format-core/serde"]
+

+
[dependencies.git-ref-format-core]
+
version = "0.2.0"
+
path = "./core"
+

+
[dependencies.git-ref-format-macro]
+
version = "0.2.0"
+
path = "./macro"
+
optional = true
added radicle-git-ext/git-ref-format/core/CHANGELOG.md
@@ -0,0 +1,11 @@
+
# CHANGELOG
+

+
## Version 0.2.0
+

+
* 2022-12-16 Update to rust-1.66 [8d451e3](https://github.com/radicle-dev/radicle-git/commit/8d451e3d9d6c0121b0ed3e4ac6cbf710625e9f9d)
+
* 2022-12-08 Fix to deserialization [db7f2d4](https://github.com/radicle-dev/radicle-git/commit/db7f2d40e32ef0fac7425054c4c49e22cb8bc70f)
+
* 2022-11-18 Remove link-literals feature [d45d63b](https://github.com/radicle-dev/radicle-git/commit/d45d63b7ceb1d03b80f6c47749b4b3e3ebb99e70)
+
* 2022-11-16 Add well-known refspec components [2b5a34a](https://github.com/radicle-dev/radicle-git/commit/2b5a34a703c182ff48d6c83ff3fa4256b4f37624)
+
* 2022-11-15 DoubleEndedIterator for Components [cf1594c](https://github.com/radicle-dev/radicle-git/commit/cf1594cf43e23c9fc8f74cf69e22f81227e82f2b)
+
* 2022-11-11 Add QualifiedPattern and NamespacedPattern [6479ba0](https://github.com/radicle-dev/radicle-git/commit/6479ba091fa3ee3b16fba61138cfd8c6ec6d985d)
+
* 2022-11-16 Component::from_refstr  [d6c7bb2](https://github.com/radicle-dev/radicle-git/commit/d6c7bb29abc0f0050e2efe8de47397eaa4564f6f)
added radicle-git-ext/git-ref-format/core/Cargo.toml
@@ -0,0 +1,33 @@
+
[package]
+
name = "git-ref-format-core"
+
version = "0.2.0"
+
authors = ["Kim Altintop <kim@eagain.st>"]
+
edition = "2018"
+
license = "GPL-3.0-or-later"
+
description = "Core types for the git-ref-format crate"
+
keywords = ["git", "references"]
+

+
[lib]
+
doctest = false
+
test = false
+

+
[dependencies]
+
thiserror = "1.0"
+

+
[dependencies.bstr]
+
version = "0.2"
+
optional = true
+

+
[dependencies.minicbor]
+
version = "0.13"
+
features = ["std"]
+
optional = true
+

+
[dependencies.percent-encoding]
+
version = "2.1.0"
+
optional = true
+

+
[dependencies.serde]
+
version = "1.0"
+
features = ["derive"]
+
optional = true
added radicle-git-ext/git-ref-format/core/src/cbor.rs
@@ -0,0 +1,117 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::convert::TryFrom;
+

+
use minicbor::{
+
    decode,
+
    encode::{self, Write},
+
    Decode,
+
    Decoder,
+
    Encode,
+
    Encoder,
+
};
+

+
use crate::{
+
    refspec::{PatternStr, PatternString},
+
    Namespaced,
+
    Qualified,
+
    RefStr,
+
    RefString,
+
};
+

+
impl<'de: 'a, 'a> Decode<'de> for &'a RefStr {
+
    #[inline]
+
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
+
        d.str()
+
            .and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
+
    }
+
}
+

+
impl<'a> Encode for &'a RefStr {
+
    #[inline]
+
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
        e.str(self.as_str())?;
+
        Ok(())
+
    }
+
}
+

+
impl<'de> Decode<'de> for RefString {
+
    #[inline]
+
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
+
        Decode::decode(d).map(|s: &RefStr| s.to_owned())
+
    }
+
}
+

+
impl Encode for RefString {
+
    #[inline]
+
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
        self.as_refstr().encode(e)
+
    }
+
}
+

+
impl<'de: 'a, 'a> Decode<'de> for &'a PatternStr {
+
    #[inline]
+
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
+
        d.str()
+
            .and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
+
    }
+
}
+

+
impl<'a> Encode for &'a PatternStr {
+
    #[inline]
+
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
        e.str(self.as_str())?;
+
        Ok(())
+
    }
+
}
+

+
impl<'de> Decode<'de> for PatternString {
+
    #[inline]
+
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
+
        Decode::decode(d).map(|s: &PatternStr| s.to_owned())
+
    }
+
}
+

+
impl Encode for PatternString {
+
    #[inline]
+
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
        self.as_pattern_str().encode(e)
+
    }
+
}
+

+
impl<'de: 'a, 'a> Decode<'de> for Qualified<'a> {
+
    #[inline]
+
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
+
        Decode::decode(d).and_then(|s: &RefStr| {
+
            s.qualified()
+
                .ok_or(decode::Error::Message("not a qualified ref"))
+
        })
+
    }
+
}
+

+
impl<'a> Encode for Qualified<'a> {
+
    #[inline]
+
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
        self.as_str().encode(e)
+
    }
+
}
+

+
impl<'de: 'a, 'a> Decode<'de> for Namespaced<'a> {
+
    #[inline]
+
    fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
+
        Decode::decode(d).and_then(|s: &RefStr| {
+
            s.to_namespaced()
+
                .ok_or(decode::Error::Message("not a namespaced ref"))
+
        })
+
    }
+
}
+

+
impl<'a> Encode for Namespaced<'a> {
+
    #[inline]
+
    fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
        self.as_str().encode(e)
+
    }
+
}
added radicle-git-ext/git-ref-format/core/src/check.rs
@@ -0,0 +1,106 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use thiserror::Error;
+

+
pub struct Options {
+
    /// If `false`, the refname must contain at least one `/`.
+
    pub allow_onelevel: bool,
+
    /// If `true`, the refname may contain exactly one `*` character.
+
    pub allow_pattern: bool,
+
}
+

+
#[derive(Debug, PartialEq, Eq, Error)]
+
#[non_exhaustive]
+
pub enum Error {
+
    #[error("empty input")]
+
    Empty,
+
    #[error("lone '@' character")]
+
    LoneAt,
+
    #[error("consecutive or trailing slash")]
+
    Slash,
+
    #[error("ends with '.lock'")]
+
    DotLock,
+
    #[error("consecutive dots ('..')")]
+
    DotDot,
+
    #[error("at-open-brace ('@{{')")]
+
    AtOpenBrace,
+
    #[error("invalid character {0:?}")]
+
    InvalidChar(char),
+
    #[error("component starts with '.'")]
+
    StartsDot,
+
    #[error("component ends with '.'")]
+
    EndsDot,
+
    #[error("control character")]
+
    Control,
+
    #[error("whitespace")]
+
    Space,
+
    #[error("must contain at most one '*'")]
+
    Pattern,
+
    #[error("must contain at least one '/'")]
+
    OneLevel,
+
}
+

+
/// Validate that a string slice is a valid refname.
+
pub fn ref_format(opts: Options, s: &str) -> Result<(), Error> {
+
    match s {
+
        "" => Err(Error::Empty),
+
        "@" => Err(Error::LoneAt),
+
        "." => Err(Error::StartsDot),
+
        _ => {
+
            let mut globs = 0usize;
+
            let mut parts = 0usize;
+

+
            for x in s.split('/') {
+
                if x.is_empty() {
+
                    return Err(Error::Slash);
+
                }
+

+
                parts += 1;
+

+
                if x.ends_with(".lock") {
+
                    return Err(Error::DotLock);
+
                }
+

+
                let last_char = x.chars().count() - 1;
+
                for (i, y) in x.chars().zip(x.chars().cycle().skip(1)).enumerate() {
+
                    match y {
+
                        ('.', '.') => return Err(Error::DotDot),
+
                        ('@', '{') => return Err(Error::AtOpenBrace),
+

+
                        ('\0', _) => return Err(Error::InvalidChar('\0')),
+
                        ('\\', _) => return Err(Error::InvalidChar('\\')),
+
                        ('~', _) => return Err(Error::InvalidChar('~')),
+
                        ('^', _) => return Err(Error::InvalidChar('^')),
+
                        (':', _) => return Err(Error::InvalidChar(':')),
+
                        ('?', _) => return Err(Error::InvalidChar('?')),
+
                        ('[', _) => return Err(Error::InvalidChar('[')),
+

+
                        ('*', _) => globs += 1,
+

+
                        ('.', _) if i == 0 => return Err(Error::StartsDot),
+
                        ('.', _) if i == last_char => return Err(Error::EndsDot),
+

+
                        (' ', _) => return Err(Error::Space),
+

+
                        (z, _) if z.is_ascii_control() => return Err(Error::Control),
+

+
                        _ => continue,
+
                    }
+
                }
+
            }
+

+
            if parts < 2 && !opts.allow_onelevel {
+
                Err(Error::OneLevel)
+
            } else if globs > 1 && opts.allow_pattern {
+
                Err(Error::Pattern)
+
            } else if globs > 0 && !opts.allow_pattern {
+
                Err(Error::InvalidChar('*'))
+
            } else {
+
                Ok(())
+
            }
+
        },
+
    }
+
}
added radicle-git-ext/git-ref-format/core/src/deriv.rs
@@ -0,0 +1,397 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{
+
    borrow::Cow,
+
    fmt::{self, Display},
+
    ops::Deref,
+
};
+

+
use crate::{
+
    lit,
+
    name,
+
    refspec::{PatternStr, QualifiedPattern},
+
    Component,
+
    RefStr,
+
    RefString,
+
};
+

+
/// A fully-qualified refname.
+
///
+
/// A refname is qualified _iff_ it starts with "refs/" and has at least three
+
/// components. This implies that a [`Qualified`] ref has a category, such as
+
/// "refs/heads/main".
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Qualified<'a>(pub(crate) Cow<'a, RefStr>);
+

+
impl<'a> Qualified<'a> {
+
    /// Infallibly create a [`Qualified`] from components.
+
    ///
+
    /// Note that the "refs/" prefix is implicitly added, so `a` is the second
+
    /// [`Component`]. Mirroring [`Self::non_empty_components`], providing
+
    /// two [`Component`]s guarantees well-formedness of the [`Qualified`].
+
    /// `tail` may be empty.
+
    ///
+
    /// # Example
+
    ///
+
    /// ```no_run
+
    /// use git_ref_format::{component, Qualified};
+
    ///
+
    /// assert_eq!(
+
    ///     "refs/heads/main",
+
    ///     Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
+
    /// )
+
    /// ```
+
    pub fn from_components<'b, 'c, 'd, A, B, C>(a: A, b: B, tail: C) -> Self
+
    where
+
        A: Into<Component<'b>>,
+
        B: Into<Component<'c>>,
+
        C: IntoIterator<Item = Component<'d>>,
+
    {
+
        let mut inner = name::REFS.join(a.into()).and(b.into());
+
        inner.extend(tail);
+

+
        Self(inner.into())
+
    }
+

+
    pub fn from_refstr(r: impl Into<Cow<'a, RefStr>>) -> Option<Self> {
+
        Self::_from_refstr(r.into())
+
    }
+

+
    fn _from_refstr(r: Cow<'a, RefStr>) -> Option<Self> {
+
        let mut iter = r.iter();
+
        match (iter.next()?, iter.next()?, iter.next()?) {
+
            ("refs", _, _) => Some(Qualified(r)),
+
            _ => None,
+
        }
+
    }
+

+
    #[inline]
+
    pub fn as_str(&self) -> &str {
+
        self.as_ref()
+
    }
+

+
    #[inline]
+
    pub fn join<'b, R>(&self, other: R) -> Qualified<'b>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        Qualified(self.0.join(other).into())
+
    }
+

+
    pub fn to_pattern<P>(&self, pattern: P) -> QualifiedPattern
+
    where
+
        P: AsRef<PatternStr>,
+
    {
+
        QualifiedPattern(Cow::Owned(RefStr::to_pattern(self, pattern.as_ref())))
+
    }
+

+
    #[inline]
+
    pub fn to_namespaced(&self) -> Option<Namespaced> {
+
        self.0.as_ref().into()
+
    }
+

+
    /// Add a namespace.
+
    ///
+
    /// Creates a new [`Namespaced`] by prefxing `self` with
+
    /// `refs/namespaces/<ns>`.
+
    pub fn with_namespace<'b>(&self, ns: Component<'b>) -> Namespaced<'a> {
+
        Namespaced(Cow::Owned(
+
            IntoIterator::into_iter([lit::Refs.into(), lit::Namespaces.into(), ns])
+
                .chain(self.0.components())
+
                .collect(),
+
        ))
+
    }
+

+
    /// Like [`Self::non_empty_components`], but with string slices.
+
    pub fn non_empty_iter(&self) -> (&str, &str, &str, name::Iter) {
+
        let mut iter = self.iter();
+
        (
+
            iter.next().unwrap(),
+
            iter.next().unwrap(),
+
            iter.next().unwrap(),
+
            iter,
+
        )
+
    }
+

+
    /// Return the first three [`Component`]s, and a possibly empty iterator
+
    /// over the remaining ones.
+
    ///
+
    /// A qualified ref is guaranteed to have at least three components, which
+
    /// this method provides a witness of. This is useful eg. for pattern
+
    /// matching on the prefix.
+
    pub fn non_empty_components(&self) -> (Component, Component, Component, name::Components) {
+
        let mut cs = self.components();
+
        (
+
            cs.next().unwrap(),
+
            cs.next().unwrap(),
+
            cs.next().unwrap(),
+
            cs,
+
        )
+
    }
+

+
    #[inline]
+
    pub fn to_owned<'b>(&self) -> Qualified<'b> {
+
        Qualified(Cow::Owned(self.0.clone().into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_owned<'b>(self) -> Qualified<'b> {
+
        Qualified(Cow::Owned(self.0.into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_refstring(self) -> RefString {
+
        self.into()
+
    }
+
}
+

+
impl Deref for Qualified<'_> {
+
    type Target = RefStr;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<RefStr> for Qualified<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &RefStr {
+
        self
+
    }
+
}
+

+
impl AsRef<str> for Qualified<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self.0.as_str()
+
    }
+
}
+

+
impl AsRef<Self> for Qualified<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &Self {
+
        self
+
    }
+
}
+

+
impl<'a> From<Qualified<'a>> for Cow<'a, RefStr> {
+
    #[inline]
+
    fn from(q: Qualified<'a>) -> Self {
+
        q.0
+
    }
+
}
+

+
impl From<Qualified<'_>> for RefString {
+
    #[inline]
+
    fn from(q: Qualified) -> Self {
+
        q.0.into_owned()
+
    }
+
}
+

+
impl<T, U> From<(lit::Refs, T, U)> for Qualified<'_>
+
where
+
    T: AsRef<RefStr>,
+
    U: AsRef<RefStr>,
+
{
+
    #[inline]
+
    fn from((refs, cat, name): (lit::Refs, T, U)) -> Self {
+
        let refs: &RefStr = refs.into();
+
        Self(Cow::Owned(refs.join(cat).and(name)))
+
    }
+
}
+

+
impl<T> From<lit::RefsHeads<T>> for Qualified<'_>
+
where
+
    T: AsRef<RefStr>,
+
{
+
    #[inline]
+
    fn from((refs, heads, name): lit::RefsHeads<T>) -> Self {
+
        Self(Cow::Owned(
+
            IntoIterator::into_iter([Component::from(refs), heads.into()])
+
                .collect::<RefString>()
+
                .and(name),
+
        ))
+
    }
+
}
+

+
impl<T> From<lit::RefsTags<T>> for Qualified<'_>
+
where
+
    T: AsRef<RefStr>,
+
{
+
    #[inline]
+
    fn from((refs, tags, name): lit::RefsTags<T>) -> Self {
+
        Self(Cow::Owned(
+
            IntoIterator::into_iter([Component::from(refs), tags.into()])
+
                .collect::<RefString>()
+
                .and(name),
+
        ))
+
    }
+
}
+

+
impl<T> From<lit::RefsNotes<T>> for Qualified<'_>
+
where
+
    T: AsRef<RefStr>,
+
{
+
    #[inline]
+
    fn from((refs, notes, name): lit::RefsNotes<T>) -> Self {
+
        Self(Cow::Owned(
+
            IntoIterator::into_iter([Component::from(refs), notes.into()])
+
                .collect::<RefString>()
+
                .and(name),
+
        ))
+
    }
+
}
+

+
impl<T> From<lit::RefsRemotes<T>> for Qualified<'_>
+
where
+
    T: AsRef<RefStr>,
+
{
+
    #[inline]
+
    fn from((refs, remotes, name): lit::RefsRemotes<T>) -> Self {
+
        Self(Cow::Owned(
+
            IntoIterator::into_iter([Component::from(refs), remotes.into()])
+
                .collect::<RefString>()
+
                .and(name),
+
        ))
+
    }
+
}
+

+
impl Display for Qualified<'_> {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
+

+
/// A [`Qualified`] ref under a git namespace.
+
///
+
/// A ref is namespaced if it starts with "refs/namespaces/", another path
+
/// component, and "refs/". Eg.
+
///
+
///     refs/namespaces/xyz/refs/heads/main
+
///
+
/// Note that namespaces can be nested, so the result of
+
/// [`Namespaced::strip_namespace`] may be convertible to a [`Namespaced`]
+
/// again. For example:
+
///
+
/// ```no_run
+
/// let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
+
/// let namespaced = full.namespaced().unwrap();
+
/// let strip_first = namespaced.strip_namespace();
+
/// let nested = strip_first.namespaced().unwrap();
+
/// let strip_second = nested.strip_namespace();
+
///
+
/// assert_eq!("a", namespaced.namespace().as_str());
+
/// assert_eq!("b", nested.namespace().as_str());
+
/// assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
+
/// assert_eq!("refs/heads/main", strip_second.as_str());
+
/// ```
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Namespaced<'a>(Cow<'a, RefStr>);
+

+
impl<'a> Namespaced<'a> {
+
    pub fn namespace(&self) -> Component {
+
        self.components().nth(2).unwrap()
+
    }
+

+
    pub fn strip_namespace<'b>(&self) -> Qualified<'b> {
+
        const REFS_NAMESPACES: &RefStr = RefStr::from_str("refs/namespaces");
+

+
        Qualified(Cow::Owned(
+
            self.strip_prefix(REFS_NAMESPACES)
+
                .unwrap()
+
                .components()
+
                .skip(1)
+
                .collect(),
+
        ))
+
    }
+

+
    pub fn strip_namespace_recursive<'b>(&self) -> Qualified<'b> {
+
        let mut strip = self.strip_namespace();
+
        while let Some(ns) = strip.to_namespaced() {
+
            strip = ns.strip_namespace();
+
        }
+
        strip
+
    }
+

+
    #[inline]
+
    pub fn to_owned<'b>(&self) -> Namespaced<'b> {
+
        Namespaced(Cow::Owned(self.0.clone().into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_owned<'b>(self) -> Namespaced<'b> {
+
        Namespaced(Cow::Owned(self.0.into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_qualified(self) -> Qualified<'a> {
+
        self.into()
+
    }
+
}
+

+
impl Deref for Namespaced<'_> {
+
    type Target = RefStr;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<RefStr> for Namespaced<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &RefStr {
+
        self
+
    }
+
}
+

+
impl AsRef<str> for Namespaced<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self.0.as_str()
+
    }
+
}
+

+
impl<'a> From<Namespaced<'a>> for Qualified<'a> {
+
    #[inline]
+
    fn from(ns: Namespaced<'a>) -> Self {
+
        Self(ns.0)
+
    }
+
}
+

+
impl<'a> From<&'a RefStr> for Option<Namespaced<'a>> {
+
    fn from(rs: &'a RefStr) -> Self {
+
        let mut cs = rs.iter();
+
        match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) {
+
            ("refs", "namespaces", _, "refs") => Some(Namespaced(Cow::from(rs))),
+

+
            _ => None,
+
        }
+
    }
+
}
+

+
impl<'a, T> From<lit::RefsNamespaces<'_, T>> for Namespaced<'static>
+
where
+
    T: Into<Component<'a>>,
+
{
+
    #[inline]
+
    fn from((refs, namespaces, namespace, name): lit::RefsNamespaces<T>) -> Self {
+
        Self(Cow::Owned(
+
            IntoIterator::into_iter([refs.into(), namespaces.into(), namespace.into()])
+
                .collect::<RefString>()
+
                .and(name),
+
        ))
+
    }
+
}
+

+
impl Display for Namespaced<'_> {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
added radicle-git-ext/git-ref-format/core/src/lib.rs
@@ -0,0 +1,25 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
mod check;
+
pub use check::{ref_format as check_ref_format, Error, Options};
+

+
mod deriv;
+
pub use deriv::{Namespaced, Qualified};
+

+
pub mod lit;
+

+
pub mod name;
+
#[cfg(feature = "percent-encoding")]
+
pub use name::PercentEncode;
+
pub use name::{Component, RefStr, RefString};
+

+
pub mod refspec;
+
pub use refspec::DuplicateGlob;
+

+
#[cfg(feature = "minicbor")]
+
mod cbor;
+
#[cfg(feature = "serde")]
+
mod serde;
added radicle-git-ext/git-ref-format/core/src/lit.rs
@@ -0,0 +1,197 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use crate::{name, Qualified, RefStr};
+

+
/// A literal [`RefStr`].
+
///
+
/// Types implementing [`Lit`] must be [`name::Component`]s, and provide a
+
/// conversion from a component _iff_ the component's [`RefStr`] representation
+
/// is equal to [`Lit::NAME`]. Because these morphisms can only be guaranteed
+
/// axiomatically, the trait can not currently be implemented by types outside
+
/// of this crate.
+
///
+
/// [`Lit`] types are useful for efficiently creating known-valid [`Qualified`]
+
/// refs, and sometimes for pattern matching.
+
pub trait Lit: Sized + sealed::Sealed {
+
    const SELF: Self;
+
    const NAME: &'static RefStr;
+

+
    #[inline]
+
    fn from_component(c: &name::Component) -> Option<Self> {
+
        (c.as_ref() == Self::NAME).then_some(Self::SELF)
+
    }
+
}
+

+
impl<T: Lit> From<T> for &'static RefStr {
+
    #[inline]
+
    fn from(_: T) -> Self {
+
        T::NAME
+
    }
+
}
+

+
mod sealed {
+
    pub trait Sealed {}
+
}
+

+
/// All known literal [`RefStr`]s.
+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub enum KnownLit {
+
    Refs,
+
    Heads,
+
    Namespaces,
+
    Remotes,
+
    Tags,
+
    Notes,
+
}
+

+
impl KnownLit {
+
    #[inline]
+
    pub fn from_component(c: &name::Component) -> Option<Self> {
+
        let c: &RefStr = c.as_ref();
+
        if c == Refs::NAME {
+
            Some(Self::Refs)
+
        } else if c == Heads::NAME {
+
            Some(Self::Heads)
+
        } else if c == Namespaces::NAME {
+
            Some(Self::Namespaces)
+
        } else if c == Remotes::NAME {
+
            Some(Self::Remotes)
+
        } else if c == Tags::NAME {
+
            Some(Self::Tags)
+
        } else if c == Notes::NAME {
+
            Some(Self::Notes)
+
        } else {
+
            None
+
        }
+
    }
+
}
+

+
impl From<KnownLit> for name::Component<'_> {
+
    #[inline]
+
    fn from(k: KnownLit) -> Self {
+
        match k {
+
            KnownLit::Refs => Refs.into(),
+
            KnownLit::Heads => Heads.into(),
+
            KnownLit::Namespaces => Namespaces.into(),
+
            KnownLit::Remotes => Remotes.into(),
+
            KnownLit::Tags => Tags.into(),
+
            KnownLit::Notes => Notes.into(),
+
        }
+
    }
+
}
+

+
/// Either a [`KnownLit`] or a [`name::Component`]
+
pub enum SomeLit<'a> {
+
    Known(KnownLit),
+
    Any(name::Component<'a>),
+
}
+

+
impl SomeLit<'_> {
+
    pub fn known(self) -> Option<KnownLit> {
+
        match self {
+
            Self::Known(k) => Some(k),
+
            _ => None,
+
        }
+
    }
+
}
+

+
impl<'a> From<name::Component<'a>> for SomeLit<'a> {
+
    #[inline]
+
    fn from(c: name::Component<'a>) -> Self {
+
        match KnownLit::from_component(&c) {
+
            Some(k) => Self::Known(k),
+
            None => Self::Any(c),
+
        }
+
    }
+
}
+

+
pub type RefsHeads<T> = (Refs, Heads, T);
+
pub type RefsTags<T> = (Refs, Tags, T);
+
pub type RefsNotes<T> = (Refs, Notes, T);
+
pub type RefsRemotes<T> = (Refs, Remotes, T);
+
pub type RefsNamespaces<'a, T> = (Refs, Namespaces, T, Qualified<'a>);
+

+
#[inline]
+
pub fn refs_heads<T: AsRef<RefStr>>(name: T) -> RefsHeads<T> {
+
    (Refs, Heads, name)
+
}
+

+
#[inline]
+
pub fn refs_tags<T: AsRef<RefStr>>(name: T) -> RefsTags<T> {
+
    (Refs, Tags, name)
+
}
+

+
#[inline]
+
pub fn refs_notes<T: AsRef<RefStr>>(name: T) -> RefsNotes<T> {
+
    (Refs, Notes, name)
+
}
+

+
#[inline]
+
pub fn refs_remotes<T: AsRef<RefStr>>(name: T) -> RefsRemotes<T> {
+
    (Refs, Remotes, name)
+
}
+

+
#[inline]
+
pub fn refs_namespaces<'a, 'b, T>(namespace: T, name: Qualified<'b>) -> RefsNamespaces<'b, T>
+
where
+
    T: Into<name::Component<'a>>,
+
{
+
    (Refs, Namespaces, namespace, name)
+
}
+

+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Refs;
+

+
impl Lit for Refs {
+
    const SELF: Self = Self;
+
    const NAME: &'static RefStr = name::REFS;
+
}
+
impl sealed::Sealed for Refs {}
+

+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Heads;
+

+
impl Lit for Heads {
+
    const SELF: Self = Self;
+
    const NAME: &'static RefStr = name::HEADS;
+
}
+
impl sealed::Sealed for Heads {}
+

+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Namespaces;
+

+
impl Lit for Namespaces {
+
    const SELF: Self = Self;
+
    const NAME: &'static RefStr = name::NAMESPACES;
+
}
+
impl sealed::Sealed for Namespaces {}
+

+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Remotes;
+

+
impl Lit for Remotes {
+
    const SELF: Self = Self;
+
    const NAME: &'static RefStr = name::REMOTES;
+
}
+
impl sealed::Sealed for Remotes {}
+

+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Tags;
+

+
impl Lit for Tags {
+
    const SELF: Self = Self;
+
    const NAME: &'static RefStr = name::TAGS;
+
}
+
impl sealed::Sealed for Tags {}
+

+
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Notes;
+

+
impl Lit for Notes {
+
    const SELF: Self = Self;
+
    const NAME: &'static RefStr = name::NOTES;
+
}
+
impl sealed::Sealed for Notes {}
added radicle-git-ext/git-ref-format/core/src/name.rs
@@ -0,0 +1,470 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{
+
    borrow::{Borrow, Cow},
+
    convert::TryFrom,
+
    fmt::{self, Display},
+
    iter::{Extend, FromIterator},
+
    ops::Deref,
+
};
+

+
use crate::{
+
    check,
+
    refspec::{PatternStr, PatternString},
+
    Namespaced,
+
    Qualified,
+
};
+

+
mod iter;
+
pub use iter::{component, Component, Components, Iter};
+

+
#[cfg(feature = "percent-encoding")]
+
pub use percent_encoding::PercentEncode;
+

+
pub const HEADS: &RefStr = RefStr::from_str(str::HEADS);
+
pub const MAIN: &RefStr = RefStr::from_str(str::MAIN);
+
pub const MASTER: &RefStr = RefStr::from_str(str::MASTER);
+
pub const NAMESPACES: &RefStr = RefStr::from_str(str::NAMESPACES);
+
pub const NOTES: &RefStr = RefStr::from_str(str::NOTES);
+
pub const ORIGIN: &RefStr = RefStr::from_str(str::ORIGIN);
+
pub const REFS: &RefStr = RefStr::from_str(str::REFS);
+
pub const REMOTES: &RefStr = RefStr::from_str(str::REMOTES);
+
pub const TAGS: &RefStr = RefStr::from_str(str::TAGS);
+

+
pub const REFS_HEADS_MAIN: &RefStr = RefStr::from_str(str::REFS_HEADS_MAIN);
+
pub const REFS_HEADS_MASTER: &RefStr = RefStr::from_str(str::REFS_HEADS_MASTER);
+

+
pub mod str {
+
    pub const HEADS: &str = "heads";
+
    pub const MAIN: &str = "main";
+
    pub const MASTER: &str = "master";
+
    pub const NAMESPACES: &str = "namespaces";
+
    pub const NOTES: &str = "notes";
+
    pub const ORIGIN: &str = "origin";
+
    pub const REFS: &str = "refs";
+
    pub const REMOTES: &str = "remotes";
+
    pub const TAGS: &str = "tags";
+

+
    pub const REFS_HEADS_MAIN: &str = "refs/heads/main";
+
    pub const REFS_HEADS_MASTER: &str = "refs/heads/master";
+
}
+

+
pub mod bytes {
+
    use super::str;
+

+
    pub const HEADS: &[u8] = str::HEADS.as_bytes();
+
    pub const MAIN: &[u8] = str::MAIN.as_bytes();
+
    pub const MASTER: &[u8] = str::MASTER.as_bytes();
+
    pub const NAMESPACES: &[u8] = str::NAMESPACES.as_bytes();
+
    pub const NOTES: &[u8] = str::NOTES.as_bytes();
+
    pub const ORIGIN: &[u8] = str::ORIGIN.as_bytes();
+
    pub const REFS: &[u8] = str::REFS.as_bytes();
+
    pub const REMOTES: &[u8] = str::REMOTES.as_bytes();
+
    pub const TAGS: &[u8] = str::TAGS.as_bytes();
+

+
    pub const REFS_HEADS_MAIN: &[u8] = str::REFS_HEADS_MAIN.as_bytes();
+
    pub const REFS_HEADS_MASTER: &[u8] = str::REFS_HEADS_MASTER.as_bytes();
+
}
+

+
const CHECK_OPTS: check::Options = check::Options {
+
    allow_pattern: false,
+
    allow_onelevel: true,
+
};
+

+
#[repr(transparent)]
+
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct RefStr(str);
+

+
impl RefStr {
+
    pub fn try_from_str(s: &str) -> Result<&RefStr, check::Error> {
+
        TryFrom::try_from(s)
+
    }
+

+
    #[inline]
+
    pub fn as_str(&self) -> &str {
+
        self
+
    }
+

+
    #[inline]
+
    pub fn to_ref_string(&self) -> RefString {
+
        self.to_owned()
+
    }
+

+
    pub fn strip_prefix<P>(&self, base: P) -> Option<&RefStr>
+
    where
+
        P: AsRef<RefStr>,
+
    {
+
        self._strip_prefix(base.as_ref())
+
    }
+

+
    fn _strip_prefix(&self, base: &RefStr) -> Option<&RefStr> {
+
        self.0
+
            .strip_prefix(base.as_str())
+
            .and_then(|s| s.strip_prefix('/'))
+
            .map(Self::from_str)
+
    }
+

+
    /// Join `other` onto `self`, yielding a new [`RefString`].
+
    ///
+
    /// Consider to use [`RefString::and`] when chaining multiple fragments
+
    /// together, and the intermediate values are not needed.
+
    pub fn join<R>(&self, other: R) -> RefString
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self._join(other.as_ref())
+
    }
+

+
    fn _join(&self, other: &RefStr) -> RefString {
+
        let mut buf = self.to_ref_string();
+
        buf.push(other);
+
        buf
+
    }
+

+
    pub fn to_pattern<P>(&self, pattern: P) -> PatternString
+
    where
+
        P: AsRef<PatternStr>,
+
    {
+
        self._to_pattern(pattern.as_ref())
+
    }
+

+
    fn _to_pattern(&self, pattern: &PatternStr) -> PatternString {
+
        self.to_owned().with_pattern(pattern)
+
    }
+

+
    #[inline]
+
    pub fn qualified(&self) -> Option<Qualified> {
+
        Qualified::from_refstr(self)
+
    }
+

+
    #[inline]
+
    pub fn to_namespaced(&self) -> Option<Namespaced> {
+
        self.into()
+
    }
+

+
    pub fn iter(&self) -> Iter {
+
        self.0.split('/')
+
    }
+

+
    pub fn components(&self) -> Components {
+
        Components::from(self)
+
    }
+

+
    pub fn head(&self) -> Component {
+
        self.components().next().expect("`RefStr` cannot be empty")
+
    }
+

+
    #[cfg(feature = "percent-encoding")]
+
    pub fn percent_encode(&self) -> PercentEncode {
+
        /// https://url.spec.whatwg.org/#fragment-percent-encode-set
+
        const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
+
            &percent_encoding::CONTROLS
+
                .add(b' ')
+
                .add(b'"')
+
                .add(b'<')
+
                .add(b'>')
+
                .add(b'`');
+

+
        /// https://url.spec.whatwg.org/#path-percent-encode-set
+
        const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
+
            .add(b'#')
+
            .add(b'?')
+
            .add(b'{')
+
            .add(b'}');
+

+
        percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
+
    }
+

+
    #[cfg(feature = "bstr")]
+
    #[inline]
+
    pub fn as_bstr(&self) -> &bstr::BStr {
+
        self.as_ref()
+
    }
+

+
    pub(crate) const fn from_str(s: &str) -> &RefStr {
+
        unsafe { &*(s as *const str as *const RefStr) }
+
    }
+
}
+

+
impl Deref for RefStr {
+
    type Target = str;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<str> for RefStr {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self
+
    }
+
}
+

+
#[cfg(feature = "bstr")]
+
impl AsRef<bstr::BStr> for RefStr {
+
    #[inline]
+
    fn as_ref(&self) -> &bstr::BStr {
+
        use bstr::ByteSlice as _;
+
        self.as_str().as_bytes().as_bstr()
+
    }
+
}
+

+
impl<'a> AsRef<RefStr> for &'a RefStr {
+
    #[inline]
+
    fn as_ref(&self) -> &RefStr {
+
        self
+
    }
+
}
+

+
impl<'a> TryFrom<&'a str> for &'a RefStr {
+
    type Error = check::Error;
+

+
    #[inline]
+
    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
+
        check::ref_format(CHECK_OPTS, s).map(|()| RefStr::from_str(s))
+
    }
+
}
+

+
impl<'a> From<&'a RefStr> for Cow<'a, RefStr> {
+
    #[inline]
+
    fn from(rs: &'a RefStr) -> Cow<'a, RefStr> {
+
        Cow::Borrowed(rs)
+
    }
+
}
+

+
impl Display for RefStr {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(self)
+
    }
+
}
+

+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct RefString(String);
+

+
impl RefString {
+
    #[inline]
+
    pub fn as_refstr(&self) -> &RefStr {
+
        self
+
    }
+

+
    /// Join `other` onto `self` in place.
+
    ///
+
    /// This is a consuming version of [`RefString::push`] which can be chained.
+
    /// Prefer this over chaining calls to [`RefStr::join`] if the
+
    /// intermediate values are not needed.
+
    pub fn and<R>(self, other: R) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self._and(other.as_ref())
+
    }
+

+
    fn _and(mut self, other: &RefStr) -> Self {
+
        self.push(other);
+
        self
+
    }
+

+
    pub fn push<R>(&mut self, other: R)
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self.0.push('/');
+
        self.0.push_str(other.as_ref().as_str());
+
    }
+

+
    #[inline]
+
    pub fn pop(&mut self) -> bool {
+
        match self.0.rfind('/') {
+
            None => false,
+
            Some(idx) => {
+
                self.0.truncate(idx);
+
                true
+
            },
+
        }
+
    }
+

+
    /// Append a [`PatternStr`], turning self into a new [`PatternString`].
+
    pub fn with_pattern<P>(self, pattern: P) -> PatternString
+
    where
+
        P: AsRef<PatternStr>,
+
    {
+
        self._with_pattern(pattern.as_ref())
+
    }
+

+
    fn _with_pattern(self, pattern: &PatternStr) -> PatternString {
+
        let mut buf = self.0;
+
        buf.push('/');
+
        buf.push_str(pattern.as_str());
+

+
        PatternString(buf)
+
    }
+

+
    #[inline]
+
    pub fn into_qualified<'a>(self) -> Option<Qualified<'a>> {
+
        Qualified::from_refstr(self)
+
    }
+

+
    #[inline]
+
    pub fn reserve(&mut self, additional: usize) {
+
        self.0.reserve(additional)
+
    }
+

+
    #[inline]
+
    pub fn shrink_to_fit(&mut self) {
+
        self.0.shrink_to_fit()
+
    }
+

+
    #[cfg(feature = "bstr")]
+
    #[inline]
+
    pub fn into_bstring(self) -> bstr::BString {
+
        self.into()
+
    }
+

+
    #[cfg(feature = "bstr")]
+
    #[inline]
+
    pub fn as_bstr(&self) -> &bstr::BStr {
+
        self.as_ref()
+
    }
+
}
+

+
impl Deref for RefString {
+
    type Target = RefStr;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        self.borrow()
+
    }
+
}
+

+
impl AsRef<RefStr> for RefString {
+
    #[inline]
+
    fn as_ref(&self) -> &RefStr {
+
        self
+
    }
+
}
+

+
impl AsRef<str> for RefString {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self.0.as_str()
+
    }
+
}
+

+
#[cfg(feature = "bstr")]
+
impl AsRef<bstr::BStr> for RefString {
+
    #[inline]
+
    fn as_ref(&self) -> &bstr::BStr {
+
        use bstr::ByteSlice as _;
+
        self.as_str().as_bytes().as_bstr()
+
    }
+
}
+

+
impl Borrow<RefStr> for RefString {
+
    #[inline]
+
    fn borrow(&self) -> &RefStr {
+
        RefStr::from_str(self.0.as_str())
+
    }
+
}
+

+
impl ToOwned for RefStr {
+
    type Owned = RefString;
+

+
    #[inline]
+
    fn to_owned(&self) -> Self::Owned {
+
        RefString(self.0.to_owned())
+
    }
+
}
+

+
impl TryFrom<&str> for RefString {
+
    type Error = check::Error;
+

+
    #[inline]
+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        RefStr::try_from_str(s).map(ToOwned::to_owned)
+
    }
+
}
+

+
impl TryFrom<String> for RefString {
+
    type Error = check::Error;
+

+
    #[inline]
+
    fn try_from(s: String) -> Result<Self, Self::Error> {
+
        check::ref_format(CHECK_OPTS, s.as_str()).map(|()| RefString(s))
+
    }
+
}
+

+
impl<'a> From<&'a RefString> for Cow<'a, RefStr> {
+
    #[inline]
+
    fn from(rs: &'a RefString) -> Cow<'a, RefStr> {
+
        Cow::Borrowed(rs.as_refstr())
+
    }
+
}
+

+
impl<'a> From<RefString> for Cow<'a, RefStr> {
+
    #[inline]
+
    fn from(rs: RefString) -> Cow<'a, RefStr> {
+
        Cow::Owned(rs)
+
    }
+
}
+

+
impl From<RefString> for String {
+
    #[inline]
+
    fn from(rs: RefString) -> Self {
+
        rs.0
+
    }
+
}
+

+
#[cfg(feature = "bstr")]
+
impl From<RefString> for bstr::BString {
+
    #[inline]
+
    fn from(rs: RefString) -> Self {
+
        bstr::BString::from(rs.0.into_bytes())
+
    }
+
}
+

+
impl Display for RefString {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(&self.0)
+
    }
+
}
+

+
impl<A> FromIterator<A> for RefString
+
where
+
    A: AsRef<RefStr>,
+
{
+
    fn from_iter<T>(iter: T) -> Self
+
    where
+
        T: IntoIterator<Item = A>,
+
    {
+
        let mut buf = String::new();
+
        for c in iter {
+
            buf.push_str(c.as_ref().as_str());
+
            buf.push('/');
+
        }
+
        assert!(!buf.is_empty(), "empty iterator");
+
        buf.truncate(buf.len() - 1);
+

+
        Self(buf)
+
    }
+
}
+

+
impl<A> Extend<A> for RefString
+
where
+
    A: AsRef<RefStr>,
+
{
+
    fn extend<T>(&mut self, iter: T)
+
    where
+
        T: IntoIterator<Item = A>,
+
    {
+
        for x in iter {
+
            self.push(x)
+
        }
+
    }
+
}
added radicle-git-ext/git-ref-format/core/src/name/iter.rs
@@ -0,0 +1,160 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{
+
    borrow::Cow,
+
    fmt::{self, Display},
+
    ops::Deref,
+
};
+

+
use super::RefStr;
+
use crate::lit;
+

+
pub type Iter<'a> = std::str::Split<'a, char>;
+

+
/// Iterator created by the [`RefStr::iter`] method.
+
#[must_use = "iterators are lazy and do nothing unless consumed"]
+
#[derive(Clone)]
+
pub struct Components<'a> {
+
    inner: std::str::Split<'a, char>,
+
}
+

+
impl<'a> Iterator for Components<'a> {
+
    type Item = Component<'a>;
+

+
    #[inline]
+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.inner
+
            .next()
+
            .map(RefStr::from_str)
+
            .map(Cow::from)
+
            .map(Component)
+
    }
+
}
+

+
impl<'a> DoubleEndedIterator for Components<'a> {
+
    #[inline]
+
    fn next_back(&mut self) -> Option<Self::Item> {
+
        self.inner
+
            .next_back()
+
            .map(RefStr::from_str)
+
            .map(Cow::from)
+
            .map(Component)
+
    }
+
}
+

+
impl<'a> From<&'a RefStr> for Components<'a> {
+
    #[inline]
+
    fn from(rs: &'a RefStr) -> Self {
+
        Self {
+
            inner: rs.as_str().split('/'),
+
        }
+
    }
+
}
+

+
/// A path component of a [`RefStr`].
+
///
+
/// A [`Component`] is a valid [`RefStr`] which does not contain any '/'
+
/// separators.
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct Component<'a>(Cow<'a, RefStr>);
+

+
impl<'a> Component<'a> {
+
    #[inline]
+
    pub fn from_refstr(r: impl Into<Cow<'a, RefStr>>) -> Option<Self> {
+
        let r = r.into();
+
        if !r.contains('/') {
+
            Some(Self(r))
+
        } else {
+
            None
+
        }
+
    }
+

+
    #[inline]
+
    pub fn as_lit<T: lit::Lit>(&self) -> Option<T> {
+
        T::from_component(self)
+
    }
+

+
    #[inline]
+
    pub fn into_inner(self) -> Cow<'a, RefStr> {
+
        self.0
+
    }
+
}
+

+
impl<'a> Deref for Component<'a> {
+
    type Target = RefStr;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<RefStr> for Component<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &RefStr {
+
        self
+
    }
+
}
+

+
impl<'a> From<&'a RefStr> for Option<Component<'a>> {
+
    #[inline]
+
    fn from(r: &'a RefStr) -> Self {
+
        if !r.contains('/') {
+
            Some(Component(Cow::from(r)))
+
        } else {
+
            None
+
        }
+
    }
+
}
+

+
impl<'a> From<Component<'a>> for Cow<'a, RefStr> {
+
    #[inline]
+
    fn from(c: Component<'a>) -> Self {
+
        c.0
+
    }
+
}
+

+
impl<T: lit::Lit> From<T> for Component<'static> {
+
    #[inline]
+
    fn from(_: T) -> Self {
+
        Component(Cow::from(T::NAME))
+
    }
+
}
+

+
impl<'a> From<lit::SomeLit<'a>> for Component<'a> {
+
    #[inline]
+
    fn from(s: lit::SomeLit<'a>) -> Self {
+
        use lit::SomeLit::*;
+

+
        match s {
+
            Known(k) => k.into(),
+
            Any(c) => c,
+
        }
+
    }
+
}
+

+
impl Display for Component<'_> {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(self.0.as_str())
+
    }
+
}
+

+
pub mod component {
+
    use super::Component;
+
    use crate::name;
+
    use std::borrow::Cow;
+

+
    pub const HEADS: Component = Component(Cow::Borrowed(name::HEADS));
+
    pub const MAIN: Component = Component(Cow::Borrowed(name::MAIN));
+
    pub const MASTER: Component = Component(Cow::Borrowed(name::MASTER));
+
    pub const NAMESPACES: Component = Component(Cow::Borrowed(name::NAMESPACES));
+
    pub const NOTES: Component = Component(Cow::Borrowed(name::NOTES));
+
    pub const ORIGIN: Component = Component(Cow::Borrowed(name::ORIGIN));
+
    pub const REFS: Component = Component(Cow::Borrowed(name::REFS));
+
    pub const REMOTES: Component = Component(Cow::Borrowed(name::REMOTES));
+
    pub const TAGS: Component = Component(Cow::Borrowed(name::TAGS));
+
}
added radicle-git-ext/git-ref-format/core/src/refspec.rs
@@ -0,0 +1,560 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{
+
    borrow::{Borrow, Cow},
+
    convert::TryFrom,
+
    fmt::{self, Display},
+
    iter::FromIterator,
+
    ops::Deref,
+
};
+

+
use thiserror::Error;
+

+
use crate::{check, lit, RefStr, RefString};
+

+
mod iter;
+
pub use iter::{component, Component, Components, Iter};
+

+
pub const STAR: &PatternStr = PatternStr::from_str("*");
+

+
const CHECK_OPTS: check::Options = check::Options {
+
    allow_onelevel: true,
+
    allow_pattern: true,
+
};
+

+
#[repr(transparent)]
+
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct PatternStr(str);
+

+
impl PatternStr {
+
    #[inline]
+
    pub fn try_from_str(s: &str) -> Result<&Self, check::Error> {
+
        TryFrom::try_from(s)
+
    }
+

+
    #[inline]
+
    pub fn as_str(&self) -> &str {
+
        self
+
    }
+

+
    pub fn join<R>(&self, other: R) -> PatternString
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self._join(other.as_ref())
+
    }
+

+
    fn _join(&self, other: &RefStr) -> PatternString {
+
        let mut buf = self.to_owned();
+
        buf.push(other);
+
        buf
+
    }
+

+
    #[inline]
+
    pub fn qualified(&self) -> Option<QualifiedPattern> {
+
        QualifiedPattern::from_patternstr(self)
+
    }
+

+
    #[inline]
+
    pub fn to_namespaced(&self) -> Option<NamespacedPattern> {
+
        self.into()
+
    }
+

+
    #[inline]
+
    pub fn iter(&self) -> Iter {
+
        self.0.split('/')
+
    }
+

+
    #[inline]
+
    pub fn components(&self) -> Components {
+
        Components::from(self)
+
    }
+

+
    pub(crate) const fn from_str(s: &str) -> &PatternStr {
+
        unsafe { &*(s as *const str as *const PatternStr) }
+
    }
+
}
+

+
impl Deref for PatternStr {
+
    type Target = str;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<str> for PatternStr {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self
+
    }
+
}
+

+
impl AsRef<Self> for PatternStr {
+
    #[inline]
+
    fn as_ref(&self) -> &Self {
+
        self
+
    }
+
}
+

+
impl<'a> TryFrom<&'a str> for &'a PatternStr {
+
    type Error = check::Error;
+

+
    #[inline]
+
    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
+
        check::ref_format(CHECK_OPTS, s).map(|()| PatternStr::from_str(s))
+
    }
+
}
+

+
impl<'a> From<&'a RefStr> for &'a PatternStr {
+
    #[inline]
+
    fn from(rs: &'a RefStr) -> Self {
+
        PatternStr::from_str(rs.as_str())
+
    }
+
}
+

+
impl<'a> From<&'a PatternStr> for Cow<'a, PatternStr> {
+
    #[inline]
+
    fn from(p: &'a PatternStr) -> Cow<'a, PatternStr> {
+
        Cow::Borrowed(p)
+
    }
+
}
+

+
impl Display for PatternStr {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(self)
+
    }
+
}
+

+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct PatternString(pub(crate) String);
+

+
impl PatternString {
+
    #[inline]
+
    pub fn as_str(&self) -> &str {
+
        self.as_ref()
+
    }
+

+
    #[inline]
+
    pub fn as_pattern_str(&self) -> &PatternStr {
+
        self.as_ref()
+
    }
+

+
    #[inline]
+
    pub fn from_components<'a, T>(iter: T) -> Result<Self, DuplicateGlob>
+
    where
+
        T: IntoIterator<Item = Component<'a>>,
+
    {
+
        iter.into_iter().collect()
+
    }
+

+
    #[inline]
+
    pub fn and<R>(mut self, other: R) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self._push(other.as_ref());
+
        self
+
    }
+

+
    #[inline]
+
    pub fn push<R>(&mut self, other: R)
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self._push(other.as_ref())
+
    }
+

+
    fn _push(&mut self, other: &RefStr) {
+
        self.0.push('/');
+
        self.0.push_str(other.as_str());
+
    }
+

+
    #[inline]
+
    pub fn pop(&mut self) -> bool {
+
        match self.0.rfind('/') {
+
            None => false,
+
            Some(idx) => {
+
                self.0.truncate(idx);
+
                true
+
            },
+
        }
+
    }
+
}
+

+
impl Deref for PatternString {
+
    type Target = PatternStr;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        self.borrow()
+
    }
+
}
+

+
impl AsRef<PatternStr> for PatternString {
+
    #[inline]
+
    fn as_ref(&self) -> &PatternStr {
+
        self
+
    }
+
}
+

+
impl AsRef<str> for PatternString {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self.0.as_str()
+
    }
+
}
+

+
impl Borrow<PatternStr> for PatternString {
+
    #[inline]
+
    fn borrow(&self) -> &PatternStr {
+
        PatternStr::from_str(self.0.as_str())
+
    }
+
}
+

+
impl ToOwned for PatternStr {
+
    type Owned = PatternString;
+

+
    #[inline]
+
    fn to_owned(&self) -> Self::Owned {
+
        PatternString(self.0.to_owned())
+
    }
+
}
+

+
impl From<RefString> for PatternString {
+
    #[inline]
+
    fn from(rs: RefString) -> Self {
+
        Self(rs.into())
+
    }
+
}
+

+
impl<'a> From<&'a PatternString> for Cow<'a, PatternStr> {
+
    #[inline]
+
    fn from(p: &'a PatternString) -> Cow<'a, PatternStr> {
+
        Cow::Borrowed(p.as_ref())
+
    }
+
}
+

+
impl From<PatternString> for String {
+
    #[inline]
+
    fn from(p: PatternString) -> Self {
+
        p.0
+
    }
+
}
+

+
impl TryFrom<&str> for PatternString {
+
    type Error = check::Error;
+

+
    #[inline]
+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        PatternStr::try_from_str(s).map(ToOwned::to_owned)
+
    }
+
}
+

+
impl TryFrom<String> for PatternString {
+
    type Error = check::Error;
+

+
    #[inline]
+
    fn try_from(s: String) -> Result<Self, Self::Error> {
+
        check::ref_format(CHECK_OPTS, s.as_str()).map(|()| PatternString(s))
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
#[error("more than one '*' encountered")]
+
pub struct DuplicateGlob;
+

+
impl<'a> FromIterator<Component<'a>> for Result<PatternString, DuplicateGlob> {
+
    fn from_iter<T>(iter: T) -> Self
+
    where
+
        T: IntoIterator<Item = Component<'a>>,
+
    {
+
        use Component::*;
+

+
        let mut buf = String::new();
+
        let mut seen_glob = false;
+
        for c in iter {
+
            if let Glob(_) = c {
+
                if seen_glob {
+
                    return Err(DuplicateGlob);
+
                }
+

+
                seen_glob = true;
+
            }
+

+
            buf.push_str(c.as_str());
+
            buf.push('/');
+
        }
+
        buf.truncate(buf.len() - 1);
+

+
        Ok(PatternString(buf))
+
    }
+
}
+

+
impl Display for PatternString {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(&self.0)
+
    }
+
}
+

+
/// A fully-qualified refspec.
+
///
+
/// A refspec is qualified _iff_ it starts with "refs/" and has at least three
+
/// components. This implies that a [`QualifiedPattern`] ref has a category,
+
/// such as "refs/heads/*".
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct QualifiedPattern<'a>(pub(crate) Cow<'a, PatternStr>);
+

+
impl<'a> QualifiedPattern<'a> {
+
    pub fn from_patternstr(r: impl Into<Cow<'a, PatternStr>>) -> Option<Self> {
+
        Self::_from_patternstr(r.into())
+
    }
+

+
    fn _from_patternstr(r: Cow<'a, PatternStr>) -> Option<Self> {
+
        let mut iter = r.iter();
+
        match (iter.next()?, iter.next()?, iter.next()?) {
+
            ("refs", _, _) => Some(QualifiedPattern(r)),
+
            _ => None,
+
        }
+
    }
+

+
    #[inline]
+
    pub fn as_str(&self) -> &str {
+
        self.as_ref()
+
    }
+

+
    #[inline]
+
    pub fn join<'b, R>(&self, other: R) -> QualifiedPattern<'b>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        QualifiedPattern(Cow::Owned(self.0.join(other)))
+
    }
+

+
    #[inline]
+
    pub fn to_namespaced(&self) -> Option<NamespacedPattern> {
+
        self.0.as_ref().into()
+
    }
+

+
    /// Add a namespace.
+
    ///
+
    /// Creates a new [`NamespacedPattern`] by prefxing `self` with
+
    /// `refs/namespaces/<ns>`.
+
    pub fn with_namespace<'b>(
+
        &self,
+
        ns: Component<'b>,
+
    ) -> Result<NamespacedPattern<'a>, DuplicateGlob> {
+
        PatternString::from_components(
+
            IntoIterator::into_iter([lit::Refs.into(), lit::Namespaces.into(), ns])
+
                .chain(self.components()),
+
        )
+
        .map(|pat| NamespacedPattern(Cow::Owned(pat)))
+
    }
+

+
    /// Like [`Self::non_empty_components`], but with string slices.
+
    pub fn non_empty_iter(&self) -> (&str, &str, &str, Iter) {
+
        let mut iter = self.iter();
+
        (
+
            iter.next().unwrap(),
+
            iter.next().unwrap(),
+
            iter.next().unwrap(),
+
            iter,
+
        )
+
    }
+

+
    /// Return the first three [`Component`]s, and a possibly empty iterator
+
    /// over the remaining ones.
+
    ///
+
    /// A qualified ref is guaranteed to have at least three components, which
+
    /// this method provides a witness of. This is useful eg. for pattern
+
    /// matching on the prefix.
+
    pub fn non_empty_components(&self) -> (Component, Component, Component, Components) {
+
        let mut cs = self.components();
+
        (
+
            cs.next().unwrap(),
+
            cs.next().unwrap(),
+
            cs.next().unwrap(),
+
            cs,
+
        )
+
    }
+

+
    #[inline]
+
    pub fn to_owned<'b>(&self) -> QualifiedPattern<'b> {
+
        QualifiedPattern(Cow::Owned(self.0.clone().into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_owned<'b>(self) -> QualifiedPattern<'b> {
+
        QualifiedPattern(Cow::Owned(self.0.into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_patternstring(self) -> PatternString {
+
        self.into()
+
    }
+
}
+

+
impl Deref for QualifiedPattern<'_> {
+
    type Target = PatternStr;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<PatternStr> for QualifiedPattern<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &PatternStr {
+
        self
+
    }
+
}
+

+
impl AsRef<str> for QualifiedPattern<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self.0.as_str()
+
    }
+
}
+

+
impl AsRef<Self> for QualifiedPattern<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &Self {
+
        self
+
    }
+
}
+

+
impl<'a> From<QualifiedPattern<'a>> for Cow<'a, PatternStr> {
+
    #[inline]
+
    fn from(q: QualifiedPattern<'a>) -> Self {
+
        q.0
+
    }
+
}
+

+
impl From<QualifiedPattern<'_>> for PatternString {
+
    #[inline]
+
    fn from(q: QualifiedPattern) -> Self {
+
        q.0.into_owned()
+
    }
+
}
+

+
/// A [`PatternString`] ref under a git namespace.
+
///
+
/// A ref is namespaced if it starts with "refs/namespaces/", another path
+
/// component, and "refs/". Eg.
+
///
+
///     refs/namespaces/xyz/refs/heads/main
+
///
+
/// Note that namespaces can be nested, so the result of
+
/// [`NamespacedPattern::strip_namespace`] may be convertible to a
+
/// [`NamespacedPattern`] again. For example:
+
///
+
/// ```no_run
+
/// let full = pattern!("refs/namespaces/a/refs/namespaces/b/refs/heads/*");
+
/// let namespaced = full.to_namespaced().unwrap();
+
/// let strip_first = namespaced.strip_namespace();
+
/// let nested = strip_first.namespaced().unwrap();
+
/// let strip_second = nested.strip_namespace();
+
///
+
/// assert_eq!("a", namespaced.namespace().as_str());
+
/// assert_eq!("b", nested.namespace().as_str());
+
/// assert_eq!("refs/namespaces/b/refs/heads/*", strip_first.as_str());
+
/// assert_eq!("refs/heads/*", strip_second.as_str());
+
/// ```
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
pub struct NamespacedPattern<'a>(Cow<'a, PatternStr>);
+

+
impl<'a> NamespacedPattern<'a> {
+
    pub fn namespace(&self) -> Component {
+
        self.components().nth(2).unwrap()
+
    }
+

+
    pub fn strip_namespace(&self) -> PatternString {
+
        PatternString::from_components(self.components().skip(3))
+
            .expect("BUG: NamespacedPattern was constructed with a duplicate glob")
+
    }
+

+
    pub fn strip_namespace_recursive(&self) -> PatternString {
+
        let mut strip = self.strip_namespace();
+
        while let Some(ns) = strip.to_namespaced() {
+
            strip = ns.strip_namespace();
+
        }
+
        strip
+
    }
+

+
    #[inline]
+
    pub fn to_owned<'b>(&self) -> NamespacedPattern<'b> {
+
        NamespacedPattern(Cow::Owned(self.0.clone().into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_owned<'b>(self) -> NamespacedPattern<'b> {
+
        NamespacedPattern(Cow::Owned(self.0.into_owned()))
+
    }
+

+
    #[inline]
+
    pub fn into_qualified(self) -> QualifiedPattern<'a> {
+
        self.into()
+
    }
+
}
+

+
impl Deref for NamespacedPattern<'_> {
+
    type Target = PatternStr;
+

+
    #[inline]
+
    fn deref(&self) -> &Self::Target {
+
        self.borrow()
+
    }
+
}
+

+
impl AsRef<PatternStr> for NamespacedPattern<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &PatternStr {
+
        self.0.as_ref()
+
    }
+
}
+

+
impl AsRef<str> for NamespacedPattern<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        self.0.as_str()
+
    }
+
}
+

+
impl Borrow<PatternStr> for NamespacedPattern<'_> {
+
    #[inline]
+
    fn borrow(&self) -> &PatternStr {
+
        PatternStr::from_str(self.0.as_str())
+
    }
+
}
+

+
impl<'a> From<NamespacedPattern<'a>> for QualifiedPattern<'a> {
+
    #[inline]
+
    fn from(ns: NamespacedPattern<'a>) -> Self {
+
        Self(ns.0)
+
    }
+
}
+

+
impl<'a> From<&'a PatternStr> for Option<NamespacedPattern<'a>> {
+
    fn from(rs: &'a PatternStr) -> Self {
+
        let mut cs = rs.iter();
+
        match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) {
+
            ("refs", "namespaces", _, "refs") => Some(NamespacedPattern(Cow::from(rs))),
+

+
            _ => None,
+
        }
+
    }
+
}
+

+
impl Display for NamespacedPattern<'_> {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
added radicle-git-ext/git-ref-format/core/src/refspec/iter.rs
@@ -0,0 +1,103 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::fmt::{self, Display};
+

+
use super::PatternStr;
+
use crate::{lit, RefStr};
+

+
pub type Iter<'a> = std::str::Split<'a, char>;
+

+
pub enum Component<'a> {
+
    Glob(Option<&'a PatternStr>),
+
    Normal(&'a RefStr),
+
}
+

+
impl Component<'_> {
+
    #[inline]
+
    pub fn as_str(&self) -> &str {
+
        self.as_ref()
+
    }
+
}
+

+
impl AsRef<str> for Component<'_> {
+
    #[inline]
+
    fn as_ref(&self) -> &str {
+
        match self {
+
            Self::Glob(None) => "*",
+
            Self::Glob(Some(x)) => x.as_str(),
+
            Self::Normal(x) => x.as_str(),
+
        }
+
    }
+
}
+

+
impl Display for Component<'_> {
+
    #[inline]
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(self.as_str())
+
    }
+
}
+

+
impl<T: lit::Lit> From<T> for Component<'static> {
+
    #[inline]
+
    fn from(_: T) -> Self {
+
        Self::Normal(T::NAME)
+
    }
+
}
+

+
#[must_use = "iterators are lazy and do nothing unless consumed"]
+
#[derive(Clone)]
+
pub struct Components<'a> {
+
    inner: Iter<'a>,
+
}
+

+
impl<'a> Iterator for Components<'a> {
+
    type Item = Component<'a>;
+

+
    #[inline]
+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.inner.next().map(|next| match next {
+
            "*" => Component::Glob(None),
+
            x if x.contains('*') => Component::Glob(Some(PatternStr::from_str(x))),
+
            x => Component::Normal(RefStr::from_str(x)),
+
        })
+
    }
+
}
+

+
impl<'a> DoubleEndedIterator for Components<'a> {
+
    #[inline]
+
    fn next_back(&mut self) -> Option<Self::Item> {
+
        self.inner.next_back().map(|next| match next {
+
            "*" => Component::Glob(None),
+
            x if x.contains('*') => Component::Glob(Some(PatternStr::from_str(x))),
+
            x => Component::Normal(RefStr::from_str(x)),
+
        })
+
    }
+
}
+

+
impl<'a> From<&'a PatternStr> for Components<'a> {
+
    #[inline]
+
    fn from(p: &'a PatternStr) -> Self {
+
        Self {
+
            inner: p.as_str().split('/'),
+
        }
+
    }
+
}
+

+
pub mod component {
+
    use super::Component;
+
    use crate::name;
+

+
    pub const STAR: Component = Component::Glob(None);
+
    pub const HEADS: Component = Component::Normal(name::HEADS);
+
    pub const MAIN: Component = Component::Normal(name::MAIN);
+
    pub const MASTER: Component = Component::Normal(name::MASTER);
+
    pub const NAMESPACES: Component = Component::Normal(name::NAMESPACES);
+
    pub const NOTES: Component = Component::Normal(name::NOTES);
+
    pub const ORIGIN: Component = Component::Normal(name::ORIGIN);
+
    pub const REFS: Component = Component::Normal(name::REFS);
+
    pub const REMOTES: Component = Component::Normal(name::REMOTES);
+
    pub const TAGS: Component = Component::Normal(name::TAGS);
+
}
added radicle-git-ext/git-ref-format/core/src/serde.rs
@@ -0,0 +1,150 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::convert::TryFrom;
+

+
use ::serde::{de, Deserialize, Deserializer, Serialize, Serializer};
+

+
use crate::{
+
    refspec::{PatternStr, PatternString},
+
    Namespaced,
+
    Qualified,
+
    RefStr,
+
    RefString,
+
};
+

+
impl<'de: 'a, 'a> Deserialize<'de> for &'a RefStr {
+
    #[inline]
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        Deserialize::deserialize(deserializer)
+
            .and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
+
    }
+
}
+

+
impl<'a> Serialize for &'a RefStr {
+
    #[inline]
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        serializer.serialize_str(self.as_str())
+
    }
+
}
+

+
impl<'de> Deserialize<'de> for RefString {
+
    #[inline]
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        Deserialize::deserialize(deserializer)
+
            .and_then(|x: String| Self::try_from(x).map_err(de::Error::custom))
+
    }
+
}
+

+
impl Serialize for RefString {
+
    #[inline]
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        serializer.serialize_str(self.as_str())
+
    }
+
}
+

+
impl<'de: 'a, 'a> Deserialize<'de> for &'a PatternStr {
+
    #[inline]
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        Deserialize::deserialize(deserializer)
+
            .and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
+
    }
+
}
+

+
impl<'a> Serialize for &'a PatternStr {
+
    #[inline]
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        serializer.serialize_str(self.as_str())
+
    }
+
}
+

+
impl<'de> Deserialize<'de> for PatternString {
+
    #[inline]
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        Deserialize::deserialize(deserializer)
+
            .and_then(|x: String| Self::try_from(x).map_err(de::Error::custom))
+
    }
+
}
+

+
impl Serialize for PatternString {
+
    #[inline]
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        serializer.serialize_str(self.as_str())
+
    }
+
}
+

+
impl<'de> Deserialize<'de> for Qualified<'static> {
+
    #[inline]
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        Deserialize::deserialize(deserializer).and_then(|s: String| {
+
            let s = RefString::try_from(s).map_err(de::Error::custom)?;
+
            s.qualified()
+
                .ok_or_else(|| de::Error::custom("not a qualified ref"))
+
                .map(|q| q.into_owned())
+
        })
+
    }
+
}
+

+
impl Serialize for Qualified<'_> {
+
    #[inline]
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        serializer.serialize_str(self.as_str())
+
    }
+
}
+

+
impl<'de> Deserialize<'de> for Namespaced<'static> {
+
    #[inline]
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        Deserialize::deserialize(deserializer).and_then(|s: String| {
+
            let s = RefString::try_from(s).map_err(de::Error::custom)?;
+
            s.to_namespaced()
+
                .ok_or_else(|| de::Error::custom("not a namespaced ref"))
+
                .map(|ns| ns.into_owned())
+
        })
+
    }
+
}
+

+
impl Serialize for Namespaced<'_> {
+
    #[inline]
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        serializer.serialize_str(self.as_str())
+
    }
+
}
added radicle-git-ext/git-ref-format/macro/CHANGELOG.md
@@ -0,0 +1,5 @@
+
# CHANGELOG
+

+
## Version 0.2.0
+

+
* 2022-11-16 Import Qualified within macro [629191f](https://github.com/radicle-dev/radicle-git/commit/629191f55afe77c00808cfe3d6f35d4cff1f4f73)
added radicle-git-ext/git-ref-format/macro/Cargo.toml
@@ -0,0 +1,22 @@
+
[package]
+
name = "git-ref-format-macro"
+
version = "0.2.0"
+
authors = ["Kim Altintop <kim@eagain.st>"]
+
edition = "2018"
+
license = "GPL-3.0-or-later"
+
description = "Macros for the git-ref-format crate"
+
keywords = ["git", "references"]
+

+
[lib]
+
doctest = false
+
proc-macro = true
+
test = false
+

+
[dependencies]
+
proc-macro-error = "1.0.4"
+
quote = "1"
+
syn = "1"
+

+
[dependencies.git-ref-format-core]
+
version = "0.2.0"
+
path = "../core"
added radicle-git-ext/git-ref-format/macro/src/lib.rs
@@ -0,0 +1,168 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
#[macro_use]
+
extern crate proc_macro_error;
+

+
use std::convert::TryInto;
+

+
use proc_macro::TokenStream;
+
use proc_macro_error::abort;
+
use quote::quote;
+
use syn::{parse_macro_input, LitStr};
+

+
use git_ref_format_core::{refspec::PatternStr, Component, Error, Qualified, RefStr};
+

+
/// Create a [`git_ref_format_core::RefString`] from a string literal.
+
///
+
/// The string is validated at compile time, and an unsafe conversion is
+
/// emitted.
+
#[proc_macro_error]
+
#[proc_macro]
+
pub fn refname(input: TokenStream) -> TokenStream {
+
    let lit = parse_macro_input!(input as LitStr);
+
    let val = lit.value();
+

+
    let parsed: Result<&RefStr, Error> = val.as_str().try_into();
+
    match parsed {
+
        Ok(safe) => {
+
            let safe: &str = safe.as_str();
+
            let expand = quote! {
+
                unsafe {
+
                    use ::std::mem::transmute;
+
                    use ::radicle_git_ext::ref_format::RefString;
+

+
                    transmute::<_, RefString>(#safe.to_owned())
+
                }
+
            };
+
            TokenStream::from(expand)
+
        },
+

+
        Err(e) => {
+
            abort!(lit.span(), "invalid refname literal: {}", e);
+
        },
+
    }
+
}
+

+
/// Create a [`git_ref_format_core::Qualified`] from a string literal.
+
///
+
/// The string is validated at compile time, and an unsafe conversion is
+
/// emitted.
+
#[proc_macro_error]
+
#[proc_macro]
+
pub fn qualified(input: TokenStream) -> TokenStream {
+
    let lit = parse_macro_input!(input as LitStr);
+
    let val = lit.value();
+

+
    let parsed: Result<&RefStr, Error> = val.as_str().try_into();
+
    match parsed {
+
        Ok(name) => {
+
            let qualified: Option<Qualified> = Qualified::from_refstr(name);
+
            match qualified {
+
                Some(safe) => {
+
                    let safe: &str = safe.as_str();
+
                    let expand = quote! {
+
                        unsafe {
+
                            use ::std::{borrow::Cow, mem::transmute};
+
                            use ::radicle_git_ext::ref_format::{Component, RefStr, RefString, Qualified};
+

+
                            let inner: RefString = transmute(#safe.to_owned());
+
                            let cow: Cow<'static, RefStr> = Cow::Owned(inner);
+
                            transmute::<_, Qualified>(cow)
+
                        }
+
                    };
+

+
                    TokenStream::from(expand)
+
                },
+

+
                None => {
+
                    abort!(
+
                        lit.span(),
+
                        "refname is not of the form 'refs/<category>/<name>'"
+
                    );
+
                },
+
            }
+
        },
+

+
        Err(e) => {
+
            abort!(lit.span(), "invalid refname literal: {}", e);
+
        },
+
    }
+
}
+

+
/// Create a [`git_ref_format_core::Component`] from a string literal.
+
///
+
/// The string is validated at compile time, and an unsafe conversion is
+
/// emitted.
+
#[proc_macro_error]
+
#[proc_macro]
+
pub fn component(input: TokenStream) -> TokenStream {
+
    let lit = parse_macro_input!(input as LitStr);
+
    let val = lit.value();
+

+
    let name: Result<&RefStr, Error> = val.as_str().try_into();
+
    match name {
+
        Ok(name) => {
+
            let comp: Option<Component> = name.into();
+
            match comp {
+
                Some(safe) => {
+
                    let safe: &str = safe.as_ref().as_str();
+
                    let expand = quote! {
+
                        unsafe {
+
                            use ::std::{borrow::Cow, mem::transmute};
+
                            use ::radicle_git_ext::ref_format::{Component, RefStr, RefString};
+

+
                            let inner: RefString = transmute(#safe.to_owned());
+
                            let cow: Cow<'static, RefStr> = Cow::Owned(inner);
+
                            transmute::<_, Component>(cow)
+
                        }
+
                    };
+

+
                    TokenStream::from(expand)
+
                },
+

+
                None => {
+
                    abort!(lit.span(), "component contains a '/'");
+
                },
+
            }
+
        },
+

+
        Err(e) => {
+
            abort!(lit.span(), "invalid refname literal: {}", e);
+
        },
+
    }
+
}
+

+
/// Create a [`git_ref_format_core::refspec::PatternString`] from a string
+
/// literal.
+
///
+
/// The string is validated at compile time, and an unsafe conversion is
+
/// emitted.
+
#[proc_macro_error]
+
#[proc_macro]
+
pub fn pattern(input: TokenStream) -> TokenStream {
+
    let lit = parse_macro_input!(input as LitStr);
+
    let val = lit.value();
+

+
    let parsed: Result<&PatternStr, Error> = val.as_str().try_into();
+
    match parsed {
+
        Ok(safe) => {
+
            let safe: &str = safe.as_str();
+
            let expand = quote! {
+
                unsafe {
+
                    use ::std::mem::transmute;
+
                    use ::radicle_git_ext::ref_format::refspec::PatternString;
+

+
                    transmute::<_, PatternString>(#safe.to_owned())
+
                }
+
            };
+
            TokenStream::from(expand)
+
        },
+

+
        Err(e) => {
+
            abort!(lit.span(), "invalid refspec pattern literal: {}", e);
+
        },
+
    }
+
}
added radicle-git-ext/git-ref-format/src/lib.rs
@@ -0,0 +1,157 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
//! Everything you never knew you wanted for handling git ref names.
+
//!
+
//! # Overview
+
//!
+
//! This crate provides a number of types which allow to validate git ref names,
+
//! create new ones which are valid by construction, make assertions about their
+
//! structure, and deconstruct them into their components.
+
//!
+
//! ## Basic Types
+
//!
+
//! The basic types are:
+
//!
+
//! * [`RefStr`]
+
//! * [`RefString`]
+
//!
+
//! They are wrappers around [`str`] and [`String`] respectively, with the
+
//! additional guarantee that they are also valid ref names as per
+
//! [`git-check-ref-format`] (which is also exposed directly as
+
//! [`check_ref_format`]). Both types are referred to as "ref strings".
+
//!
+
//! Note that this implies that ref names must be valid UTF-8, which git itself
+
//! doesn't require.
+
//!
+
//! Ref strings can be iterated over, either yielding `&str` or [`Component`]. A
+
//! [`Component`] is guaranteed to not contain a '/' separator, and can thus
+
//! also be used to conveniently construct known-valid ref strings. The [`lit`]
+
//! module contains a number of types (and `const` values thereof) which can be
+
//! coerced into [`Component`], and thus can be used to construct known-valid
+
//! ref strings.
+
//!
+
//! The [`name`] module also provides a number of constant values of commonly
+
//! used ref strings / components, which are useful for pattern matching.
+
//!
+
//! The `"macro"` feature enables the `refstring!` and `component!` macros,
+
//! which can be convenient to construct compile-time validated [`RefString`]s
+
//! respectively [`Component`]s.
+
//!
+
//! ## Refspec Patterns
+
//!
+
//! The types
+
//!
+
//! * [`refspec::PatternStr`]
+
//! * [`refspec::PatternString`]
+
//!
+
//! guarantee that their values are valid ref strings but additionally _may_
+
//! contain at most one "*" character. It is thus possible to convert a ref
+
//! string to a refspec pattern, but not the other way round. Refspec patterns
+
//! are commonly used for mapping remote to local refs (cf. [`git-fetch`]).
+
//!
+
//! The `"macro"` feature enables the `refspec::pattern!` macro, which
+
//! constructs a compile-time validated [`refspec::PatternString`].
+
//!
+
//! ## Structured Ref Strings
+
//!
+
//! Ref strings may be [`Qualified`], which essentially means that they start
+
//! with "refs/". [`Qualified`] ref string also require at least three
+
//! components (eg. "refs/heads/main"), which makes it easier to deal with
+
//! common naming conventions.
+
//!
+
//! [`Qualified`] refs may be [`Namespaced`], or can be given a namespace
+
//! (namespaces can be nested). [`Namespaced`] refs are also [`Qualified`], and
+
//! can have their namespace(s) stripped.
+
//!
+
//! # On Git Ref Name Conventions
+
//!
+
//! Git references are essentially path names pointing to their traditional
+
//! storage location in a the repository (`$GIT_DIR/refs`). Unlike (UNIX) file
+
//! paths, they are subject to a few restrictions, as described in
+
//! [`git-check-ref-format`].
+
//!
+
//! On top of that, there are a number of conventions around the hierarchical
+
//! naming, _some_ of which are treated specially by tools such as the `git`
+
//! CLI. For example:
+
//!
+
//! * `refs/heads/..` are also called "branches".
+
//!
+
//!   Omitting the "refs/heads/" prefix is typically accepted. Such a branch
+
//!   name is also referred to as a "shorthand" ref.
+
//!
+
//! * `refs/tags/..` are assumed to contain tags.
+
//!
+
//!   `git` treats tags specially, specifically it insists that they be globally
+
//!   unique across all   copies of the repository.
+
//!
+
//! * `refs/remotes/../..` is where "remote tracking branches" are stored.
+
//!
+
//!   In `git`, the first element after "remotes" is considered the name of the
+
//!   [remote][git-remote] (as it appears in the config file), while everything
+
//!   after that is considered a shorthand branch. Note, however, that the
+
//!   remote name may itself contain '/' separators, so it is not generally
+
//!   possible to extract  the branch name without access to the config.
+
//!
+
//! * `refs/namespaces/..` is hidden unless [`gitnamespaces`] are in effect.
+
//!
+
//!   The structure of namespaces is recursive: they contain full refs, which
+
//!   can themselves be namespaces (eg.
+
//!   `refs/namespaces/a/refs/namespaces/b/refs/heads/branch`). Note that,
+
//!   unlike remote names,  namespace names can **not** contain forward slashes
+
//!   but there is no tooling which would enforce that.
+
//!
+
//! There are also other such ref hierachies `git` knows about, and this crate
+
//! doesn't attempt to cover all of them. More importantly, `git` does not
+
//! impose any restrictions on ref hierarchies: as long as they don't collide
+
//! with convential ones, applications can introduce any hierchies they want.
+
//!
+
//! This restricts the transformations between conventional refs which can be
+
//! made without additional information besides the ref name: for example, it is
+
//! not generally possible to turn a remote tracking branch into a branch (or a
+
//! shorthand) without knowning about all possible remote names.
+
//!
+
//! Therefore, this crate doesn't attempt to interpret all possible semantics
+
//! associated with refs, and instead tries to make it easy for library
+
//! consumers to do so.
+
//!
+
//! [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
+
//! [`git-fetch`]: https://git-scm.com/docs/git-fetch
+
//! [git-remote]: https://git-scm.com/docs/git-remote
+
//! [`gitnamespaces`]: https://git-scm.com/docs/gitnamespaces
+
#[cfg(feature = "percent-encoding")]
+
pub use git_ref_format_core::PercentEncode;
+
pub use git_ref_format_core::{
+
    check_ref_format,
+
    lit,
+
    name::component,
+
    Component,
+
    DuplicateGlob,
+
    Error,
+
    Namespaced,
+
    Options,
+
    Qualified,
+
    RefStr,
+
    RefString,
+
};
+

+
pub mod name {
+
    pub use git_ref_format_core::name::*;
+

+
    #[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
+
    pub use git_ref_format_macro::component;
+
}
+

+
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
+
pub use git_ref_format_macro::qualified;
+
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
+
pub use git_ref_format_macro::refname;
+

+
pub mod refspec {
+
    pub use git_ref_format_core::refspec::*;
+

+
    #[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
+
    pub use git_ref_format_macro::pattern;
+
}
added radicle-git-ext/git-ref-format/t/Cargo.toml
@@ -0,0 +1,29 @@
+
[package]
+
name = "git-ref-format-test"
+
version = "0.1.0"
+
edition = "2021"
+
license = "GPL-3.0-or-later"
+

+
publish = false
+

+
[lib]
+
doctest = false
+
test = true
+
doc = false
+

+
[features]
+
test = []
+

+
[dependencies]
+
proptest = "1"
+

+
[dev-dependencies]
+
assert_matches = "1.5"
+
serde_json = "1"
+

+
[dev-dependencies.git-ref-format]
+
path = ".."
+
features = ["macro", "minicbor", "serde"]
+

+
[dev-dependencies.test-helpers]
+
path = "../../test/test-helpers"
modified radicle-git-ext/src/lib.rs
@@ -20,3 +20,5 @@ pub use oid::*;
pub use revwalk::*;
pub use transport::*;
pub use tree::Tree;
+

+
pub use git_ref_format as ref_format;
added radicle-git-ext/t/src/git_ref_format.rs
@@ -0,0 +1,11 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
#[cfg(any(test, feature = "test"))]
+
pub mod gen;
+

+
#[cfg(test)]
+
pub mod properties;
+

+
#[cfg(test)]
+
pub mod tests;
added radicle-git-ext/t/src/git_ref_format/gen.rs
@@ -0,0 +1,101 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use proptest::prelude::*;
+

+
/// Any unicode "word" is trivially a valid refname.
+
pub fn trivial() -> impl Strategy<Value = String> {
+
    "\\w+"
+
}
+

+
pub fn valid() -> impl Strategy<Value = String> {
+
    prop::collection::vec(trivial(), 1..20).prop_map(|xs| xs.join("/"))
+
}
+

+
pub fn invalid_char() -> impl Strategy<Value = char> {
+
    prop_oneof![
+
        Just('\0'),
+
        Just('\\'),
+
        Just('~'),
+
        Just('^'),
+
        Just(':'),
+
        Just('?'),
+
        Just('[')
+
    ]
+
}
+

+
pub fn with_invalid_char() -> impl Strategy<Value = String> {
+
    ("\\w*", invalid_char(), "\\w*").prop_map(|(mut pre, invalid, suf)| {
+
        pre.push(invalid);
+
        pre.push_str(&suf);
+
        pre
+
    })
+
}
+

+
pub fn ends_with_dot_lock() -> impl Strategy<Value = String> {
+
    "\\w*\\.lock"
+
}
+

+
pub fn with_double_dot() -> impl Strategy<Value = String> {
+
    "\\w*\\.\\.\\w*"
+
}
+

+
pub fn starts_with_dot() -> impl Strategy<Value = String> {
+
    "\\.\\w*"
+
}
+

+
pub fn ends_with_dot() -> impl Strategy<Value = String> {
+
    "\\w+\\."
+
}
+

+
pub fn with_control_char() -> impl Strategy<Value = String> {
+
    "\\w*[\x01-\x1F\x7F]+\\w*"
+
}
+

+
pub fn with_space() -> impl Strategy<Value = String> {
+
    "\\w* +\\w*"
+
}
+

+
pub fn with_consecutive_slashes() -> impl Strategy<Value = String> {
+
    "\\w*//\\w*"
+
}
+

+
pub fn with_glob() -> impl Strategy<Value = String> {
+
    "\\w*\\*\\w*"
+
}
+

+
pub fn multi_glob() -> impl Strategy<Value = String> {
+
    (
+
        prop::collection::vec(with_glob(), 2..5),
+
        prop::collection::vec(trivial(), 0..5),
+
    )
+
        .prop_map(|(mut globs, mut valids)| {
+
            globs.append(&mut valids);
+
            globs
+
        })
+
        .prop_shuffle()
+
        .prop_map(|xs| xs.join("/"))
+
}
+

+
pub fn invalid() -> impl Strategy<Value = String> {
+
    fn path(s: impl Strategy<Value = String>) -> impl Strategy<Value = String> {
+
        prop::collection::vec(s, 1..20).prop_map(|xs| xs.join("/"))
+
    }
+

+
    prop_oneof![
+
        Just(String::from("")),
+
        Just(String::from("@")),
+
        path(with_invalid_char()),
+
        path(ends_with_dot_lock()),
+
        path(with_double_dot()),
+
        path(starts_with_dot()),
+
        path(ends_with_dot()),
+
        path(with_control_char()),
+
        path(with_space()),
+
        path(with_consecutive_slashes()),
+
        path(trivial()).prop_map(|mut p| {
+
            p.push('/');
+
            p
+
        }),
+
    ]
+
}
added radicle-git-ext/t/src/git_ref_format/properties.rs
@@ -0,0 +1,25 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use proptest::prelude::*;
+
use radicle_git_ext::ref_format::{check_ref_format, Error, Options};
+

+
use crate::git_ref_format::gen;
+

+
mod name;
+
mod pattern;
+

+
proptest! {
+
    #[test]
+
    fn disallow_onelevel(input in gen::trivial(), allow_pattern in any::<bool>()) {
+
        assert_matches!(
+
            check_ref_format(Options {
+
                    allow_onelevel: false,
+
                    allow_pattern,
+
                },
+
                &input
+
            ),
+
            Err(Error::OneLevel)
+
        )
+
    }
+
}
added radicle-git-ext/t/src/git_ref_format/properties/name.rs
@@ -0,0 +1,100 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use std::convert::TryFrom;
+

+
use proptest::prelude::*;
+
use radicle_git_ext::ref_format::{name, refname, Error, RefStr, RefString};
+
use test_helpers::roundtrip;
+

+
use crate::git_ref_format::gen;
+

+
proptest! {
+
    #[test]
+
    fn valid(input in gen::valid()) {
+
        assert_eq!(input.as_str(), RefStr::try_from_str(&input).unwrap().as_str())
+
    }
+

+
    #[test]
+
    fn invalid_char(input in gen::with_invalid_char()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::InvalidChar(_)))
+
    }
+

+
    #[test]
+
    fn dot_lock(input in gen::ends_with_dot_lock()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::DotLock))
+
    }
+

+
    #[test]
+
    fn double_dot(input in gen::with_double_dot()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::DotDot))
+
    }
+

+
    #[test]
+
    fn starts_dot(input in gen::starts_with_dot()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::StartsDot))
+
    }
+

+
    #[test]
+
    fn ends_dot(input in gen::ends_with_dot()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::EndsDot))
+
    }
+

+
    #[test]
+
    fn control_char(input in gen::with_control_char()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::Control))
+
    }
+

+
    #[test]
+
    fn space(input in gen::with_space()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::Space))
+
    }
+

+
    #[test]
+
    fn consecutive_slashes(input in gen::with_consecutive_slashes()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::Slash))
+
    }
+

+
    #[test]
+
    fn glob(input in gen::with_glob()) {
+
        assert_matches!(RefString::try_from(input), Err(Error::InvalidChar('*')))
+
    }
+

+
    #[test]
+
    fn invalid(input in gen::invalid()) {
+
        assert_matches!(RefString::try_from(input), Err(_))
+
    }
+

+
    #[test]
+
    fn roundtrip_components(input in gen::valid()) {
+
        assert_eq!(
+
            input.as_str(),
+
            RefStr::try_from_str(&input).unwrap().components().collect::<RefString>().as_str()
+
        )
+
    }
+

+
    #[test]
+
    fn json(input in gen::valid()) {
+
        let input = RefString::try_from(input).unwrap();
+
        roundtrip::json(input.clone());
+
        let qualified = refname!("refs/heads").and(input).qualified().unwrap().into_owned();
+
        roundtrip::json(qualified.clone());
+
        let namespaced = qualified.with_namespace(name::component!("foo"));
+
        roundtrip::json(namespaced);
+
    }
+

+
    #[test]
+
    fn json_value(input in gen::valid()) {
+
        let input = RefString::try_from(input).unwrap();
+
        roundtrip::json_value(input.clone());
+
        let qualified = refname!("refs/heads").and(input).qualified().unwrap().into_owned();
+
        roundtrip::json_value(qualified.clone());
+
        let namespaced = qualified.with_namespace(name::component!("foo"));
+
        roundtrip::json_value(namespaced);
+
    }
+

+
    #[test]
+
    fn cbor(input in gen::valid()) {
+
        roundtrip::cbor(RefString::try_from(input).unwrap())
+
    }
+
}
added radicle-git-ext/t/src/git_ref_format/properties/pattern.rs
@@ -0,0 +1,60 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use std::convert::TryFrom;
+

+
use proptest::prelude::*;
+
use radicle_git_ext::ref_format::{refspec, Error};
+
use test_helpers::roundtrip;
+

+
use crate::git_ref_format::gen;
+

+
proptest! {
+
    #[test]
+
    fn valid(input in gen::with_glob()) {
+
        assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
+
    }
+

+
    #[test]
+
    fn refname_is_pattern(input in gen::valid()) {
+
        assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
+
    }
+

+
    #[test]
+
    fn no_more_than_one_star(input in gen::multi_glob()) {
+
        assert_matches!(refspec::PatternString::try_from(input), Err(Error::Pattern))
+
    }
+

+
    #[test]
+
    fn invalid_refname_is_invalid_pattern(input in gen::invalid()) {
+
        assert_matches!(refspec::PatternString::try_from(input), Err(_))
+
    }
+

+
    #[test]
+
    fn roundtrip_components(input in gen::with_glob()) {
+
        assert_eq!(
+
            input.as_str(),
+
            refspec::PatternStr::try_from_str(&input)
+
                .unwrap()
+
                .components()
+
                .collect::<Result<refspec::PatternString, _>>()
+
                .unwrap()
+
                .as_str()
+
        )
+
    }
+

+
    #[test]
+
    fn json(input in gen::with_glob()) {
+
        roundtrip::json(refspec::PatternString::try_from(input).unwrap())
+
    }
+

+
    #[test]
+
    fn json_value(input in gen::with_glob()) {
+
        roundtrip::json_value(refspec::PatternString::try_from(input).unwrap())
+
    }
+

+
    #[test]
+
    fn cbor(input in gen::with_glob()) {
+
        roundtrip::cbor(refspec::PatternString::try_from(input).unwrap())
+
    }
+
}
added radicle-git-ext/t/src/git_ref_format/tests.rs
@@ -0,0 +1,372 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use radicle_git_ext::ref_format::{
+
    component,
+
    name,
+
    qualified,
+
    refname,
+
    refspec,
+
    Error,
+
    Qualified,
+
    RefStr,
+
    RefString,
+
};
+

+
#[test]
+
fn refname_macro_works() {
+
    assert_eq!("refs/heads/main", refname!("refs/heads/main").as_str())
+
}
+

+
#[test]
+
fn qualified_macro_works() {
+
    assert_eq!("refs/heads/main", qualified!("refs/heads/main").as_str())
+
}
+

+
#[test]
+
fn component_macro_works() {
+
    assert_eq!("self", name::component!("self").as_str())
+
}
+

+
#[test]
+
fn pattern_macro_works() {
+
    assert_eq!("refs/heads/*", refspec::pattern!("refs/heads/*").as_str())
+
}
+

+
#[test]
+
fn empty() {
+
    assert_matches!(RefStr::try_from_str(""), Err(Error::Empty));
+
    assert_matches!(RefString::try_from("".to_owned()), Err(Error::Empty));
+
}
+

+
#[test]
+
fn join() {
+
    let s = name::REFS.join(name::HEADS);
+
    let t = s.join(name::MAIN);
+
    assert_eq!("refs/heads", s.as_str());
+
    assert_eq!("refs/heads/main", t.as_str());
+
}
+

+
#[test]
+
fn join_and() {
+
    assert_eq!(
+
        "refs/heads/this/that",
+
        name::REFS
+
            .join(name::HEADS)
+
            .and(refname!("this"))
+
            .and(refname!("that"))
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn strip_prefix() {
+
    assert_eq!(
+
        "main",
+
        name::REFS_HEADS_MAIN
+
            .strip_prefix(refname!("refs/heads"))
+
            .unwrap()
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn strip_prefix_not_prefix() {
+
    assert!(name::REFS_HEADS_MAIN
+
        .strip_prefix(refname!("refs/tags"))
+
        .is_none())
+
}
+

+
#[test]
+
fn qualified() {
+
    assert_eq!(
+
        "refs/heads/main",
+
        name::REFS_HEADS_MAIN.qualified().unwrap().as_str()
+
    )
+
}
+

+
#[test]
+
fn qualified_tag() {
+
    assert_eq!(
+
        "refs/tags/v1",
+
        refname!("refs/tags/v1").qualified().unwrap().as_str()
+
    )
+
}
+

+
#[test]
+
fn qualified_remote_tracking() {
+
    assert_eq!(
+
        "refs/remotes/origin/master",
+
        refname!("refs/remotes/origin/master")
+
            .qualified()
+
            .unwrap()
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn not_qualified() {
+
    assert!(name::MAIN.qualified().is_none())
+
}
+

+
#[test]
+
fn qualified_from_components() {
+
    assert_eq!(
+
        "refs/heads/main",
+
        Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
+
    )
+
}
+

+
#[test]
+
fn qualified_from_components_with_iter() {
+
    assert_eq!(
+
        "refs/heads/foo/bar/baz",
+
        Qualified::from_components(
+
            component::HEADS,
+
            name::component!("foo"),
+
            [name::component!("bar"), name::component!("baz")]
+
        )
+
        .as_str()
+
    )
+
}
+

+
#[test]
+
fn qualified_from_components_non_empty_iter() {
+
    let q = Qualified::from_components(component::HEADS, component::MAIN, None);
+
    let (refs, heads, main, mut empty) = q.non_empty_iter();
+
    assert!(empty.next().is_none());
+
    assert_eq!(("refs", "heads", "main"), (refs, heads, main))
+
}
+

+
#[test]
+
fn qualified_from_components_non_empty_components() {
+
    let q = Qualified::from_components(component::HEADS, component::MAIN, Some(component::MASTER));
+
    let (refs, heads, main, mut master) = q.non_empty_components();
+
    assert_eq!(
+
        (
+
            component::REFS,
+
            component::HEADS,
+
            component::MAIN,
+
            component::MASTER
+
        ),
+
        (refs, heads, main, master.next().unwrap())
+
    )
+
}
+

+
#[test]
+
fn namespaced() {
+
    assert_eq!(
+
        "refs/namespaces/foo/refs/heads/main",
+
        refname!("refs/namespaces/foo/refs/heads/main")
+
            .to_namespaced()
+
            .unwrap()
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn not_namespaced() {
+
    assert!(name::REFS_HEADS_MAIN.to_namespaced().is_none())
+
}
+

+
#[test]
+
fn not_namespaced_because_not_qualified() {
+
    assert!(refname!("refs/namespaces/foo/banana")
+
        .to_namespaced()
+
        .is_none())
+
}
+

+
#[test]
+
fn strip_namespace() {
+
    assert_eq!(
+
        "refs/rad/id",
+
        refname!("refs/namespaces/xyz/refs/rad/id")
+
            .to_namespaced()
+
            .unwrap()
+
            .strip_namespace()
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn strip_nested_namespaces() {
+
    let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
+
    let namespaced = full.to_namespaced().unwrap();
+
    let strip_first = namespaced.strip_namespace();
+
    let nested = strip_first.to_namespaced().unwrap();
+
    let strip_second = nested.strip_namespace();
+

+
    assert_eq!("a", namespaced.namespace().as_str());
+
    assert_eq!("b", nested.namespace().as_str());
+
    assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
+
    assert_eq!("refs/heads/main", strip_second.as_str());
+
}
+

+
#[test]
+
fn with_namespace() {
+
    assert_eq!(
+
        "refs/namespaces/foo/refs/heads/main",
+
        name::REFS_HEADS_MAIN
+
            .qualified()
+
            .unwrap()
+
            .with_namespace(refname!("foo").head())
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn iter() {
+
    assert_eq!(
+
        vec!["refs", "heads", "main"],
+
        name::REFS_HEADS_MAIN.iter().collect::<Vec<_>>()
+
    )
+
}
+

+
#[test]
+
fn push_pop() {
+
    let mut s = name::REFS.to_owned();
+
    s.push(name::HEADS);
+
    s.push(name::MAIN);
+

+
    assert_eq!("refs/heads/main", s.as_str());
+
    assert!(s.pop());
+
    assert!(s.pop());
+
    assert_eq!("refs", s.as_str());
+
    assert!(!s.pop());
+
    assert_eq!("refs", s.as_str());
+
}
+

+
#[test]
+
fn to_pattern() {
+
    assert_eq!(
+
        "refs/heads/*",
+
        refname!("refs/heads")
+
            .to_pattern(refspec::pattern!("*"))
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn with_pattern() {
+
    assert_eq!(
+
        "refs/heads/*",
+
        refname!("refs/heads").with_pattern(refspec::STAR).as_str()
+
    )
+
}
+

+
#[test]
+
fn with_pattern_and() {
+
    assert_eq!(
+
        "refs/*/heads",
+
        refname!("refs")
+
            .with_pattern(refspec::STAR)
+
            .and(name::HEADS)
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn pattern_is_qualified() {
+
    assert!(refspec::pattern!("refs/heads/*").qualified().is_some())
+
}
+

+
#[test]
+
fn pattern_is_not_qualified() {
+
    assert!(refspec::pattern!("heads/*").qualified().is_none())
+
}
+

+
#[test]
+
fn pattern_with_namespaced() {
+
    assert_eq!(
+
        "refs/namespaces/a/refs/heads/*",
+
        refspec::pattern!("refs/heads/*")
+
            .qualified()
+
            .unwrap()
+
            .with_namespace(refspec::Component::Normal(refname!("a").as_ref()))
+
            .unwrap()
+
            .as_str(),
+
    )
+
}
+

+
#[test]
+
fn pattern_not_namespaced_because_not_qualified() {
+
    assert!(refspec::pattern!("refs/namespaces/foo/*")
+
        .to_namespaced()
+
        .is_none())
+
}
+

+
#[test]
+
fn pattern_namespaced() {
+
    assert!(refspec::pattern!("refs/namespaces/a/refs/foo/*")
+
        .to_namespaced()
+
        .is_some())
+
}
+

+
#[test]
+
fn pattern_strip_namespace() {
+
    assert_eq!(
+
        "refs/rad/*",
+
        refspec::pattern!("refs/namespaces/xyz/refs/rad/*")
+
            .to_namespaced()
+
            .unwrap()
+
            .strip_namespace()
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn pattern_strip_nested_namespaces() {
+
    let full = refspec::pattern!("refs/namespaces/a/refs/namespaces/b/refs/heads/*");
+
    let namespaced = full.to_namespaced().unwrap();
+
    let strip_first = namespaced.strip_namespace();
+
    let nested = strip_first.to_namespaced().unwrap();
+
    let strip_second = nested.strip_namespace();
+

+
    assert_eq!("a", namespaced.namespace().as_str());
+
    assert_eq!("b", nested.namespace().as_str());
+
    assert_eq!("refs/namespaces/b/refs/heads/*", strip_first.as_str());
+
    assert_eq!("refs/heads/*", strip_second.as_str());
+
}
+

+
#[test]
+
fn pattern_qualified_with_namespace() {
+
    assert_eq!(
+
        "refs/namespaces/a/refs/heads/*",
+
        refspec::pattern!("refs/heads/*")
+
            .qualified()
+
            .unwrap()
+
            .with_namespace(refspec::Component::Normal(refname!("a").as_ref()))
+
            .unwrap()
+
            .as_str(),
+
    )
+
}
+

+
#[test]
+
fn collect() {
+
    assert_eq!(
+
        "refs/heads/main",
+
        IntoIterator::into_iter([name::REFS, name::HEADS, name::MAIN])
+
            .collect::<RefString>()
+
            .as_str()
+
    )
+
}
+

+
#[test]
+
fn collect_components() {
+
    let a = name::REFS_HEADS_MAIN.to_owned();
+
    let b = a.components().collect();
+
    assert_eq!(a, b)
+
}
+

+
#[test]
+
fn collect_pattern_duplicate_glob() {
+
    assert_matches!(
+
        IntoIterator::into_iter([
+
            refspec::Component::Normal(name::REFS),
+
            refspec::Component::Glob(None),
+
            refspec::Component::Glob(Some(refspec::pattern!("fo*").as_ref()))
+
        ])
+
        .collect::<Result<_, _>>(),
+
        Err(refspec::DuplicateGlob)
+
    )
+
}
modified radicle-git-ext/t/src/lib.rs
@@ -1,8 +1,14 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
+
#[cfg(test)]
+
#[macro_use]
+
extern crate assert_matches;

#[cfg(any(test, feature = "test"))]
pub mod gen;

#[cfg(test)]
mod commit;
+

+
#[cfg(any(test, feature = "test"))]
+
pub mod git_ref_format;
modified radicle-surf/Cargo.toml
@@ -37,11 +37,6 @@ version = "0.16.1"
default-features = false
features = ["vendored-libgit2"]

-
[dependencies.git-ref-format]
-
version = "0.2.0"
-
path = "../git-ref-format"
-
features = ["macro", "serde"]
-

[dependencies.radicle-git-ext]
version = "0.2.0"
path = "../radicle-git-ext"
modified radicle-surf/src/branch.rs
@@ -3,7 +3,7 @@ use std::{
    str::{self, FromStr},
};

-
use git_ref_format::{component, lit, Component, Qualified, RefStr, RefString};
+
use git_ext::ref_format::{component, lit, Component, Qualified, RefStr, RefString};

use crate::refs::refstr_join;

@@ -295,7 +295,7 @@ impl FromStr for Remote {
}

pub mod error {
-
    use git_ref_format::RefString;
+
    use radicle_git_ext::ref_format::{self, RefString};
    use thiserror::Error;

    #[derive(Debug, Error)]
@@ -305,7 +305,7 @@ pub mod error {
        #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
        NotQualified(String),
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
+
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] std::str::Utf8Error),
    }
@@ -317,7 +317,7 @@ pub mod error {
        #[error("the refname '{0}' did not begin with 'refs/heads'")]
        NotQualified(String),
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
+
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] std::str::Utf8Error),
    }
@@ -329,7 +329,7 @@ pub mod error {
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
        NotRemotes(RefString),
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
+
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] std::str::Utf8Error),
    }
modified radicle-surf/src/error.rs
@@ -44,7 +44,7 @@ pub enum Error {
    #[error(transparent)]
    Namespace(#[from] namespace::Error),
    #[error(transparent)]
-
    RefFormat(#[from] git_ref_format::Error),
+
    RefFormat(#[from] git_ext::ref_format::Error),
    #[error(transparent)]
    Revision(Box<dyn std::error::Error + Send + Sync + 'static>),
    #[error(transparent)]
modified radicle-surf/src/glob.rs
@@ -17,7 +17,8 @@

use std::marker::PhantomData;

-
use git_ref_format::{
+
use git_ext::ref_format::{
+
    self,
    refname,
    refspec::{self, PatternString, QualifiedPattern},
    Qualified,
@@ -31,7 +32,7 @@ use crate::{Branch, Local, Namespace, Remote, Tag};
#[derive(Debug, Error)]
pub enum Error {
    #[error(transparent)]
-
    RefFormat(#[from] git_ref_format::Error),
+
    RefFormat(#[from] ref_format::Error),
}

/// A collection of globs for a git reference type.
modified radicle-surf/src/lib.rs
@@ -32,8 +32,10 @@
//!
//! [serde]: https://crates.io/crates/serde

+
extern crate radicle_git_ext as git_ext;
+

/// Re-exports.
-
pub use git_ref_format;
+
pub use radicle_git_ext::ref_format;

/// Represents an object id in Git. Re-exported from `radicle-git-ext`.
pub type Oid = radicle_git_ext::Oid;
modified radicle-surf/src/namespace.rs
@@ -21,7 +21,8 @@ use std::{
    str::{self, FromStr},
};

-
use git_ref_format::{
+
use git_ext::ref_format::{
+
    self,
    refspec::{NamespacedPattern, PatternString, QualifiedPattern},
    Component,
    Namespaced,
@@ -30,7 +31,6 @@ use git_ref_format::{
    RefString,
};
use nonempty::NonEmpty;
-
pub use radicle_git_ext::Oid;
use thiserror::Error;

#[derive(Debug, Error)]
@@ -40,7 +40,7 @@ pub enum Error {
    #[error("namespaces must not be empty")]
    EmptyNamespace,
    #[error(transparent)]
-
    RefFormat(#[from] git_ref_format::Error),
+
    RefFormat(#[from] ref_format::Error),
    #[error(transparent)]
    Utf8(#[from] str::Utf8Error),
}
modified radicle-surf/src/refs.rs
@@ -6,7 +6,7 @@ use std::{
    convert::TryFrom as _,
};

-
use git_ref_format::{lit, name::Components, Component, Qualified, RefString};
+
use git_ext::ref_format::{self, lit, name::Components, Component, Qualified, RefString};

use crate::{tag, Branch, Namespace, Tag};

@@ -187,7 +187,7 @@ impl<'a> Iterator for Categories<'a> {
                    Some(res) => {
                        return Some(res.map_err(error::Category::from).and_then(|r| {
                            let name = std::str::from_utf8(r.name_bytes())?;
-
                            let name = git_ref_format::RefStr::try_from_str(name)?;
+
                            let name = ref_format::RefStr::try_from_str(name)?;
                            let name = name.qualified().ok_or_else(|| {
                                error::Category::NotQualified(name.to_ref_string())
                            })?;
@@ -207,7 +207,7 @@ impl<'a> Iterator for Categories<'a> {
pub mod error {
    use std::str;

-
    use git_ref_format::RefString;
+
    use radicle_git_ext::ref_format::{self, RefString};
    use thiserror::Error;

    use crate::{branch, tag};
@@ -227,7 +227,7 @@ pub mod error {
        #[error("the reference '{0}' was expected to be qualified, i.e. 'refs/<category>/<path>'")]
        NotQualified(RefString),
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
+
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }
modified radicle-surf/src/repo.rs
@@ -22,8 +22,10 @@ use std::{
    str,
};

-
use git_ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString};
-
use radicle_git_ext::Oid;
+
use git_ext::{
+
    ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString},
+
    Oid,
+
};

use crate::{
    blob::{Blob, BlobRef},
modified radicle-surf/src/revision.rs
@@ -17,8 +17,10 @@

use std::{convert::Infallible, str::FromStr};

-
use git_ref_format::{Qualified, RefString};
-
use radicle_git_ext::Oid;
+
use git_ext::{
+
    ref_format::{Qualified, RefString},
+
    Oid,
+
};

use crate::{Branch, Commit, Error, Repository, Tag};

modified radicle-surf/src/tag.rs
@@ -1,7 +1,9 @@
use std::{convert::TryFrom, str};

-
use git_ref_format::{component, lit, Qualified, RefStr, RefString};
-
use radicle_git_ext::Oid;
+
use git_ext::{
+
    ref_format::{component, lit, Qualified, RefStr, RefString},
+
    Oid,
+
};

use crate::{refs::refstr_join, Author};

@@ -60,13 +62,13 @@ impl Tag {
pub mod error {
    use std::str;

-
    use git_ref_format::RefString;
+
    use radicle_git_ext::ref_format::{self, RefString};
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum FromTag {
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
+
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }
@@ -82,7 +84,7 @@ pub mod error {
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
        NotTag(RefString),
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
+
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }
modified radicle-surf/t/Cargo.toml
@@ -23,17 +23,13 @@ version = "0.16.1"
default-features = false
features = ["vendored-libgit2"]

-
[dev-dependencies.git-ref-format]
-
path = "../../git-ref-format"
-
features = ["macro"]
-

-
[dev-dependencies.git-ref-format-test]
-
path = "../../git-ref-format/t"
-
features = ["test"]
-

[dev-dependencies.radicle-git-ext]
path = "../../radicle-git-ext"

+
[dev-dependencies.radicle-git-ext-test]
+
path = "../../radicle-git-ext/t"
+
features = ["test"]
+

[dev-dependencies.radicle-surf]
path = ".."
features = ["serde"]
modified radicle-surf/t/src/branch.rs
@@ -1,9 +1,8 @@
-
use git_ref_format::{RefStr, RefString};
-
use git_ref_format_test::gen;
use proptest::prelude::*;
-
use test_helpers::roundtrip;
-

+
use radicle_git_ext::ref_format::{RefStr, RefString};
+
use radicle_git_ext_test::git_ref_format::gen;
use radicle_surf::Branch;
+
use test_helpers::roundtrip;

proptest! {
    #[test]
modified radicle-surf/t/src/code_browsing.rs
@@ -1,6 +1,6 @@
use std::path::Path;

-
use git_ref_format::refname;
+
use radicle_git_ext::ref_format::refname;
use radicle_surf::{
    fs::{self, Directory},
    Branch,
modified radicle-surf/t/src/diff.rs
@@ -1,6 +1,5 @@
-
use git_ref_format::refname;
use pretty_assertions::assert_eq;
-
use radicle_git_ext::Oid;
+
use radicle_git_ext::{ref_format::refname, Oid};
use radicle_surf::{
    diff::{
        Added,
modified radicle-surf/t/src/file_system.rs
@@ -4,7 +4,7 @@
//! Unit tests for radicle_surf::file_system

mod directory {
-
    use git_ref_format::refname;
+
    use radicle_git_ext::ref_format::refname;
    use radicle_surf::{
        fs::{self, Entry},
        Branch,
modified radicle-surf/t/src/last_commit.rs
@@ -1,7 +1,6 @@
use std::{path::PathBuf, str::FromStr};

-
use git_ref_format::refname;
-
use radicle_git_ext::Oid;
+
use radicle_git_ext::{ref_format::refname, Oid};
use radicle_surf::{Branch, Repository};

use super::GIT_PLATINUM;
modified radicle-surf/t/src/namespace.rs
@@ -1,5 +1,5 @@
-
use git_ref_format::{name::component, refname, refspec};
use pretty_assertions::{assert_eq, assert_ne};
+
use radicle_git_ext::ref_format::{name::component, refname, refspec};
use radicle_surf::{Branch, Error, Glob, Repository};

use super::GIT_PLATINUM;
modified radicle-surf/t/src/reference.rs
@@ -1,4 +1,4 @@
-
use git_ref_format::refspec;
+
use radicle_git_ext::ref_format::refspec;
use radicle_surf::{Glob, Repository};

use super::GIT_PLATINUM;
modified radicle-surf/t/src/rev.rs
@@ -1,6 +1,6 @@
use std::str::FromStr;

-
use git_ref_format::{name::component, refname};
+
use radicle_git_ext::ref_format::{name::component, refname};
use radicle_surf::{Branch, Error, Oid, Repository};

use super::GIT_PLATINUM;
modified radicle-surf/t/src/source.rs
@@ -1,6 +1,6 @@
use std::path::PathBuf;

-
use git_ref_format::refname;
+
use radicle_git_ext::ref_format::refname;
use radicle_surf::{Branch, Glob, Repository};
use serde_json::json;

modified radicle-surf/t/src/submodule.rs
@@ -2,7 +2,7 @@
#[test]
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
fn test_submodule_failure() {
-
    use git_ref_format::refname;
+
    use radicle_git_ext::ref_format::refname;
    use radicle_surf::{Branch, Repository};

    let repo = Repository::discover(".").unwrap();
modified radicle-surf/t/src/threading.rs
@@ -1,6 +1,6 @@
use std::sync::{Mutex, MutexGuard};

-
use git_ref_format::{name::component, refname};
+
use radicle_git_ext::ref_format::{name::component, refname};
use radicle_surf::{Branch, Error, Glob, Repository};

use super::GIT_PLATINUM;
modified test/Cargo.toml
@@ -19,10 +19,6 @@ features = ["test"]
path = "../radicle-git-ext/t"
features = ["test"]

-
[dev-dependencies.git-ref-format-test]
-
path = "../git-ref-format/t"
-
features = ["test"]
-

[dev-dependencies.git-trailers-test]
path = "../git-trailers/t"
features = ["test"]
@@ -38,4 +34,3 @@ features = ["test"]
[dev-dependencies.git-storage-test]
path = "../git-storage/t"
features = ["test"]
-