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!(