mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-31 00:40:51 +08:00
Merge remote-tracking branch 'origin/surf/git-ref-namespaces'
* origin/surf/git-ref-namespaces: radicle-surf: add short_name methods radicle-surf: Namespace using RefString git-ref-format: add well-known refspec components git-ref-format: fix typo in documentation git-ref-format: DoubleEndedIterator for Components git-ref-format: add QualifiedPattern and NamespacedPattern Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -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<P>(&self, pattern: P) -> QualifiedPattern
|
||||
where
|
||||
P: AsRef<PatternStr>,
|
||||
{
|
||||
QualifiedPattern(Cow::Owned(RefStr::to_pattern(self, pattern.as_ref())))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_namespaced(&self) -> Option<Namespaced> {
|
||||
self.0.as_ref().into()
|
||||
|
||||
@@ -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<R>(self, other: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
|
||||
@@ -34,6 +34,17 @@ impl<'a> Iterator for Components<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoubleEndedIterator for Components<'a> {
|
||||
#[inline]
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
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 {
|
||||
|
||||
@@ -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> {
|
||||
QualifiedPattern::from_patternstr(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_namespaced(&self) -> Option<NamespacedPattern> {
|
||||
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<Cow<'a, PatternStr>>) -> Option<Self> {
|
||||
Self::_from_patternstr(r.into())
|
||||
}
|
||||
|
||||
fn _from_patternstr(r: Cow<'a, PatternStr>) -> Option<Self> {
|
||||
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<RefStr>,
|
||||
{
|
||||
QualifiedPattern(Cow::Owned(self.0.join(other)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_namespaced(&self) -> Option<NamespacedPattern> {
|
||||
self.0.as_ref().into()
|
||||
}
|
||||
|
||||
/// Add a namespace.
|
||||
///
|
||||
/// Creates a new [`NamespacedPattern`] by prefxing `self` with
|
||||
/// "refs/namespaces/<ns>".
|
||||
pub fn with_namespace<'b>(
|
||||
&self,
|
||||
ns: Component<'b>,
|
||||
) -> Result<NamespacedPattern<'a>, 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<PatternStr> for QualifiedPattern<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &PatternStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for QualifiedPattern<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for QualifiedPattern<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<QualifiedPattern<'a>> for Cow<'a, PatternStr> {
|
||||
#[inline]
|
||||
fn from(q: QualifiedPattern<'a>) -> Self {
|
||||
q.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<QualifiedPattern<'_>> 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<PatternStr> for NamespacedPattern<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &PatternStr {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for NamespacedPattern<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<PatternStr> for NamespacedPattern<'_> {
|
||||
#[inline]
|
||||
fn borrow(&self) -> &PatternStr {
|
||||
PatternStr::from_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<NamespacedPattern<'a>> for QualifiedPattern<'a> {
|
||||
#[inline]
|
||||
fn from(ns: NamespacedPattern<'a>) -> Self {
|
||||
Self(ns.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PatternStr> for Option<NamespacedPattern<'a>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T: lit::Lit> From<T> 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::Item> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -194,7 +194,7 @@ pub fn commit<R: git::Revision>(repo: &RepositoryRef, rev: R) -> Result<Commit,
|
||||
}
|
||||
|
||||
let branches = repo
|
||||
.revision_branches(&sha1, &Glob::heads("*")?.and_remotes("*")?)?
|
||||
.revision_branches(&sha1, Glob::heads("*")?.and_remotes("*")?)?
|
||||
.into_iter()
|
||||
.map(|b| b.refname().into())
|
||||
.collect();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<T> {
|
||||
globs: Vec<PatternString>,
|
||||
globs: Vec<QualifiedPattern<'static>>,
|
||||
glob_type: PhantomData<T>, // To support different methods for different T.
|
||||
}
|
||||
|
||||
impl<T> Glob<T> {
|
||||
/// Returns the globs.
|
||||
pub fn globs(&self) -> Vec<&str> {
|
||||
self.globs.iter().map(|g| g.as_str()).collect()
|
||||
pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
|
||||
self.globs.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Namespace> Glob<Namespace> {
|
||||
impl Glob<Namespace> {
|
||||
/// Creates a `Glob` for namespaces.
|
||||
pub fn namespaces(glob: &str) -> Result<Self, Error> {
|
||||
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<Namespace> Glob<Namespace> {
|
||||
|
||||
/// Adds namespaces patterns to existing `Glob`.
|
||||
pub fn and(mut self, glob: &str) -> Result<Self, Error> {
|
||||
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<QualifiedPattern<'static>, Error> {
|
||||
Ok(
|
||||
qualify(refname!("refs/namespaces"), PatternString::try_from(glob)?)
|
||||
.expect("BUG: pattern is qualified"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tag> Glob<Tag> {
|
||||
impl FromIterator<PatternString> for Glob<Namespace> {
|
||||
fn from_iter<T: IntoIterator<Item = PatternString>>(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<Tag> {
|
||||
/// Creates a `Glob` for local tags.
|
||||
pub fn tags(glob: &str) -> Result<Self, Error> {
|
||||
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<Tag> Glob<Tag> {
|
||||
|
||||
/// Updates a `Glob` to include other tags.
|
||||
pub fn and_tags(mut self, glob: &str) -> Result<Self, Error> {
|
||||
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<QualifiedPattern<'static>, Error> {
|
||||
Ok(
|
||||
qualify(refname!("refs/tags"), PatternString::try_from(glob)?)
|
||||
.expect("BUG: pattern is qualified"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Branch> Glob<Branch> {
|
||||
impl FromIterator<PatternString> for Glob<Tag> {
|
||||
fn from_iter<T: IntoIterator<Item = PatternString>>(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<Branch> {
|
||||
/// Creates a `Glob` for local branches.
|
||||
pub fn heads(glob: &str) -> Result<Self, Error> {
|
||||
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<Branch> Glob<Branch> {
|
||||
|
||||
/// Creates a `Glob` for remote branches.
|
||||
pub fn remotes(glob: &str) -> Result<Self, Error> {
|
||||
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<Branch> Glob<Branch> {
|
||||
|
||||
/// Updates a `Glob` to include local branches.
|
||||
pub fn and_heads(mut self, glob: &str) -> Result<Self, Error> {
|
||||
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<Self, Error> {
|
||||
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<QualifiedPattern<'static>, Error> {
|
||||
Ok(
|
||||
qualify(refname!("refs/heads"), PatternString::try_from(glob)?)
|
||||
.expect("BUG: pattern is qualified"),
|
||||
)
|
||||
}
|
||||
|
||||
fn qualify_remotes(glob: &str) -> Result<QualifiedPattern<'static>, Error> {
|
||||
Ok(
|
||||
qualify(refname!("refs/remotes"), PatternString::try_from(glob)?)
|
||||
.expect("BUG: pattern is qualified"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn qualify(prefix: RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
|
||||
prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
|
||||
}
|
||||
|
||||
@@ -15,8 +15,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<String>,
|
||||
// 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::<Namespace>();
|
||||
/// 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::<Namespace>();
|
||||
/// 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<NonEmpty<Component<'a>>> for Namespace {
|
||||
fn from(cs: NonEmpty<Component<'a>>) -> Self {
|
||||
Self {
|
||||
namespaces: cs.into_iter().collect::<RefString>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Namespace {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(namespace: &str) -> Result<Self, Self::Error> {
|
||||
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, Self::Error> {
|
||||
Self::from_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,29 +153,38 @@ impl TryFrom<&[u8]> for Namespace {
|
||||
fn try_from(namespace: &[u8]) -> Result<Self, Self::Error> {
|
||||
str::from_utf8(namespace)
|
||||
.map_err(Error::from)
|
||||
.and_then(Namespace::try_from)
|
||||
.and_then(Self::from_str)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<git2::Reference<'_>> for Namespace {
|
||||
impl FromStr for Namespace {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
let namespaces = RefStr::try_from_str(name)?.to_ref_string();
|
||||
Ok(Self { namespaces })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Namespaced<'_>> 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<Self, Self::Error> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
NonEmpty::from_vec(values)
|
||||
.map(|values| Self { values })
|
||||
fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
|
||||
let name = RefStr::try_from_str(str::from_utf8(reference.name_bytes())?)?;
|
||||
name.to_namespaced()
|
||||
.ok_or(Error::EmptyNamespace)
|
||||
.map(Self::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/<category>/<path>'")]
|
||||
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<dyn std::error::Error + Send + Sync + 'static>),
|
||||
/// 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<Branch>) -> Result<Branches, Error> {
|
||||
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<Tag>) -> Result<Tags, Error> {
|
||||
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<Namespace>) -> Result<Namespaces, Error> {
|
||||
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::<Result<BTreeSet<Namespace>, 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<String, Error> {
|
||||
pub(crate) fn namespaced_refname(
|
||||
&'a self,
|
||||
refname: &Qualified<'a>,
|
||||
) -> Result<Qualified<'a>, 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<QualifiedPattern<'a>, 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<Branch>) -> Result<Vec<Branch>, Error> {
|
||||
pub fn revision_branches(&self, oid: &Oid, glob: Glob<Branch>) -> Result<Vec<Branch>, 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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user