diff --git a/git-ref-format/core/src/deriv.rs b/git-ref-format/core/src/deriv.rs index 2631d23..e091f16 100644 --- a/git-ref-format/core/src/deriv.rs +++ b/git-ref-format/core/src/deriv.rs @@ -9,7 +9,14 @@ use std::{ ops::Deref, }; -use crate::{lit, name, Component, RefStr, RefString}; +use crate::{ + lit, + name, + refspec::{PatternStr, QualifiedPattern}, + Component, + RefStr, + RefString, +}; /// A fully-qualified refname. /// @@ -74,6 +81,13 @@ impl<'a> Qualified<'a> { Qualified(self.0.join(other).into()) } + pub fn to_pattern

(&self, pattern: P) -> QualifiedPattern + where + P: AsRef, + { + QualifiedPattern(Cow::Owned(RefStr::to_pattern(self, pattern.as_ref()))) + } + #[inline] pub fn to_namespaced(&self) -> Option { self.0.as_ref().into() diff --git a/git-ref-format/core/src/refspec.rs b/git-ref-format/core/src/refspec.rs index 4be17ae..ea17d35 100644 --- a/git-ref-format/core/src/refspec.rs +++ b/git-ref-format/core/src/refspec.rs @@ -13,7 +13,7 @@ use std::{ use thiserror::Error; -use crate::{check, RefStr, RefString}; +use crate::{check, lit, RefStr, RefString}; mod iter; pub use iter::{Component, Components, Iter}; @@ -53,6 +53,16 @@ impl PatternStr { buf } + #[inline] + pub fn qualified(&self) -> Option { + QualifiedPattern::from_patternstr(self) + } + + #[inline] + pub fn to_namespaced(&self) -> Option { + self.into() + } + #[inline] pub fn iter(&self) -> Iter { self.0.split('/') @@ -292,3 +302,259 @@ impl Display for PatternString { 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(&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(&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 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<'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(&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 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 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) + } +} diff --git a/git-ref-format/core/src/refspec/iter.rs b/git-ref-format/core/src/refspec/iter.rs index 32de58b..3ab8a2c 100644 --- a/git-ref-format/core/src/refspec/iter.rs +++ b/git-ref-format/core/src/refspec/iter.rs @@ -6,7 +6,7 @@ use std::fmt::{self, Display}; use super::PatternStr; -use crate::RefStr; +use crate::{lit, RefStr}; pub type Iter<'a> = std::str::Split<'a, char>; @@ -40,6 +40,13 @@ impl Display for Component<'_> { } } +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> { diff --git a/git-ref-format/t/src/tests.rs b/git-ref-format/t/src/tests.rs index e1d96ef..fd777ab 100644 --- a/git-ref-format/t/src/tests.rs +++ b/git-ref-format/t/src/tests.rs @@ -265,6 +265,82 @@ fn with_pattern_and() { ) } +#[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!(