From 6479ba091fa3ee3b16fba61138cfd8c6ec6d985d Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Fri, 11 Nov 2022 17:03:51 +0000 Subject: [PATCH 1/6] git-ref-format: add QualifiedPattern and NamespacedPattern There was no mechanism for building refspec patterns that included namespaces. To get around this limitation, QualifiedPattern and NamespacedPattern are added to the refspec module. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- git-ref-format/core/src/deriv.rs | 16 +- git-ref-format/core/src/refspec.rs | 268 +++++++++++++++++++++++- git-ref-format/core/src/refspec/iter.rs | 9 +- git-ref-format/t/src/tests.rs | 76 +++++++ 4 files changed, 366 insertions(+), 3 deletions(-) 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!( From cf1594cf43e23c9fc8f74cf69e22f81227e82f2b Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Tue, 15 Nov 2022 16:53:44 +0000 Subject: [PATCH 2/6] git-ref-format: DoubleEndedIterator for Components Add DoubleEndedIterator implementations for the two Components iterators. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- git-ref-format/core/src/name/iter.rs | 11 +++++++++++ git-ref-format/core/src/refspec/iter.rs | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/git-ref-format/core/src/name/iter.rs b/git-ref-format/core/src/name/iter.rs index de14c5f..e51e050 100644 --- a/git-ref-format/core/src/name/iter.rs +++ b/git-ref-format/core/src/name/iter.rs @@ -34,6 +34,17 @@ impl<'a> Iterator for Components<'a> { } } +impl<'a> DoubleEndedIterator for Components<'a> { + #[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 { diff --git a/git-ref-format/core/src/refspec/iter.rs b/git-ref-format/core/src/refspec/iter.rs index 3ab8a2c..a5daf1c 100644 --- a/git-ref-format/core/src/refspec/iter.rs +++ b/git-ref-format/core/src/refspec/iter.rs @@ -66,6 +66,17 @@ impl<'a> Iterator for Components<'a> { } } +impl<'a> DoubleEndedIterator for Components<'a> { + #[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 { From a4fc3db933740da865d5c4def9528f7915f03614 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Wed, 16 Nov 2022 10:28:16 +0000 Subject: [PATCH 3/6] git-ref-format: fix typo in documentation Fixes small typo in documentation of `and` function. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- git-ref-format/core/src/name.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-ref-format/core/src/name.rs b/git-ref-format/core/src/name.rs index e3b8673..34ee6bc 100644 --- a/git-ref-format/core/src/name.rs +++ b/git-ref-format/core/src/name.rs @@ -309,7 +309,7 @@ impl RefString { /// /// 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 neede. + /// intermediate values are not needed. pub fn and(self, other: R) -> Self where R: AsRef, From 2b5a34a703c182ff48d6c83ff3fa4256b4f37624 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Wed, 16 Nov 2022 10:29:44 +0000 Subject: [PATCH 4/6] git-ref-format: add well-known refspec components Add constants for well-known refspec component names. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- git-ref-format/core/src/refspec.rs | 2 +- git-ref-format/core/src/refspec/iter.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/git-ref-format/core/src/refspec.rs b/git-ref-format/core/src/refspec.rs index ea17d35..5bd19dd 100644 --- a/git-ref-format/core/src/refspec.rs +++ b/git-ref-format/core/src/refspec.rs @@ -16,7 +16,7 @@ use thiserror::Error; use crate::{check, lit, RefStr, RefString}; mod iter; -pub use iter::{Component, Components, Iter}; +pub use iter::{component, Component, Components, Iter}; pub const STAR: &PatternStr = PatternStr::from_str("*"); diff --git a/git-ref-format/core/src/refspec/iter.rs b/git-ref-format/core/src/refspec/iter.rs index a5daf1c..09da0b7 100644 --- a/git-ref-format/core/src/refspec/iter.rs +++ b/git-ref-format/core/src/refspec/iter.rs @@ -85,3 +85,19 @@ impl<'a> From<&'a PatternStr> for Components<'a> { } } } + +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); +} From 1ac99ac456a74a66b5996b4bfd01f3f3f70dd8c8 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Wed, 16 Nov 2022 10:32:17 +0000 Subject: [PATCH 5/6] radicle-surf: Namespace using RefString A `Namespace` is a '/' separated list of components, where each component represents another level of the namespaces hierarchy. Notice that a `RefString` is also a '/' separated list of components, thus it could be reused to define a `Namespace`. This change uses this observation and reworks `Namespace` to be backed by a `RefString`. This naturally leads to defining its helper functions to return `Namespaced` and `NamespacedPattern` reference names. They are reworked to take a `Qualified` for regular reference names and `QualifiedPattern` for refspec names, respectively. This leads to changing the backing vector of `Glob` to use `QualifiedPattern` instead of `PatternString`. While changing `Glob`, it was also noticed that the PhantomData was not used correctly -- all `impl`s used a generic parameter. This was changed to use the `Namespace`, `Tag`, and `Branch` types directly, providing the type-safety that would be expected. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- radicle-surf/src/commit.rs | 2 +- radicle-surf/src/git/glob.rs | 106 +++++++++++++++----- radicle-surf/src/git/namespace.rs | 161 +++++++++++++++++++++++------- radicle-surf/src/git/repo.rs | 45 ++++++--- radicle-surf/t/src/git.rs | 14 +-- 5 files changed, 243 insertions(+), 85 deletions(-) diff --git a/radicle-surf/src/commit.rs b/radicle-surf/src/commit.rs index 68e522f..e54c1a9 100644 --- a/radicle-surf/src/commit.rs +++ b/radicle-surf/src/commit.rs @@ -194,7 +194,7 @@ pub fn commit(repo: &RepositoryRef, rev: R) -> Result { - globs: Vec, + globs: Vec>, glob_type: PhantomData, // To support different methods for different T. } impl Glob { - /// Returns the globs. - pub fn globs(&self) -> Vec<&str> { - self.globs.iter().map(|g| g.as_str()).collect() + pub fn globs(&self) -> impl Iterator> { + self.globs.iter() } } -impl Glob { +impl Glob { /// Creates a `Glob` for namespaces. pub fn namespaces(glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/namespaces/{}", glob))?; - let globs = vec![pattern]; + let globs = vec![Self::qualify(glob)?]; Ok(Self { globs, glob_type: PhantomData, @@ -52,16 +56,38 @@ impl Glob { /// Adds namespaces patterns to existing `Glob`. pub fn and(mut self, glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/namespaces/{}", glob))?; - self.globs.push(pattern); + self.globs.push(Self::qualify(glob)?); Ok(self) } + + fn qualify(glob: &str) -> Result, Error> { + Ok( + qualify(refname!("refs/namespaces"), PatternString::try_from(glob)?) + .expect("BUG: pattern is qualified"), + ) + } } -impl Glob { +impl FromIterator for Glob { + fn from_iter>(iter: T) -> Self { + let globs = iter + .into_iter() + .map(|pat| { + qualify(refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified") + }) + .collect(); + + Self { + globs, + glob_type: PhantomData, + } + } +} + +impl Glob { /// Creates a `Glob` for local tags. pub fn tags(glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/tags/{}", glob))?; + let pattern = Self::qualify(glob)?; let globs = vec![pattern]; Ok(Self { globs, @@ -71,17 +97,36 @@ impl Glob { /// Updates a `Glob` to include other tags. pub fn and_tags(mut self, glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/tags/{}", glob))?; - self.globs.push(pattern); + self.globs.push(Self::qualify(glob)?); Ok(self) } + + fn qualify(glob: &str) -> Result, Error> { + Ok( + qualify(refname!("refs/tags"), PatternString::try_from(glob)?) + .expect("BUG: pattern is qualified"), + ) + } } -impl Glob { +impl FromIterator for Glob { + fn from_iter>(iter: T) -> Self { + let globs = iter + .into_iter() + .map(|pat| qualify(refname!("refs/tags"), pat).expect("BUG: pattern is qualified")) + .collect(); + + Self { + globs, + glob_type: PhantomData, + } + } +} + +impl Glob { /// Creates a `Glob` for local branches. pub fn heads(glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/heads/{}", glob))?; - let globs = vec![pattern]; + let globs = vec![Self::qualify_heads(glob)?]; Ok(Self { globs, glob_type: PhantomData, @@ -90,8 +135,7 @@ impl Glob { /// Creates a `Glob` for remote branches. pub fn remotes(glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/remotes/{}", glob))?; - let globs = vec![pattern]; + let globs = vec![Self::qualify_remotes(glob)?]; Ok(Self { globs, glob_type: PhantomData, @@ -100,15 +144,31 @@ impl Glob { /// Updates a `Glob` to include local branches. pub fn and_heads(mut self, glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/heads/{}", glob))?; - self.globs.push(pattern); + self.globs.push(Self::qualify_heads(glob)?); Ok(self) } /// Updates a `Glob` to include remote branches. pub fn and_remotes(mut self, glob: &str) -> Result { - let pattern = PatternString::try_from(format!("refs/remotes/{}", glob))?; - self.globs.push(pattern); + self.globs.push(Self::qualify_remotes(glob)?); Ok(self) } + + fn qualify_heads(glob: &str) -> Result, Error> { + Ok( + qualify(refname!("refs/heads"), PatternString::try_from(glob)?) + .expect("BUG: pattern is qualified"), + ) + } + + fn qualify_remotes(glob: &str) -> Result, Error> { + Ok( + qualify(refname!("refs/remotes"), PatternString::try_from(glob)?) + .expect("BUG: pattern is qualified"), + ) + } +} + +fn qualify(prefix: RefString, glob: PatternString) -> Option> { + prefix.to_pattern(glob).qualified().map(|q| q.into_owned()) } diff --git a/radicle-surf/src/git/namespace.rs b/radicle-surf/src/git/namespace.rs index a0801ff..3ced7d5 100644 --- a/radicle-surf/src/git/namespace.rs +++ b/radicle-surf/src/git/namespace.rs @@ -15,8 +15,20 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{convert::TryFrom, fmt, str}; +use std::{ + convert::TryFrom, + fmt, + str::{self, FromStr}, +}; +use git_ref_format::{ + refspec::{NamespacedPattern, PatternString, QualifiedPattern}, + Component, + Namespaced, + Qualified, + RefStr, + RefString, +}; use nonempty::NonEmpty; pub use radicle_git_ext::Oid; use thiserror::Error; @@ -25,49 +37,113 @@ use thiserror::Error; pub enum Error { /// When parsing a namespace we may come across one that was an empty /// string. - #[error("tried parsing the namespace but it was empty")] + #[error("namespaces must not be empty")] EmptyNamespace, #[error(transparent)] + RefFormat(#[from] git_ref_format::Error), + #[error(transparent)] Utf8(#[from] str::Utf8Error), } /// A `Namespace` value allows us to switch the git namespace of /// a repo. +/// +/// A `Namespace` is one or more name components separated by `/`, e.g. `surf`, +/// `surf/git`. +/// +/// For each `Namespace`, the reference name will add a single `refs/namespaces` +/// prefix, e.g. `refs/namespaces/surf`, +/// `refs/namespaces/surf/refs/namespaces/git`. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Namespace { - /// Since namespaces can be nested we have a vector of strings. - /// This means that the namespaces `"foo/bar"` is represented as - /// `vec!["foo", "bar"]`. - pub(super) values: NonEmpty, + // XXX: we rely on RefString being non-empty here, which + // git-ref-format ensures that there's no way to construct one. + pub(super) namespaces: RefString, } impl Namespace { - /// Appends a `refname` to this namespace, and returns - /// the full reference path. - pub fn append_refname(&self, refname: &str) -> String { - let mut prefix = String::new(); - for value in self.values.iter() { - prefix = format!("{}refs/namespaces/{}/", &prefix, value); + /// Take a `Qualified` reference name and convert it to a `Namespaced` using + /// this `Namespace`. + /// + /// # Example + /// + /// ```no_run + /// let ns = "surf/git".parse::(); + /// let name = ns.to_namespaced(qualified!("refs/heads/main")); + /// assert_eq!( + /// name.as_str(), + /// "refs/namespaces/surf/refs/namespaces/git/refs/heads/main" + /// ); + /// ``` + pub(crate) fn to_namespaced<'a>(&self, name: &Qualified<'a>) -> Namespaced<'a> { + let mut components = self.namespaces.components().rev(); + let mut namespaced = name.with_namespace( + components + .next() + .expect("BUG: 'namespaces' cannot be empty"), + ); + for ns in components { + let qualified = namespaced.into_qualified(); + namespaced = qualified.with_namespace(ns); } - format!("{}{}", &prefix, refname) + namespaced + } + + /// Take a `QualifiedPattern` reference name and convert it to a + /// `NamespacedPattern` using this `Namespace`. + /// + /// # Example + /// + /// ```no_run + /// let ns = "surf/git".parse::(); + /// let name = ns.to_namespaced(pattern!("refs/heads/*").to_qualified().unwrap()); + /// assert_eq!( + /// name.as_str(), + /// "refs/namespaces/surf/refs/namespaces/git/refs/heads/*" + /// ); + /// ``` + pub(crate) fn to_namespaced_pattern<'a>( + &self, + pat: &QualifiedPattern<'a>, + ) -> NamespacedPattern<'a> { + let pattern = PatternString::from(self.namespaces.clone()); + let mut components = pattern.components().rev(); + let mut namespaced = pat + .with_namespace( + components + .next() + .expect("BUG: 'namespaces' cannot be empty"), + ) + .expect("BUG: 'namespace' cannot have globs"); + for ns in components { + let qualified = namespaced.into_qualified(); + namespaced = qualified + .with_namespace(ns) + .expect("BUG: 'namespaces' cannot have globs"); + } + namespaced } } impl fmt::Display for Namespace { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let values: Vec<_> = self.values.clone().into(); - write!(f, "{}", values.join("/")) + write!(f, "{}", self.namespaces) + } +} + +impl<'a> From>> for Namespace { + fn from(cs: NonEmpty>) -> Self { + Self { + namespaces: cs.into_iter().collect::(), + } } } impl TryFrom<&str> for Namespace { type Error = Error; - fn try_from(namespace: &str) -> Result { - let values = namespace.split('/').map(|n| n.to_string()).collect(); - NonEmpty::from_vec(values) - .map(|values| Self { values }) - .ok_or(Error::EmptyNamespace) + fn try_from(name: &str) -> Result { + Self::from_str(name) } } @@ -77,29 +153,38 @@ impl TryFrom<&[u8]> for Namespace { fn try_from(namespace: &[u8]) -> Result { str::from_utf8(namespace) .map_err(Error::from) - .and_then(Namespace::try_from) + .and_then(Self::from_str) } } -impl TryFrom> for Namespace { +impl FromStr for Namespace { + type Err = Error; + + fn from_str(name: &str) -> Result { + let namespaces = RefStr::try_from_str(name)?.to_ref_string(); + Ok(Self { namespaces }) + } +} + +impl From> for Namespace { + fn from(namespaced: Namespaced<'_>) -> Self { + let mut namespaces = namespaced.namespace().to_ref_string(); + let mut qualified = namespaced.strip_namespace(); + while let Some(namespaced) = qualified.to_namespaced() { + namespaces.push(namespaced.namespace()); + qualified = namespaced.strip_namespace(); + } + Self { namespaces } + } +} + +impl TryFrom<&git2::Reference<'_>> for Namespace { type Error = Error; - fn try_from(reference: git2::Reference) -> Result { - let re = regex::Regex::new(r"refs/namespaces/([^/]+)/").unwrap(); - let ref_name = str::from_utf8(reference.name_bytes())?; - let values = re - .find_iter(ref_name) - .map(|m| { - String::from( - m.as_str() - .trim_start_matches("refs/namespaces/") - .trim_end_matches('/'), - ) - }) - .collect::>(); - - NonEmpty::from_vec(values) - .map(|values| Self { values }) + fn try_from(reference: &git2::Reference) -> Result { + let name = RefStr::try_from_str(str::from_utf8(reference.name_bytes())?)?; + name.to_namespaced() .ok_or(Error::EmptyNamespace) + .map(Self::from) } } diff --git a/radicle-surf/src/git/repo.rs b/radicle-surf/src/git/repo.rs index 8d8bed0..5baa872 100644 --- a/radicle-surf/src/git/repo.rs +++ b/radicle-surf/src/git/repo.rs @@ -23,7 +23,7 @@ use std::{ }; use directory::{Directory, FileContent}; -use git_ref_format::RefString; +use git_ref_format::{refspec::QualifiedPattern, Qualified, RefString}; use radicle_git_ext::Oid; use thiserror::Error; @@ -72,10 +72,14 @@ pub enum Error { Glob(#[from] glob::Error), #[error(transparent)] Namespace(#[from] namespace::Error), + #[error("the reference '{0}' should be of the form 'refs//'")] + NotQualified(String), /// The requested file was not found. #[error("path not found for: {0}")] PathNotFound(file_system::Path), #[error(transparent)] + RefFormat(#[from] git_ref_format::Error), + #[error(transparent)] Revision(Box), /// A `revspec` was provided that could not be parsed into a branch, tag, or /// commit object. @@ -131,8 +135,8 @@ impl<'a> RepositoryRef<'a> { /// Returns an iterator of branches that match `pattern`. pub fn branches(&self, pattern: &Glob) -> Result { let mut branches = Branches::default(); - for glob in pattern.globs().iter() { - let namespaced = self.namespaced_refname(glob)?; + for glob in pattern.globs() { + let namespaced = self.namespaced_pattern(glob)?; let references = self.repo_ref.references_glob(&namespaced)?; branches.push(references); } @@ -142,8 +146,8 @@ impl<'a> RepositoryRef<'a> { /// Returns an iterator of tags that match `pattern`. pub fn tags(&self, pattern: &Glob) -> Result { let mut tags = Tags::default(); - for glob in pattern.globs().iter() { - let namespaced = self.namespaced_refname(glob)?; + for glob in pattern.globs() { + let namespaced = self.namespaced_pattern(glob)?; let references = self.repo_ref.references_glob(&namespaced)?; tags.push(references); } @@ -153,14 +157,14 @@ impl<'a> RepositoryRef<'a> { /// Returns an iterator of namespaces that match `pattern`. pub fn namespaces(&self, pattern: &Glob) -> Result { let mut set = BTreeSet::new(); - for glob in pattern.globs().iter() { + for glob in pattern.globs() { let new_set = self .repo_ref .references_glob(glob)? .map(|reference| { reference .map_err(Error::Git) - .and_then(|r| Namespace::try_from(r).map_err(Error::from)) + .and_then(|r| Namespace::try_from(&r).map_err(Error::from)) }) .collect::, Error>>()?; set.extend(new_set); @@ -366,10 +370,25 @@ impl<'a> RepositoryRef<'a> { } /// Returns a full reference name with namespace(s) included. - pub(crate) fn namespaced_refname(&self, refname: &str) -> Result { + pub(crate) fn namespaced_refname( + &'a self, + refname: &Qualified<'a>, + ) -> Result, Error> { let fullname = match self.which_namespace()? { - Some(namespace) => namespace.append_refname(refname), - None => refname.to_string(), + Some(namespace) => namespace.to_namespaced(refname).into_qualified(), + None => refname.clone(), + }; + Ok(fullname) + } + + /// Returns a full reference name with namespace(s) included. + pub(crate) fn namespaced_pattern( + &'a self, + refname: &QualifiedPattern<'a>, + ) -> Result, Error> { + let fullname = match self.which_namespace()? { + Some(namespace) => namespace.to_namespaced_pattern(refname).into_qualified(), + None => refname.clone(), }; Ok(fullname) } @@ -408,12 +427,12 @@ impl<'a> RepositoryRef<'a> { } /// Lists branches that are reachable from `oid`. - pub fn revision_branches(&self, oid: &Oid, glob: &Glob) -> Result, Error> { + pub fn revision_branches(&self, oid: &Oid, glob: Glob) -> Result, Error> { let mut contained_branches = vec![]; - for branch in self.branches(glob)? { + for branch in self.branches(&glob)? { let branch = branch?; let namespaced = self.namespaced_refname(&branch.refname())?; - let reference = self.repo_ref.find_reference(&namespaced)?; + let reference = self.repo_ref.find_reference(namespaced.as_str())?; if self.reachable_from(&reference, oid)? { contained_branches.push(branch); } diff --git a/radicle-surf/t/src/git.rs b/radicle-surf/t/src/git.rs index 9848c42..810ee4a 100644 --- a/radicle-surf/t/src/git.rs +++ b/radicle-surf/t/src/git.rs @@ -8,7 +8,7 @@ use radicle_surf::git::{Author, Commit}; use radicle_surf::{ diff::*, file_system::{unsound, DirectoryEntry, Path}, - git::{Branch, Error, Glob, Namespace, Oid, Repository}, + git::{Branch, Error, Glob, Oid, Repository}, }; const GIT_PLATINUM: &str = "../data/git-platinum"; @@ -53,10 +53,7 @@ mod namespace { assert_eq!(repo.which_namespace().unwrap(), None); repo.switch_namespace("me")?; - assert_eq!( - repo.which_namespace().unwrap(), - Some(Namespace::try_from("me")?) - ); + assert_eq!(repo.which_namespace().unwrap(), Some("me".parse()?)); let history_feature = repo.history(&Branch::local(refname!("feature/#1194")))?; assert_eq!(history.head(), history_feature.head()); @@ -93,10 +90,7 @@ mod namespace { repo.switch_namespace("golden")?; - assert_eq!( - repo.which_namespace().unwrap(), - Some(Namespace::try_from("golden")?) - ); + assert_eq!(repo.which_namespace().unwrap(), Some("golden".parse()?)); let golden_history = repo.history(&Branch::local(refname!("master")))?; assert_eq!(history.head(), golden_history.head()); @@ -141,7 +135,7 @@ mod namespace { repo.switch_namespace("golden/silver")?; assert_eq!( repo.which_namespace().unwrap(), - Some(Namespace::try_from("golden/silver")?) + Some("golden/silver".parse()?) ); let silver_history = repo.history(&Branch::local(refname!("master")))?; assert_ne!(history.head(), silver_history.head()); From 9f1302649c1bb5a6b179c150bc79af5beccbae5d Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Wed, 16 Nov 2022 12:51:21 +0000 Subject: [PATCH 6/6] radicle-surf: add short_name methods Add `short_name` methods to `Branch`, `Local`, `Remote`, and `Tag` to get the short version of the reference name. Also adds a `remote` method to `Remote`. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- radicle-surf/src/git/branch.rs | 30 ++++++++++++++++++++++++++++++ radicle-surf/src/git/tag.rs | 16 +++++++++------- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/radicle-surf/src/git/branch.rs b/radicle-surf/src/git/branch.rs index 988f667..c6f3a65 100644 --- a/radicle-surf/src/git/branch.rs +++ b/radicle-surf/src/git/branch.rs @@ -37,6 +37,18 @@ impl Branch { Self::Remote(Remote::new(remote, name)) } + /// Return the short `Branch` refname, + /// e.g. `fix/ref-format`. + pub fn short_name(&self) -> &RefString { + match self { + Branch::Local(local) => local.short_name(), + Branch::Remote(remote) => remote.short_name(), + } + } + + /// Give back the fully qualified `Branch` refname, + /// e.g. `refs/remotes/origin/fix/ref-format`, + /// `refs/heads/fix/ref-format`. pub fn refname(&self) -> Qualified { match self { Branch::Local(local) => local.refname(), @@ -126,6 +138,12 @@ impl Local { } } + /// Return the short `Local` refname, + /// e.g. `fix/ref-format`. + pub fn short_name(&self) -> &RefString { + &self.name + } + /// Return the fully qualified `Local` refname, /// e.g. `refs/heads/fix/ref-format`. pub fn refname(&self) -> Qualified { @@ -219,6 +237,18 @@ impl Remote { }) } + /// Return the short `Remote` refname, + /// e.g. `fix/ref-format`. + pub fn short_name(&self) -> &RefString { + &self.name + } + + /// Return the remote of the `Remote`'s refname, + /// e.g. `origin`. + pub fn remote(&self) -> &RefString { + &self.remote + } + /// Give back the fully qualified `Remote` refname, /// e.g. `refs/remotes/origin/fix/ref-format`. pub fn refname(&self) -> Qualified { diff --git a/radicle-surf/src/git/tag.rs b/radicle-surf/src/git/tag.rs index 743599b..587bd2d 100644 --- a/radicle-surf/src/git/tag.rs +++ b/radicle-surf/src/git/tag.rs @@ -39,18 +39,20 @@ impl Tag { } } - /// Return the fully qualified `Tag` refname, - /// e.g. `refs/tags/release/v1`. - pub fn refname(&self) -> Qualified { - lit::refs_tags(self.name()).into() - } - - fn name(&self) -> &RefString { + /// Return the short `Tag` refname, + /// e.g. `release/v1`. + pub fn short_name(&self) -> &RefString { match &self { Tag::Light { name, .. } => name, Tag::Annotated { name, .. } => name, } } + + /// Return the fully qualified `Tag` refname, + /// e.g. `refs/tags/release/v1`. + pub fn refname(&self) -> Qualified { + lit::refs_tags(self.short_name()).into() + } } pub mod error {