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/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,
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.rs b/git-ref-format/core/src/refspec.rs
index 4be17ae..5bd19dd 100644
--- a/git-ref-format/core/src/refspec.rs
+++ b/git-ref-format/core/src/refspec.rs
@@ -13,10 +13,10 @@ use std::{
use thiserror::Error;
-use crate::{check, RefStr, RefString};
+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("*");
@@ -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..09da0b7 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> {
@@ -59,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 {
@@ -67,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);
+}
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!(
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 &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/glob.rs b/radicle-surf/src/git/glob.rs
index 4656519..e997d9b 100644
--- a/radicle-surf/src/git/glob.rs
+++ b/radicle-surf/src/git/glob.rs
@@ -17,9 +17,15 @@
use std::{convert::TryFrom, marker::PhantomData, str};
-use git_ref_format::refspec::PatternString;
+use git_ref_format::{
+ refname,
+ refspec::{PatternString, QualifiedPattern},
+ RefString,
+};
use thiserror::Error;
+use crate::git::{Branch, Namespace, Tag};
+
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
@@ -28,22 +34,20 @@ pub enum Error {
/// A collection of globs for T (a git reference type).
pub struct Glob {
- 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/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 {
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());