diff --git a/crates/radicle-git-ref-format/src/cbor.rs b/crates/radicle-git-ref-format/src/cbor.rs new file mode 100644 index 000000000..c45357797 --- /dev/null +++ b/crates/radicle-git-ref-format/src/cbor.rs @@ -0,0 +1,106 @@ +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 { + d.str() + .and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e)))) + } +} + +impl Encode for &RefStr { + #[inline] + fn encode(&self, e: &mut Encoder) -> Result<(), encode::Error> { + e.str(self.as_str())?; + Ok(()) + } +} + +impl<'de> Decode<'de> for RefString { + #[inline] + fn decode(d: &mut Decoder<'de>) -> Result { + Decode::decode(d).map(|s: &RefStr| s.to_owned()) + } +} + +impl Encode for RefString { + #[inline] + fn encode(&self, e: &mut Encoder) -> Result<(), encode::Error> { + self.as_refstr().encode(e) + } +} + +impl<'de: 'a, 'a> Decode<'de> for &'a PatternStr { + #[inline] + fn decode(d: &mut Decoder<'de>) -> Result { + d.str() + .and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e)))) + } +} + +impl Encode for &PatternStr { + #[inline] + fn encode(&self, e: &mut Encoder) -> Result<(), encode::Error> { + e.str(self.as_str())?; + Ok(()) + } +} + +impl<'de> Decode<'de> for PatternString { + #[inline] + fn decode(d: &mut Decoder<'de>) -> Result { + Decode::decode(d).map(|s: &PatternStr| s.to_owned()) + } +} + +impl Encode for PatternString { + #[inline] + fn encode(&self, e: &mut Encoder) -> Result<(), encode::Error> { + self.as_pattern_str().encode(e) + } +} + +impl<'de: 'a, 'a> Decode<'de> for Qualified<'a> { + #[inline] + fn decode(d: &mut Decoder<'de>) -> Result { + Decode::decode(d).and_then(|s: &RefStr| { + s.qualified() + .ok_or(decode::Error::Message("not a qualified ref")) + }) + } +} + +impl Encode for Qualified<'_> { + #[inline] + fn encode(&self, e: &mut Encoder) -> Result<(), encode::Error> { + self.as_str().encode(e) + } +} + +impl<'de: 'a, 'a> Decode<'de> for Namespaced<'a> { + #[inline] + fn decode(d: &mut Decoder<'de>) -> Result { + Decode::decode(d).and_then(|s: &RefStr| { + s.to_namespaced() + .ok_or(decode::Error::Message("not a namespaced ref")) + }) + } +} + +impl Encode for Namespaced<'_> { + #[inline] + fn encode(&self, e: &mut Encoder) -> Result<(), encode::Error> { + self.as_str().encode(e) + } +} diff --git a/crates/radicle-git-ref-format/src/check.rs b/crates/radicle-git-ref-format/src/check.rs new file mode 100644 index 000000000..36bb5f4af --- /dev/null +++ b/crates/radicle-git-ref-format/src/check.rs @@ -0,0 +1,101 @@ +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(()) + } + } + } +} diff --git a/crates/radicle-git-ref-format/src/deriv.rs b/crates/radicle-git-ref-format/src/deriv.rs new file mode 100644 index 000000000..6c373489e --- /dev/null +++ b/crates/radicle-git-ref-format/src/deriv.rs @@ -0,0 +1,397 @@ +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>, + B: Into>, + C: IntoIterator>, + { + let mut inner = name::REFS.join(a.into()).and(b.into()); + inner.extend(tail); + + Self(inner.into()) + } + + pub fn from_refstr(r: impl Into>) -> Option { + Self::_from_refstr(r.into()) + } + + fn _from_refstr(r: Cow<'a, RefStr>) -> Option { + 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, + { + Qualified(self.0.join(other).into()) + } + + pub fn to_pattern

(&'a self, pattern: P) -> QualifiedPattern<'a> + where + P: AsRef, + { + QualifiedPattern(Cow::Owned(RefStr::to_pattern(self, pattern.as_ref()))) + } + + #[inline] + pub fn to_namespaced(&'a self) -> Option> { + self.0.as_ref().into() + } + + /// Add a namespace. + /// + /// Creates a new [`Namespaced`] by prefxing `self` with + /// `refs/namespaces/`. + 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(&'a self) -> (&'a str, &'a str, &'a str, name::Iter<'a>) { + 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( + &'a self, + ) -> ( + Component<'a>, + Component<'a>, + Component<'a>, + name::Components<'a>, + ) { + 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 for Qualified<'_> { + #[inline] + fn as_ref(&self) -> &RefStr { + self + } +} + +impl AsRef for Qualified<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl AsRef for Qualified<'_> { + #[inline] + fn as_ref(&self) -> &Self { + self + } +} + +impl<'a> From> for Cow<'a, RefStr> { + #[inline] + fn from(q: Qualified<'a>) -> Self { + q.0 + } +} + +impl From> for RefString { + #[inline] + fn from(q: Qualified) -> Self { + q.0.into_owned() + } +} + +impl From<(lit::Refs, T, U)> for Qualified<'_> +where + T: AsRef, + U: AsRef, +{ + #[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 From> for Qualified<'_> +where + T: AsRef, +{ + #[inline] + fn from((refs, heads, name): lit::RefsHeads) -> Self { + Self(Cow::Owned( + IntoIterator::into_iter([Component::from(refs), heads.into()]) + .collect::() + .and(name), + )) + } +} + +impl From> for Qualified<'_> +where + T: AsRef, +{ + #[inline] + fn from((refs, tags, name): lit::RefsTags) -> Self { + Self(Cow::Owned( + IntoIterator::into_iter([Component::from(refs), tags.into()]) + .collect::() + .and(name), + )) + } +} + +impl From> for Qualified<'_> +where + T: AsRef, +{ + #[inline] + fn from((refs, notes, name): lit::RefsNotes) -> Self { + Self(Cow::Owned( + IntoIterator::into_iter([Component::from(refs), notes.into()]) + .collect::() + .and(name), + )) + } +} + +impl From> for Qualified<'_> +where + T: AsRef, +{ + #[inline] + fn from((refs, remotes, name): lit::RefsRemotes) -> Self { + Self(Cow::Owned( + IntoIterator::into_iter([Component::from(refs), remotes.into()]) + .collect::() + .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" or "HEAD". Eg. +/// +/// refs/namespaces/xyz/refs/heads/main +/// refs/namespaces/xyz/HEAD +/// +/// 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(&'a self) -> Component<'a> { + 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 for Namespaced<'_> { + #[inline] + fn as_ref(&self) -> &RefStr { + self + } +} + +impl AsRef for Namespaced<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl<'a> From> for Qualified<'a> { + #[inline] + fn from(ns: Namespaced<'a>) -> Self { + Self(ns.0) + } +} + +impl<'a> From<&'a RefStr> for Option> { + fn from(rs: &'a RefStr) -> Self { + let mut cs = rs.iter(); + match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) { + ("refs", "namespaces", _, "refs" | "HEAD") => Some(Namespaced(Cow::from(rs))), + + _ => None, + } + } +} + +impl<'a, T> From> for Namespaced<'static> +where + T: Into>, +{ + #[inline] + fn from((refs, namespaces, namespace, name): lit::RefsNamespaces) -> Self { + Self(Cow::Owned( + IntoIterator::into_iter([refs.into(), namespaces.into(), namespace.into()]) + .collect::() + .and(name), + )) + } +} + +impl Display for Namespaced<'_> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/crates/radicle-git-ref-format/src/lit.rs b/crates/radicle-git-ref-format/src/lit.rs new file mode 100644 index 000000000..bcafefb7d --- /dev/null +++ b/crates/radicle-git-ref-format/src/lit.rs @@ -0,0 +1,192 @@ +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 { + (c.as_ref() == Self::NAME).then_some(Self::SELF) + } +} + +impl From 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 { + 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 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 { + match self { + Self::Known(k) => Some(k), + _ => None, + } + } +} + +impl<'a> From> 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 = (Refs, Heads, T); +pub type RefsTags = (Refs, Tags, T); +pub type RefsNotes = (Refs, Notes, T); +pub type RefsRemotes = (Refs, Remotes, T); +pub type RefsNamespaces<'a, T> = (Refs, Namespaces, T, Qualified<'a>); + +#[inline] +pub fn refs_heads>(name: T) -> RefsHeads { + (Refs, Heads, name) +} + +#[inline] +pub fn refs_tags>(name: T) -> RefsTags { + (Refs, Tags, name) +} + +#[inline] +pub fn refs_notes>(name: T) -> RefsNotes { + (Refs, Notes, name) +} + +#[inline] +pub fn refs_remotes>(name: T) -> RefsRemotes { + (Refs, Remotes, name) +} + +#[inline] +pub fn refs_namespaces<'a, 'b, T>(namespace: T, name: Qualified<'b>) -> RefsNamespaces<'b, T> +where + T: Into>, +{ + (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 {} diff --git a/crates/radicle-git-ref-format/src/name.rs b/crates/radicle-git-ref-format/src/name.rs new file mode 100644 index 000000000..b54431f28 --- /dev/null +++ b/crates/radicle-git-ref-format/src/name.rs @@ -0,0 +1,464 @@ +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

(&self, base: P) -> Option<&RefStr> + where + P: AsRef, + { + 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(&self, other: R) -> RefString + where + R: AsRef, + { + 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

(&self, pattern: P) -> PatternString + where + P: AsRef, + { + self._to_pattern(pattern.as_ref()) + } + + fn _to_pattern(&self, pattern: &PatternStr) -> PatternString { + self.to_owned().with_pattern(pattern) + } + + #[inline] + pub fn qualified<'a>(&'a self) -> Option> { + Qualified::from_refstr(self) + } + + #[inline] + pub fn to_namespaced<'a>(&'a self) -> Option> { + self.into() + } + + pub fn iter<'a>(&'a self) -> Iter<'a> { + self.0.split('/') + } + + pub fn components<'a>(&'a self) -> Components<'a> { + Components::from(self) + } + + pub fn head<'a>(&'a self) -> Component<'a> { + 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 for RefStr { + #[inline] + fn as_ref(&self) -> &str { + self + } +} + +#[cfg(feature = "bstr")] +impl AsRef for RefStr { + #[inline] + fn as_ref(&self) -> &bstr::BStr { + use bstr::ByteSlice as _; + self.as_str().as_bytes().as_bstr() + } +} + +impl AsRef for &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 { + 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(self, other: R) -> Self + where + R: AsRef, + { + self._and(other.as_ref()) + } + + fn _and(mut self, other: &RefStr) -> Self { + self.push(other); + self + } + + pub fn push(&mut self, other: R) + where + R: AsRef, + { + 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

(self, pattern: P) -> PatternString + where + P: AsRef, + { + 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::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 for RefString { + #[inline] + fn as_ref(&self) -> &RefStr { + self + } +} + +impl AsRef for RefString { + #[inline] + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +#[cfg(feature = "bstr")] +impl AsRef for RefString { + #[inline] + fn as_ref(&self) -> &bstr::BStr { + use bstr::ByteSlice as _; + self.as_str().as_bytes().as_bstr() + } +} + +impl Borrow 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 { + RefStr::try_from_str(s).map(ToOwned::to_owned) + } +} + +impl TryFrom for RefString { + type Error = check::Error; + + #[inline] + fn try_from(s: String) -> Result { + 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 for Cow<'a, RefStr> { + #[inline] + fn from(rs: RefString) -> Cow<'a, RefStr> { + Cow::Owned(rs) + } +} + +impl From for String { + #[inline] + fn from(rs: RefString) -> Self { + rs.0 + } +} + +#[cfg(feature = "bstr")] +impl From 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 FromIterator for RefString +where + A: AsRef, +{ + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + 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 Extend for RefString +where + A: AsRef, +{ + fn extend(&mut self, iter: T) + where + T: IntoIterator, + { + for x in iter { + self.push(x) + } + } +} diff --git a/crates/radicle-git-ref-format/src/name/iter.rs b/crates/radicle-git-ref-format/src/name/iter.rs new file mode 100644 index 000000000..2a093e1c1 --- /dev/null +++ b/crates/radicle-git-ref-format/src/name/iter.rs @@ -0,0 +1,155 @@ +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.inner + .next() + .map(RefStr::from_str) + .map(Cow::from) + .map(Component) + } +} + +impl DoubleEndedIterator for Components<'_> { + #[inline] + fn next_back(&mut self) -> Option { + 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>) -> Option { + let r = r.into(); + if !r.contains('/') { + Some(Self(r)) + } else { + None + } + } + + #[inline] + pub fn as_lit(&self) -> Option { + T::from_component(self) + } + + #[inline] + pub fn into_inner(self) -> Cow<'a, RefStr> { + self.0 + } +} + +impl Deref for Component<'_> { + type Target = RefStr; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for Component<'_> { + #[inline] + fn as_ref(&self) -> &RefStr { + self + } +} + +impl<'a> From<&'a RefStr> for Option> { + #[inline] + fn from(r: &'a RefStr) -> Self { + if !r.contains('/') { + Some(Component(Cow::from(r))) + } else { + None + } + } +} + +impl<'a> From> for Cow<'a, RefStr> { + #[inline] + fn from(c: Component<'a>) -> Self { + c.0 + } +} + +impl From for Component<'static> { + #[inline] + fn from(_: T) -> Self { + Component(Cow::from(T::NAME)) + } +} + +impl<'a> From> 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)); +} diff --git a/crates/radicle-git-ref-format/src/refspec.rs b/crates/radicle-git-ref-format/src/refspec.rs new file mode 100644 index 000000000..88c35f3bf --- /dev/null +++ b/crates/radicle-git-ref-format/src/refspec.rs @@ -0,0 +1,605 @@ +use std::{ + borrow::{Borrow, Cow}, + convert::TryFrom, + fmt::{self, Display, Write as _}, + iter::FromIterator, + ops::Deref, +}; + +use thiserror::Error; + +use crate::{check, lit, Namespaced, Qualified, 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(&self, other: R) -> PatternString + where + R: AsRef, + { + 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<'a>(&'a self) -> Option> { + QualifiedPattern::from_patternstr(self) + } + + #[inline] + pub fn to_namespaced<'a>(&'a self) -> Option> { + self.into() + } + + #[inline] + pub fn iter<'a>(&'a self) -> Iter<'a> { + self.0.split('/') + } + + #[inline] + pub fn components<'a>(&'a self) -> Components<'a> { + 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 for PatternStr { + #[inline] + fn as_ref(&self) -> &str { + self + } +} + +impl AsRef 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 { + 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 + where + T: IntoIterator>, + { + iter.into_iter().collect() + } + + #[inline] + pub fn and(mut self, other: R) -> Self + where + R: AsRef, + { + self._push(other.as_ref()); + self + } + + #[inline] + pub fn push(&mut self, other: R) + where + R: AsRef, + { + 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 for PatternString { + #[inline] + fn as_ref(&self) -> &PatternStr { + self + } +} + +impl AsRef for PatternString { + #[inline] + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl Borrow 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 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 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 { + PatternStr::try_from_str(s).map(ToOwned::to_owned) + } +} + +impl TryFrom for PatternString { + type Error = check::Error; + + #[inline] + fn try_from(s: String) -> Result { + 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> for Result { + fn from_iter(iter: T) -> Self + where + T: IntoIterator>, + { + 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>) -> Option { + Self::_from_patternstr(r.into()) + } + + fn _from_patternstr(r: Cow<'a, PatternStr>) -> Option { + 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, + { + QualifiedPattern(Cow::Owned(self.0.join(other))) + } + + #[inline] + pub fn to_namespaced(&'a self) -> Option> { + self.0.as_ref().into() + } + + /// Add a namespace. + /// + /// Creates a new [`NamespacedPattern`] by prefxing `self` with + /// `refs/namespaces/`. + pub fn with_namespace<'b>( + &self, + ns: Component<'b>, + ) -> Result, 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(&'a self) -> (&'a str, &'a str, &'a str, Iter<'a>) { + 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( + &'a self, + ) -> (Component<'a>, Component<'a>, Component<'a>, Components<'a>) { + 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 for QualifiedPattern<'_> { + #[inline] + fn as_ref(&self) -> &PatternStr { + self + } +} + +impl AsRef for QualifiedPattern<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl AsRef for QualifiedPattern<'_> { + #[inline] + fn as_ref(&self) -> &Self { + self + } +} + +impl Display for QualifiedPattern<'_> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl<'a> From> for QualifiedPattern<'a> { + #[inline] + fn from(q: Qualified<'a>) -> Self { + Self(Cow::Owned(q.into_refstring().into())) + } +} + +impl<'a> From> for Cow<'a, PatternStr> { + #[inline] + fn from(q: QualifiedPattern<'a>) -> Self { + q.0 + } +} + +impl From> 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(&'a self) -> Component<'a> { + 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 for NamespacedPattern<'_> { + #[inline] + fn as_ref(&self) -> &PatternStr { + self.0.as_ref() + } +} + +impl AsRef for NamespacedPattern<'_> { + #[inline] + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl Borrow for NamespacedPattern<'_> { + #[inline] + fn borrow(&self) -> &PatternStr { + PatternStr::from_str(self.0.as_str()) + } +} + +impl<'a> From> for NamespacedPattern<'a> { + #[inline] + fn from(ns: Namespaced<'a>) -> Self { + NamespacedPattern(Cow::Owned(ns.to_ref_string().into())) + } +} + +impl<'a> From> for QualifiedPattern<'a> { + #[inline] + fn from(ns: NamespacedPattern<'a>) -> Self { + Self(ns.0) + } +} + +impl<'a> From<&'a PatternStr> for Option> { + 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) + } +} + +/// A Git [refspec]. +/// +/// **Note** that this is a simplified version of a [refspec] where +/// the `src` and `dst` are required and there is no way to construct +/// a negative refspec, e.g. `^refs/heads/no-thanks`. +/// +/// [refspec]: https://git-scm.com/book/en/v2/Git-Internals-The-Refspec +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Refspec { + pub src: T, + pub dst: U, + pub force: bool, +} + +impl fmt::Display for Refspec +where + T: fmt::Display, + U: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.force { + f.write_char('+')?; + } + write!(f, "{}:{}", self.src, self.dst) + } +} diff --git a/crates/radicle-git-ref-format/src/refspec/iter.rs b/crates/radicle-git-ref-format/src/refspec/iter.rs new file mode 100644 index 000000000..0858f3d0b --- /dev/null +++ b/crates/radicle-git-ref-format/src/refspec/iter.rs @@ -0,0 +1,98 @@ +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 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 From 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.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 DoubleEndedIterator for Components<'_> { + #[inline] + fn next_back(&mut self) -> Option { + 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); +} diff --git a/crates/radicle-git-ref-format/src/serde.rs b/crates/radicle-git-ref-format/src/serde.rs new file mode 100644 index 000000000..f43cca051 --- /dev/null +++ b/crates/radicle-git-ref-format/src/serde.rs @@ -0,0 +1,190 @@ +use std::convert::TryFrom; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ + refspec::{NamespacedPattern, PatternStr, PatternString, QualifiedPattern}, + Namespaced, Qualified, RefStr, RefString, +}; + +impl<'de: 'a, 'a> Deserialize<'de> for &'a RefStr { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer) + .and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom)) + } +} + +impl Serialize for &RefStr { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for RefString { + #[inline] + fn deserialize(deserializer: D) -> Result + 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(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de: 'a, 'a> Deserialize<'de> for &'a PatternStr { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer) + .and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom)) + } +} + +impl Serialize for &PatternStr { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for PatternString { + #[inline] + fn deserialize(deserializer: D) -> Result + 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(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for Qualified<'static> { + #[inline] + fn deserialize(deserializer: D) -> Result + 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(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for Namespaced<'static> { + #[inline] + fn deserialize(deserializer: D) -> Result + 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(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for QualifiedPattern<'_> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer).and_then(|s: String| { + let s = PatternString::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 QualifiedPattern<'_> { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for NamespacedPattern<'_> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer).and_then(|s: String| { + let s = PatternString::try_from(s).map_err(de::Error::custom)?; + s.to_namespaced() + .ok_or_else(|| de::Error::custom("not a qualified ref")) + .map(|q| q.into_owned()) + }) + } +} + +impl Serialize for NamespacedPattern<'_> { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +}