Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'han/ref-format'
Fintan Halpenny committed 3 years ago
commit 55668e6e0f07abfb3a2bdc9ac8699a656d455ffd
parent b2d2bd1
17 files changed +411 -688
modified git-ref-format/core/src/check.rs
@@ -12,7 +12,7 @@ pub struct Options {
    pub allow_pattern: bool,
}

-
#[derive(Debug, Error)]
+
#[derive(Debug, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum Error {
    #[error("empty input")]
modified radicle-surf/Cargo.toml
@@ -32,7 +32,6 @@ base64 = "0.13"
either = "1.5"
nom = "6"
nonempty = "0.5"
-
radicle-git-ext = { path = "../radicle-git-ext", features = ["serde"] }
regex = ">= 1.5.5"
serde = { features = ["serde_derive"], optional = true, version = "1" }
thiserror = "1.0"
@@ -42,6 +41,15 @@ version = "0.15.0"
default-features = false
features = ["vendored-libgit2"]

+
[dependencies.git-ref-format]
+
version = "0.1.0"
+
path = "../git-ref-format"
+

+
[dependencies.radicle-git-ext]
+
version = "0.2.0"
+
path = "../radicle-git-ext"
+
features = ["serde"]
+

[dev-dependencies]
criterion = "0.3"
pretty_assertions = "1.3.0"
modified radicle-surf/benches/last_commit.rs
@@ -18,13 +18,13 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use radicle_surf::{
    file_system::{unsound, Path},
-
    vcs::git::{Branch, Repository, Rev},
+
    vcs::git::{Branch, Repository},
};

fn last_commit_comparison(c: &mut Criterion) {
    let repo = Repository::new("./data/git-platinum")
        .expect("Could not retrieve ./data/git-platinum as git repository");
-
    let rev: Rev = Branch::local("master").into();
+
    let rev = Branch::local("master");

    let mut group = c.benchmark_group("Last Commit");
    for path in [
modified radicle-surf/src/commit.rs
@@ -17,8 +17,6 @@

//! Represents a commit.

-
use std::convert::TryFrom as _;
-

#[cfg(feature = "serialize")]
use serde::{
    ser::{SerializeStruct as _, Serializer},
@@ -28,9 +26,10 @@ use serde::{
use crate::{
    diff,
    file_system,
+
    git::Glob,
    person::Person,
    revision::Revision,
-
    vcs::git::{self, BranchName, RepositoryRef, Rev},
+
    vcs::git::{self, BranchName, RepositoryRef},
};

use radicle_git_ext::Oid;
@@ -147,7 +146,7 @@ pub struct Commits {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
-
pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
+
pub fn commit<R: git::Revision>(repo: &RepositoryRef, rev: R) -> Result<Commit, Error> {
    let commit = repo.commit(rev)?;
    let sha1 = commit.id;
    let header = Header::from(&commit);
@@ -195,7 +194,7 @@ pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
    }

    let branches = repo
-
        .revision_branches(&sha1)?
+
        .revision_branches(&sha1, &Glob::heads("*")?.and_remotes("*")?)?
        .into_iter()
        .map(|b| b.name)
        .collect();
@@ -235,11 +234,11 @@ pub fn commits<P>(
where
    P: ToString,
{
-
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
-

-
    let rev: Rev = match maybe_revision {
+
    let rev = match maybe_revision {
        Some(revision) => revision,
-
        None => repo.head_oid()?.into(),
+
        None => Revision::Sha {
+
            sha: repo.head_oid()?,
+
        },
    };

    let stats = repo.get_commit_stats(&rev)?;
modified radicle-surf/src/object/blob.rs
@@ -18,10 +18,7 @@
//! Represents git object type 'blob', i.e. actual file contents.
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.

-
use std::{
-
    convert::TryFrom as _,
-
    str::{self, FromStr as _},
-
};
+
use std::str::{self, FromStr as _};

#[cfg(feature = "serialize")]
use serde::{
@@ -35,7 +32,6 @@ use crate::{
    git::RepositoryRef,
    object::{Error, Info, ObjectType},
    revision::Revision,
-
    vcs::git::Rev,
};

#[cfg(feature = "syntax")]
@@ -139,7 +135,6 @@ where
    P: ToString,
    C: FnOnce(&[u8]) -> BlobContent,
{
-
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
    let revision = maybe_revision.unwrap();
    let root = repo.snapshot(&revision)?;
    let p = file_system::Path::from_str(path)?;
modified radicle-surf/src/object/tree.rs
@@ -18,7 +18,7 @@
//! Represents git object type 'tree', i.e. like directory entries in Unix.
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.

-
use std::{convert::TryFrom as _, str::FromStr as _};
+
use std::str::FromStr as _;

#[cfg(feature = "serialize")]
use serde::{
@@ -32,7 +32,6 @@ use crate::{
    git::RepositoryRef,
    object::{Error, Info, ObjectType},
    revision::Revision,
-
    vcs::git::{Branch, Rev},
};

/// Result of a directory listing, carries other trees and blobs.
@@ -94,12 +93,13 @@ pub fn tree<P>(
where
    P: ToString,
{
-
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
    let prefix = maybe_prefix.unwrap_or_default();
-

    let rev = match maybe_revision {
        Some(r) => r,
-
        None => Branch::local("main").into(),
+
        None => Revision::Branch {
+
            name: "main".to_string(),
+
            peer_id: None,
+
        },
    };

    let path = if prefix == "/" || prefix.is_empty() {
modified radicle-surf/src/revision.rs
@@ -17,8 +17,6 @@

//! Represents revisions

-
use std::convert::TryFrom;
-

use nonempty::NonEmpty;

#[cfg(feature = "serialize")]
@@ -27,8 +25,8 @@ use serde::{Deserialize, Serialize};
use radicle_git_ext::Oid;

use crate::{
-
    git::{BranchName, RepositoryRef},
-
    vcs::git::{self, error::Error, RefScope, Rev, TagName},
+
    git::{commit::ToCommit, BranchName, Glob, RepositoryRef},
+
    vcs::git::{self, error::Error, TagName},
};

/// Types of a peer.
@@ -79,26 +77,38 @@ pub enum Revision<P> {
    },
}

-
impl<P> TryFrom<Revision<P>> for Rev
+
impl<P> git::Revision for &Revision<P>
where
    P: ToString,
{
-
    type Error = Error;
-

-
    fn try_from(other: Revision<P>) -> Result<Self, Self::Error> {
-
        match other {
-
            Revision::Tag { name } => Ok(git::TagName::new(&name).into()),
-
            Revision::Branch { name, peer_id } => Ok(match peer_id {
-
                Some(peer) => {
-
                    git::Branch::remote(&format!("heads/{}", name), &peer.to_string()).into()
-
                },
-
                None => git::Branch::local(&name).into(),
-
            }),
-
            Revision::Sha { sha } => Ok(sha.into()),
+
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
+
        match self {
+
            Revision::Tag { name } => {
+
                repo.refname_to_oid(git::TagName::new(name)?.refname().as_str())
+
            },
+
            Revision::Branch { name, peer_id } => {
+
                let refname = match peer_id {
+
                    Some(peer) => {
+
                        git::Branch::remote(&format!("heads/{}", name), &peer.to_string()).refname()
+
                    },
+
                    None => git::Branch::local(name).refname(),
+
                };
+
                repo.refname_to_oid(&refname)
+
            },
+
            Revision::Sha { sha } => Ok(*sha),
        }
    }
}

+
impl<P> ToCommit for &Revision<P>
+
where
+
    P: ToString,
+
{
+
    fn to_commit(self, repo: &RepositoryRef) -> Result<git::Commit, Error> {
+
        repo.commit(self)
+
    }
+
}
+

/// Bundled response to retrieve both [`BranchName`]es and [`TagName`]s for
/// a user's repo.
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -114,7 +124,7 @@ pub struct Revisions<P, U> {
}

/// Provide the [`Revisions`] for the given `peer_id`, looking for the
-
/// branches as [`RefScope::Remote`].
+
/// remote branches.
///
/// If there are no branches then this returns `None`.
///
@@ -129,7 +139,8 @@ pub fn remote<P, U>(
where
    P: Clone + ToString,
{
-
    let remote_branches = repo.branch_names(Some(peer_id.clone()).into())?;
+
    let remote_branches =
+
        repo.branch_names(&Glob::remotes(&format!("{}/*", peer_id.to_string()))?)?;
    Ok(
        NonEmpty::from_vec(remote_branches).map(|branches| Revisions {
            peer_id,
@@ -143,7 +154,7 @@ where
}

/// Provide the [`Revisions`] for the given `peer_id`, looking for the
-
/// branches as [`RefScope::Local`].
+
/// local branches.
///
/// If there are no branches then this returns `None`.
///
@@ -158,7 +169,7 @@ pub fn local<P, U>(
where
    P: Clone + ToString,
{
-
    let local_branches = repo.branch_names(RefScope::Local)?;
+
    let local_branches = repo.branch_names(&Glob::heads("*")?)?;
    let tags = repo.tag_names()?;
    Ok(
        NonEmpty::from_vec(local_branches).map(|branches| Revisions {
modified radicle-surf/src/vcs/git.rs
@@ -69,13 +69,12 @@ use std::str::FromStr;
pub use git2::{self, Error as Git2Error, Time};
pub use radicle_git_ext::Oid;

-
/// Provides ways of selecting a particular reference/revision.
-
mod reference;
-
pub use reference::{ParseError, Ref, Rev};
-

mod repo;
pub use repo::{Repository, RepositoryRef};

+
mod glob;
+
pub use glob::Glob;
+

mod history;
pub use history::History;

@@ -177,21 +176,9 @@ impl Revision for &Tag {
    }
}

-
impl Revision for &Rev {
+
impl Revision for &TagName {
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
-
        match *self {
-
            Rev::Oid(oid) => Ok(*oid),
-
            Rev::Ref(r) => {
-
                let r = match repo.which_namespace()? {
-
                    None => r.clone(),
-
                    Some(namespace) => match r {
-
                        Ref::Namespace { .. } => r.clone(),
-
                        _ => r.clone().namespaced(namespace),
-
                    },
-
                };
-
                let refname = format!("{}", r);
-
                Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
-
            },
-
        }
+
        let refname = repo.namespaced_refname(&self.refname())?;
+
        Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
    }
}
modified radicle-surf/src/vcs/git/branch.rs
@@ -15,7 +15,7 @@
// 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 crate::vcs::git::{self, error::Error, ext, reference::Ref};
+
use crate::vcs::git::{self, error::Error, ext};
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
@@ -56,7 +56,7 @@ impl From<git2::BranchType> for BranchType {
/// to fetch a branch.
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct BranchName(pub(crate) String);
+
pub struct BranchName(String);

impl fmt::Display for BranchName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -65,7 +65,7 @@ impl fmt::Display for BranchName {
}

impl TryFrom<&[u8]> for BranchName {
-
    type Error = str::Utf8Error;
+
    type Error = Error;

    fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
        let name = str::from_utf8(name)?;
@@ -114,18 +114,6 @@ impl Ord for Branch {
    }
}

-
impl From<Branch> for Ref {
-
    fn from(other: Branch) -> Self {
-
        match other.locality {
-
            BranchType::Local => Self::LocalBranch { name: other.name },
-
            BranchType::Remote { name } => Self::RemoteBranch {
-
                name: other.name,
-
                remote: name.unwrap_or_else(|| "**".to_string()),
-
            },
-
        }
-
    }
-
}
-

impl Branch {
    /// Helper to create a remote `Branch` with a name
    pub fn remote(name: &str, remote: &str) -> Self {
@@ -147,11 +135,11 @@ impl Branch {

    /// Get the name of the `Branch`.
    pub fn refname(&self) -> String {
-
        let branch_name = self.name.0.clone();
+
        let branch_name = &self.name.0;
        match self.locality {
            BranchType::Local => format!("refs/heads/{}", branch_name),
            BranchType::Remote { ref name } => match name {
-
                None => branch_name,
+
                None => branch_name.to_string(),
                Some(remote_name) => format!("refs/remotes/{}/{}", remote_name, branch_name),
            },
        }
modified radicle-surf/src/vcs/git/commit.rs
@@ -17,7 +17,7 @@

use crate::{
    file_system::{self, directory},
-
    vcs::git::{error::Error, Branch, RepositoryRef, Rev, Tag},
+
    vcs::git::{error::Error, Branch, RepositoryRef, Tag, TagName},
};
use radicle_git_ext::Oid;
use std::{convert::TryFrom, str};
@@ -216,7 +216,7 @@ impl ToCommit for &Tag {
    }
}

-
impl ToCommit for &Rev {
+
impl ToCommit for &TagName {
    fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
        repo.commit(self)
    }
modified radicle-surf/src/vcs/git/error.rs
@@ -88,4 +88,7 @@ pub enum Error {
    /// A wrapper around the generic [`git2::Error`].
    #[error(transparent)]
    Git(#[from] git2::Error),
+
    /// A wrapper around git-ref-format::Error
+
    #[error(transparent)]
+
    RefFormat(#[from] git_ref_format::Error),
}
added radicle-surf/src/vcs/git/glob.rs
@@ -0,0 +1,107 @@
+
// This file is part of radicle-git
+
// <https://github.com/radicle-dev/radicle-git>
+
//
+
// Copyright (C) 2022 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// 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 crate::vcs::git::Error;
+
use git_ref_format::refspec::PatternString;
+
use std::{convert::TryFrom, marker::PhantomData, str};
+

+
/// A collection of globs for T (a git reference type).
+
pub struct Glob<T> {
+
    globs: Vec<PatternString>,
+
    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()
+
    }
+
}
+

+
impl<Namespace> 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];
+
        Ok(Self {
+
            globs,
+
            glob_type: PhantomData,
+
        })
+
    }
+

+
    /// 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);
+
        Ok(self)
+
    }
+
}
+

+
impl<Tag> 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 globs = vec![pattern];
+
        Ok(Self {
+
            globs,
+
            glob_type: PhantomData,
+
        })
+
    }
+

+
    /// 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);
+
        Ok(self)
+
    }
+
}
+

+
impl<Branch> 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];
+
        Ok(Self {
+
            globs,
+
            glob_type: PhantomData,
+
        })
+
    }
+

+
    /// 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];
+
        Ok(Self {
+
            globs,
+
            glob_type: PhantomData,
+
        })
+
    }
+

+
    /// 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);
+
        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);
+
        Ok(self)
+
    }
+
}
deleted radicle-surf/src/vcs/git/reference.rs
@@ -1,191 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// 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::{fmt, str};
-

-
use thiserror::Error;
-

-
use crate::vcs::git::{BranchName, Namespace, TagName};
-
use radicle_git_ext::Oid;
-
pub(super) mod glob;
-

-
/// A revision within the repository.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum Rev {
-
    /// A reference to a branch or tag.
-
    Ref(Ref),
-
    /// A particular commit identifier.
-
    Oid(Oid),
-
}
-

-
impl<R> From<R> for Rev
-
where
-
    R: Into<Ref>,
-
{
-
    fn from(other: R) -> Self {
-
        Self::Ref(other.into())
-
    }
-
}
-

-
impl From<Oid> for Rev {
-
    fn from(other: Oid) -> Self {
-
        Self::Oid(other)
-
    }
-
}
-

-
/// A structured way of referring to a git reference.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum Ref {
-
    /// A git tag, which can be found under `.git/refs/tags/`.
-
    Tag {
-
        /// The name of the tag, e.g. `v1.0.0`.
-
        name: TagName,
-
    },
-
    /// A git branch, which can be found under `.git/refs/heads/`.
-
    LocalBranch {
-
        /// The name of the branch, e.g. `master`.
-
        name: BranchName,
-
    },
-
    /// A git branch, which can be found under `.git/refs/remotes/`.
-
    RemoteBranch {
-
        /// The remote name, e.g. `origin`.
-
        remote: String,
-
        /// The name of the branch, e.g. `master`.
-
        name: BranchName,
-
    },
-
    /// A git namespace, which can be found under `.git/refs/namespaces/`.
-
    ///
-
    /// Note that namespaces can be nested.
-
    Namespace {
-
        /// The name value of the namespace.
-
        namespace: String,
-
        /// The reference under that namespace, e.g. The
-
        /// `refs/remotes/origin/master/ portion of `refs/namespaces/
-
        /// moi/refs/remotes/origin/master`.
-
        reference: Box<Ref>,
-
    },
-
}
-

-
impl Ref {
-
    /// Add a [`Namespace`] to a `Ref`.
-
    pub fn namespaced(self, Namespace { values: namespaces }: Namespace) -> Self {
-
        let mut ref_namespace = self;
-
        for namespace in namespaces.into_iter().rev() {
-
            ref_namespace = Self::Namespace {
-
                namespace,
-
                reference: Box::new(ref_namespace.clone()),
-
            };
-
        }
-

-
        ref_namespace
-
    }
-
}
-

-
impl fmt::Display for Ref {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Self::Tag { name } => write!(f, "refs/tags/{}", name),
-
            Self::LocalBranch { name } => write!(f, "refs/heads/{}", name),
-
            Self::RemoteBranch { remote, name } => write!(f, "refs/remotes/{}/{}", remote, name),
-
            Self::Namespace {
-
                namespace,
-
                reference,
-
            } => write!(f, "refs/namespaces/{}/{}", namespace, reference),
-
        }
-
    }
-
}
-

-
/// Error when parsing a ref.
-
#[derive(Debug, PartialEq, Eq, Error)]
-
pub enum ParseError {
-
    /// The parsed ref is malformed.
-
    #[error("the ref provided '{0}' was malformed")]
-
    MalformedRef(String),
-
}
-

-
pub mod parser {
-
    use nom::{bytes, named, tag, IResult};
-

-
    use crate::vcs::git::{BranchName, TagName};
-

-
    use super::Ref;
-

-
    const HEADS: &str = "refs/heads/";
-
    const REMOTES: &str = "refs/remotes/";
-
    const TAGS: &str = "refs/tags/";
-
    const NAMESPACES: &str = "refs/namespaces/";
-

-
    named!(heads, tag!(HEADS));
-
    named!(remotes, tag!(REMOTES));
-
    named!(tags, tag!(TAGS));
-
    named!(namsespaces, tag!(NAMESPACES));
-

-
    type Error<'a> = nom::Err<nom::error::Error<&'a str>>;
-

-
    pub fn component(s: &str) -> IResult<&str, &str> {
-
        bytes::complete::take_till(|c| c == '/')(s).and_then(|(rest, component)| {
-
            bytes::complete::take(1u8)(rest).map(|(rest, _)| (rest, component))
-
        })
-
    }
-

-
    pub fn local(s: &str) -> Result<Ref, Error> {
-
        bytes::complete::tag(HEADS)(s).map(|(name, _)| Ref::LocalBranch {
-
            name: BranchName::new(name),
-
        })
-
    }
-

-
    pub fn remote(s: &str) -> Result<Ref, Error> {
-
        bytes::complete::tag(REMOTES)(s).and_then(|(rest, _)| {
-
            component(rest).map(|(rest, remote)| Ref::RemoteBranch {
-
                remote: remote.to_owned(),
-
                name: BranchName::new(rest),
-
            })
-
        })
-
    }
-

-
    pub fn tag(s: &str) -> Result<Ref, Error> {
-
        bytes::complete::tag(TAGS)(s).map(|(name, _)| Ref::Tag {
-
            name: TagName::new(name),
-
        })
-
    }
-

-
    pub fn namespace(s: &str) -> Result<Ref, Error> {
-
        bytes::complete::tag(NAMESPACES)(s).and_then(|(rest, _)| {
-
            component(rest).and_then(|(rest, namespace)| {
-
                Ok(Ref::Namespace {
-
                    namespace: namespace.to_owned(),
-
                    reference: Box::new(parse(rest)?),
-
                })
-
            })
-
        })
-
    }
-

-
    pub fn parse(s: &str) -> Result<Ref, nom::Err<nom::error::Error<&str>>> {
-
        local(s)
-
            .or_else(|_| remote(s))
-
            .or_else(|_| tag(s))
-
            .or_else(|_| namespace(s))
-
    }
-
}
-

-
impl str::FromStr for Ref {
-
    type Err = ParseError;
-

-
    fn from_str(reference: &str) -> Result<Self, Self::Err> {
-
        parser::parse(reference).map_err(|_| ParseError::MalformedRef(reference.to_owned()))
-
    }
-
}
deleted radicle-surf/src/vcs/git/reference/glob.rs
@@ -1,183 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// 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 crate::{
-
    git::RefScope,
-
    vcs::git::{error, repo::RepositoryRef},
-
};
-
use either::Either;
-
use std::fmt::{self, Write as _};
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum RefGlob {
-
    /// When calling [`RefGlob::references`] this will return the references via
-
    /// the globs `refs/heads/*` and `refs/remotes/**/*`.
-
    Branch,
-
    /// When calling [`RefGlob::references`] this will return the references via
-
    /// the glob `refs/heads/*`.
-
    LocalBranch,
-
    /// When calling [`RefGlob::references`] this will return the references via
-
    /// either of the following globs:
-
    ///     * `refs/remotes/**/*`
-
    ///     * `refs/remotes/{remote}/*`
-
    RemoteBranch {
-
        /// If `remote` is `None` then the `**` wildcard will be used, otherwise
-
        /// the provided remote name will be used.
-
        remote: Option<String>,
-
    },
-
    /// When calling [`RefGlob::references`] this will return the references via
-
    /// the globs `refs/tags/*` and `refs/remotes/*/tags`
-
    Tag,
-
    /// When calling [`RefGlob::references`] this will return the references via
-
    /// the glob `refs/tags/*`.
-
    LocalTag,
-
    /// When calling [`RefGlob::references`] this will return the references via
-
    /// either of the following globs:
-
    ///     * `refs/remotes/*/tags/*`
-
    ///     * `refs/remotes/{remote}/tags/*`
-
    RemoteTag {
-
        /// If `remote` is `None` then the `*` wildcard will be used, otherwise
-
        /// the provided remote name will be used.
-
        remote: Option<String>,
-
    },
-
    /// refs/namespaces/**
-
    Namespace,
-
}
-

-
/// Iterator chaining multiple [`git2::References`]
-
#[must_use = "iterators are lazy and do nothing unless consumed"]
-
pub struct References<'a> {
-
    inner: Vec<git2::References<'a>>,
-
}
-

-
impl<'a> References<'a> {
-
    pub fn iter(self) -> impl Iterator<Item = Result<git2::Reference<'a>, git2::Error>> {
-
        self.inner.into_iter().flatten()
-
    }
-
}
-

-
impl RefGlob {
-
    pub fn branch(scope: RefScope) -> Self {
-
        match scope {
-
            RefScope::All => Self::Branch,
-
            RefScope::Local => Self::LocalBranch,
-
            RefScope::Remote { name } => Self::RemoteBranch { remote: name },
-
        }
-
    }
-

-
    pub fn tag(scope: RefScope) -> Self {
-
        match scope {
-
            RefScope::All => Self::Tag,
-
            RefScope::Local => Self::LocalTag,
-
            RefScope::Remote { name } => Self::RemoteTag { remote: name },
-
        }
-
    }
-

-
    pub fn references<'a>(&self, repo: &RepositoryRef<'a>) -> Result<References<'a>, error::Error> {
-
        let namespace = repo
-
            .which_namespace()?
-
            .map_or(Either::Left(std::iter::empty()), |namespace| {
-
                Either::Right(namespace.values.into_iter())
-
            });
-
        self.with_namespace_glob(namespace, repo)
-
    }
-

-
    fn with_namespace_glob<'a>(
-
        &self,
-
        namespace: impl Iterator<Item = String>,
-
        repo: &RepositoryRef<'a>,
-
    ) -> Result<References<'a>, error::Error> {
-
        let mut namespace_glob = "".to_string();
-
        for n in namespace {
-
            let _ = write!(namespace_glob, "refs/namespaces/{n}/");
-
        }
-

-
        Ok(match self {
-
            Self::Branch => {
-
                let remotes = repo.repo_ref.references_glob(&format!(
-
                    "{}{}",
-
                    namespace_glob,
-
                    Self::RemoteBranch { remote: None }
-
                ))?;
-

-
                let locals = repo.repo_ref.references_glob(&format!(
-
                    "{}{}",
-
                    namespace_glob,
-
                    &Self::LocalBranch
-
                ))?;
-
                References {
-
                    inner: vec![remotes, locals],
-
                }
-
            },
-
            Self::Tag => {
-
                let remotes = repo.repo_ref.references_glob(&format!(
-
                    "{}{}",
-
                    namespace_glob,
-
                    Self::RemoteTag { remote: None }
-
                ))?;
-

-
                let locals = repo.repo_ref.references_glob(&format!(
-
                    "{}{}",
-
                    namespace_glob,
-
                    &Self::LocalTag
-
                ))?;
-
                References {
-
                    inner: vec![remotes, locals],
-
                }
-
            },
-
            other => References {
-
                inner: vec![repo
-
                    .repo_ref
-
                    .references_glob(&format!("{}{}", namespace_glob, other,))?],
-
            },
-
        })
-
    }
-
}
-

-
impl fmt::Display for RefGlob {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Self::LocalBranch => write!(f, "refs/heads/*"),
-
            Self::RemoteBranch { remote } => {
-
                write!(f, "refs/remotes/")?;
-
                match remote {
-
                    None => write!(f, "**/*"),
-
                    Some(remote) => write!(f, "{}/*", remote),
-
                }
-
            },
-
            Self::LocalTag => write!(f, "refs/tags/*"),
-
            Self::RemoteTag { remote } => {
-
                let remote = match remote {
-
                    Some(remote) => remote.as_ref(),
-
                    None => "*",
-
                };
-
                write!(f, "refs/remotes/{}/tags/*", remote)
-
            },
-
            // Note: the glob below would be used, but libgit doesn't care for union globs.
-
            // write!(f, "refs/{{remotes/**/*,heads/*}}")
-
            Self::Branch | Self::Tag => {
-
                panic!("{}",
-
                "fatal: `Display` should not be called on `RefGlob::Branch` or `RefGlob::Tag` Since
-
                this `enum` is private to the repository, it should not be called from the outside.
-
                Unfortunately, libgit does not support union of globs otherwise this would display
-
                refs/{remotes/**/*,heads/*}"
-
            )
-
            },
-
            Self::Namespace => write!(f, "refs/namespaces/**"),
-
        }
-
    }
-
}
modified radicle-surf/src/vcs/git/repo.rs
@@ -21,13 +21,12 @@ use crate::{
    file_system::{directory, DirectoryContents, Label},
    vcs::git::{
        error::*,
-
        reference::{glob::RefGlob, Rev},
        Branch,
        BranchName,
        Commit,
+
        Glob,
        History,
        Namespace,
-
        RefScope,
        Revision,
        Signature,
        Stats,
@@ -38,7 +37,7 @@ use crate::{
use directory::Directory;
use radicle_git_ext::Oid;
use std::{
-
    collections::{BTreeSet, HashSet},
+
    collections::{btree_set, BTreeSet},
    convert::TryFrom,
    path::PathBuf,
    str,
@@ -75,6 +74,67 @@ impl<'a> From<&'a git2::Repository> for RepositoryRef<'a> {
    }
}

+
// I think the following `Tags` and `Branches` would be merged
+
// using Generic associated types supported in Rust 1.65.0.
+

+
/// An iterator for tags.
+
pub struct Tags<'a> {
+
    references: Vec<git2::References<'a>>,
+
    current: usize,
+
}
+

+
impl<'a> Iterator for Tags<'a> {
+
    type Item = Result<Tag, Error>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        while self.current < self.references.len() {
+
            match self.references.get_mut(self.current) {
+
                Some(refs) => match refs.next() {
+
                    Some(res) => return Some(res.map_err(Error::Git).and_then(Tag::try_from)),
+
                    None => self.current += 1,
+
                },
+
                None => break,
+
            }
+
        }
+
        None
+
    }
+
}
+

+
/// An iterator for branches.
+
pub struct Branches<'a> {
+
    references: Vec<git2::References<'a>>,
+
    current: usize,
+
}
+

+
impl<'a> Iterator for Branches<'a> {
+
    type Item = Result<Branch, Error>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        while self.current < self.references.len() {
+
            match self.references.get_mut(self.current) {
+
                Some(refs) => match refs.next() {
+
                    Some(res) => return Some(res.map_err(Error::Git).and_then(Branch::try_from)),
+
                    None => self.current += 1,
+
                },
+
                None => break,
+
            }
+
        }
+
        None
+
    }
+
}
+

+
/// An iterator for namespaces.
+
pub struct Namespaces {
+
    namespaces: btree_set::IntoIter<Namespace>,
+
}
+

+
impl Iterator for Namespaces {
+
    type Item = Namespace;
+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.namespaces.next()
+
    }
+
}
+

impl<'a> RepositoryRef<'a> {
    /// What is the current namespace we're browsing in.
    pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
@@ -84,56 +144,52 @@ impl<'a> RepositoryRef<'a> {
            .transpose()
    }

-
    /// List the branches within a repository, filtering out ones that do not
-
    /// parse correctly.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`Error::Git`]
-
    pub fn list_branches(&self, scope: RefScope) -> Result<Vec<Branch>, Error> {
-
        RefGlob::branch(scope)
-
            .references(self)?
-
            .iter()
-
            .try_fold(vec![], |mut acc, reference| {
-
                let branch = Branch::try_from(reference?)?;
-
                acc.push(branch);
-
                Ok(acc)
-
            })
+
    /// Returns an iterator of branches that match `pattern`.
+
    pub fn branches(&self, pattern: &Glob<Branch>) -> Result<Branches, Error> {
+
        let mut branches = Branches {
+
            references: vec![],
+
            current: 0,
+
        };
+
        for glob in pattern.globs().iter() {
+
            let namespaced = self.namespaced_refname(glob)?;
+
            let references = self.repo_ref.references_glob(&namespaced)?;
+
            branches.references.push(references);
+
        }
+
        Ok(branches)
    }

-
    /// List the tags within a repository, filtering out ones that do not parse
-
    /// correctly.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`Error::Git`]
-
    pub fn list_tags(&self, scope: RefScope) -> Result<Vec<Tag>, Error> {
-
        RefGlob::tag(scope)
-
            .references(self)?
-
            .iter()
-
            .try_fold(vec![], |mut acc, reference| {
-
                let tag = Tag::try_from(reference?)?;
-
                acc.push(tag);
-
                Ok(acc)
-
            })
+
    /// Returns an iterator of tags that match `pattern`.
+
    pub fn tags(&self, pattern: &Glob<Tag>) -> Result<Tags, Error> {
+
        let mut tags = Tags {
+
            references: vec![],
+
            current: 0,
+
        };
+
        for glob in pattern.globs().iter() {
+
            let namespaced = self.namespaced_refname(glob)?;
+
            let references = self.repo_ref.references_glob(&namespaced)?;
+
            tags.references.push(references);
+
        }
+
        Ok(tags)
    }

-
    /// List the namespaces within a repository, filtering out ones that do not
-
    /// parse correctly.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`Error::Git`]
-
    pub fn list_namespaces(&self) -> Result<Vec<Namespace>, Error> {
-
        let namespaces: Result<HashSet<Namespace>, Error> = RefGlob::Namespace
-
            .references(self)?
-
            .iter()
-
            .try_fold(HashSet::new(), |mut acc, reference| {
-
                let namespace = Namespace::try_from(reference?)?;
-
                acc.insert(namespace);
-
                Ok(acc)
-
            });
-
        Ok(namespaces?.into_iter().collect())
+
    /// 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() {
+
            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::EmptyNamespace))
+
                })
+
                .collect::<Result<BTreeSet<Namespace>, Error>>()?;
+
            set.extend(new_set);
+
        }
+
        Ok(Namespaces {
+
            namespaces: set.into_iter(),
+
        })
    }

    /// Get the [`Diff`] between two commits.
@@ -174,7 +230,11 @@ impl<'a> RepositoryRef<'a> {

    /// Returns the last commit, if exists, for a `path` in the history of
    /// `rev`.
-
    pub fn last_commit(&self, path: file_system::Path, rev: &Rev) -> Result<Option<Commit>, Error> {
+
    pub fn last_commit<C: ToCommit>(
+
        &self,
+
        path: file_system::Path,
+
        rev: C,
+
    ) -> Result<Option<Commit>, Error> {
        let history = self.history(rev)?;
        history.by_path(path).next().transpose()
    }
@@ -190,7 +250,7 @@ impl<'a> RepositoryRef<'a> {

    /// Gets stats of `commit`.
    pub fn get_commit_stats<C: ToCommit>(&self, commit: C) -> Result<Stats, Error> {
-
        let branches = self.list_branches(RefScope::Local)?.len();
+
        let branches = self.branches(&Glob::heads("*")?)?.count();
        let history = self.history(commit)?;
        let mut commits = 0;

@@ -212,13 +272,10 @@ impl<'a> RepositoryRef<'a> {
    }

    /// Lists branch names with `filter`.
-
    pub fn branch_names(&self, filter: RefScope) -> Result<Vec<BranchName>, Error> {
-
        let mut branches = self
-
            .list_branches(filter)?
-
            .into_iter()
-
            .map(|b| b.name)
-
            .collect::<Vec<BranchName>>();
-

+
    pub fn branch_names(&self, filter: &Glob<Branch>) -> Result<Vec<BranchName>, Error> {
+
        let branches: Result<Vec<BranchName>, Error> =
+
            self.branches(filter)?.map(|b| b.map(|b| b.name)).collect();
+
        let mut branches = branches?;
        branches.sort();

        Ok(branches)
@@ -226,12 +283,10 @@ impl<'a> RepositoryRef<'a> {

    /// Lists tag names in the local RefScope.
    pub fn tag_names(&self) -> Result<Vec<TagName>, Error> {
-
        let tag_names = self.list_tags(RefScope::Local)?;
-
        let mut tags: Vec<TagName> = tag_names
-
            .into_iter()
-
            .map(|tag_name| tag_name.name())
-
            .collect();
-

+
        let mut tags = self
+
            .tags(&Glob::tags("*")?)?
+
            .map(|t| t.map(|t| t.name()))
+
            .collect::<Result<Vec<TagName>, Error>>()?;
        tags.sort();

        Ok(tags)
@@ -258,6 +313,11 @@ impl<'a> RepositoryRef<'a> {
        Ok(fullname)
    }

+
    pub(crate) fn refname_to_oid(&self, refname: &str) -> Result<Oid, Error> {
+
        let oid = self.repo_ref.refname_to_id(refname)?;
+
        Ok(oid.into())
+
    }
+

    /// Get a particular `git2::Commit` of `oid`.
    pub(crate) fn get_git2_commit(&self, oid: Oid) -> Result<git2::Commit, Error> {
        self.repo_ref.find_commit(oid.into()).map_err(Error::Git)
@@ -292,23 +352,16 @@ impl<'a> RepositoryRef<'a> {
    }

    /// Lists branches that are reachable from `oid`.
-
    pub fn revision_branches(&self, oid: &Oid) -> Result<Vec<Branch>, Error> {
-
        let local = RefGlob::LocalBranch.references(self)?;
-
        let remote = RefGlob::RemoteBranch { remote: None }.references(self)?;
-
        let mut references = local.iter().chain(remote.iter());
-

+
    pub fn revision_branches(&self, oid: &Oid, glob: &Glob<Branch>) -> Result<Vec<Branch>, Error> {
        let mut contained_branches = vec![];
-

-
        references.try_for_each(|reference| {
-
            let reference = reference?;
-
            self.reachable_from(&reference, oid).and_then(|contains| {
-
                if contains {
-
                    let branch = Branch::try_from(reference)?;
-
                    contained_branches.push(branch);
-
                }
-
                Ok(())
-
            })
-
        })?;
+
        for branch in self.branches(glob)? {
+
            let branch = branch?;
+
            let namespaced = self.namespaced_refname(&branch.refname())?;
+
            let reference = self.repo_ref.find_reference(&namespaced)?;
+
            if self.reachable_from(&reference, oid)? {
+
                contained_branches.push(branch);
+
            }
+
        }

        Ok(contained_branches)
    }
modified radicle-surf/src/vcs/git/tag.rs
@@ -15,14 +15,15 @@
// 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 crate::vcs::git::{self, error::Error, reference::Ref, Author};
+
use crate::vcs::git::{self, error::Error, Author};
+
use git_ref_format::RefString;
use radicle_git_ext::Oid;
use std::{convert::TryFrom, fmt, str};

/// A newtype wrapper over `String` to separate out the fact that a caller wants
/// to fetch a tag.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct TagName(String);
+
pub struct TagName(RefString);

impl fmt::Display for TagName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -31,7 +32,7 @@ impl fmt::Display for TagName {
}

impl TryFrom<&[u8]> for TagName {
-
    type Error = str::Utf8Error;
+
    type Error = Error;

    fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
        let name = str::from_utf8(name)?;
@@ -39,26 +40,27 @@ impl TryFrom<&[u8]> for TagName {
            Ok(stripped) => stripped,
            Err(original) => original,
        };
-
        Ok(Self(short_name))
-
    }
-
}
-

-
impl From<TagName> for Ref {
-
    fn from(other: TagName) -> Self {
-
        Self::Tag { name: other }
+
        let refstring = RefString::try_from(short_name)?;
+
        Ok(Self(refstring))
    }
}

impl TagName {
    /// Create a new `TagName`.
-
    pub fn new(name: &str) -> Self {
-
        TagName(name.into())
+
    pub fn new(name: &str) -> Result<Self, Error> {
+
        let refstring = RefString::try_from(name)?;
+
        Ok(Self(refstring))
    }

    /// Access the string value of the `TagName`.
    pub fn name(&self) -> &str {
        &self.0
    }
+

+
    /// Returns the full ref name of the tag.
+
    pub fn refname(&self) -> String {
+
        format!("refs/tags/{}", self.name())
+
    }
}

/// The static information of a [`git2::Tag`].
@@ -109,12 +111,12 @@ impl Tag {

    /// Returns the full ref name of the tag.
    pub fn refname(&self) -> String {
-
        format!("refs/tags/{}", self.name().name())
+
        self.name().refname()
    }
}

impl<'repo> TryFrom<git2::Tag<'repo>> for Tag {
-
    type Error = str::Utf8Error;
+
    type Error = Error;

    fn try_from(tag: git2::Tag) -> Result<Self, Self::Error> {
        let id = tag.id().into();
@@ -152,7 +154,7 @@ impl<'repo> TryFrom<git2::Reference<'repo>> for Tag {
            let mut split = name.0.splitn(2, '/');
            let remote = split.next().map(|x| x.to_owned());
            let name = split.next().unwrap();
-
            (remote, TagName(name.to_owned()))
+
            (remote, TagName::new(name)?)
        } else {
            (None, name)
        };
modified radicle-surf/t/src/git.rs
@@ -8,7 +8,7 @@ use radicle_surf::git::{Author, BranchType, Commit};
use radicle_surf::{
    diff::*,
    file_system::{unsound, DirectoryContents, Path},
-
    git::{error::Error, Branch, BranchName, Namespace, Oid, RefScope, Repository, Rev, TagName},
+
    git::{error::Error, Branch, Glob, Namespace, Oid, Repository, TagName},
};

const GIT_PLATINUM: &str = "../data/git-platinum";
@@ -54,15 +54,17 @@ mod namespace {
        assert_eq!(history.head(), history_feature.head());

        let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
-
        let mut branches = repo.list_branches(RefScope::Local)?;
+
        let mut branches = repo
+
            .branches(&Glob::heads("*")?)?
+
            .collect::<Result<Vec<Branch>, Error>>()?;
        branches.sort();

        assert_eq!(expected_branches, branches);

        let expected_branches: Vec<Branch> = vec![Branch::remote("feature/#1194", "fein")];
-
        let mut branches = repo.list_branches(RefScope::Remote {
-
            name: Some("fein".to_string()),
-
        })?;
+
        let mut branches = repo
+
            .branches(&Glob::remotes("fein/*")?)?
+
            .collect::<Result<Vec<Branch>, Error>>()?;
        branches.sort();

        assert_eq!(expected_branches, branches);
@@ -89,7 +91,9 @@ mod namespace {
        assert_eq!(history.head(), golden_history.head());

        let expected_branches: Vec<Branch> = vec![Branch::local("banana"), Branch::local("master")];
-
        let mut branches = repo.list_branches(RefScope::Local)?;
+
        let mut branches = repo
+
            .branches(&Glob::heads("*")?)?
+
            .collect::<Result<Vec<Branch>, Error>>()?;
        branches.sort();

        assert_eq!(expected_branches, branches);
@@ -99,9 +103,9 @@ mod namespace {
            Branch::remote("heelflip", "kickflip"),
            Branch::remote("v0.1.0", "kickflip"),
        ];
-
        let mut branches = repo.list_branches(RefScope::Remote {
-
            name: Some("kickflip".to_string()),
-
        })?;
+
        let mut branches = repo
+
            .branches(&Glob::remotes("kickflip/*")?)?
+
            .collect::<Result<Vec<Branch>, Error>>()?;
        branches.sort();

        assert_eq!(expected_branches, branches);
@@ -126,7 +130,9 @@ mod namespace {
        assert_ne!(history.head(), silver_history.head());

        let expected_branches: Vec<Branch> = vec![Branch::local("master")];
-
        let mut branches = repo.list_branches(RefScope::All)?;
+
        let mut branches = repo
+
            .branches(&Glob::heads("*")?.and_remotes("*")?)?
+
            .collect::<Result<Vec<Branch>, Error>>()?;
        branches.sort();

        assert_eq!(expected_branches, branches);
@@ -180,8 +186,8 @@ mod rev {
    fn commit() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
-
        let mut history = repo.history(&rev)?;
+
        let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
        let mut history = repo.history(rev)?;

        let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
        assert!(history.any(|commit| commit.unwrap().id == commit1));
@@ -193,8 +199,8 @@ mod rev {
    fn commit_parents() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
-
        let history = repo.history(&rev)?;
+
        let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
        let history = repo.history(rev)?;
        let commit = history.head();

        assert_eq!(
@@ -209,8 +215,8 @@ mod rev {
    fn commit_short() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let rev: Rev = repo.oid("3873745c8")?.into();
-
        let mut history = repo.history(&rev)?;
+
        let rev = repo.oid("3873745c8")?;
+
        let mut history = repo.history(rev)?;

        let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
        assert!(history.any(|commit| commit.unwrap().id == commit1));
@@ -222,7 +228,7 @@ mod rev {
    fn tag() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let rev: Rev = TagName::new("v0.2.0").into();
+
        let rev = TagName::new("v0.2.0")?;
        let history = repo.history(&rev)?;

        let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
@@ -245,12 +251,11 @@ mod last_commit {
            Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");

        // memory.rs is commited later so it should not exist here.
-
        let rev: Rev = oid.into();
        let memory_last_commit_oid = repo
            .as_ref()
            .last_commit(
                Path::with_root(&[unsound::label::new("src"), unsound::label::new("memory.rs")]),
-
                &rev,
+
                oid,
            )
            .expect("Failed to get last commit")
            .map(|commit| commit.id);
@@ -260,7 +265,7 @@ mod last_commit {
        // README.md exists in this commit.
        let readme_last_commit = repo
            .as_ref()
-
            .last_commit(Path::with_root(&[unsound::label::new("README.md")]), &rev)
+
            .last_commit(Path::with_root(&[unsound::label::new("README.md")]), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

@@ -274,13 +279,12 @@ mod last_commit {
        // Check that last commit is the actual last commit even if head commit differs.
        let oid =
            Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
-
        let rev: Rev = oid.into();

        let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();

        let folder_svelte = repo
            .as_ref()
-
            .last_commit(unsound::path::new("~/examples/Folder.svelte"), &rev)
+
            .last_commit(unsound::path::new("~/examples/Folder.svelte"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

@@ -294,7 +298,6 @@ mod last_commit {
        // Check that last commit is the actual last commit even if head commit differs.
        let oid =
            Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
-
        let rev: Rev = oid.into();

        let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap();

@@ -302,7 +305,7 @@ mod last_commit {
            .as_ref()
            .last_commit(
                unsound::path::new("~/this/is/a/really/deeply/nested/directory/tree"),
-
                &rev,
+
                oid,
            )
            .expect("Failed to get last commit")
            .map(|commit| commit.id);
@@ -319,20 +322,19 @@ mod last_commit {
        // Check that last commit is the actual last commit even if head commit differs.
        let oid =
            Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA");
-
        let rev: Rev = oid.into();

        let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();

        let backslash_commit_id = repo
            .as_ref()
-
            .last_commit(unsound::path::new("~/special/faux\\path"), &rev)
+
            .last_commit(unsound::path::new("~/special/faux\\path"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);
        assert_eq!(backslash_commit_id, Some(expected_commit_id));

        let ogre_commit_id = repo
            .as_ref()
-
            .last_commit(unsound::path::new("~/special/👹👹👹"), &rev)
+
            .last_commit(unsound::path::new("~/special/👹👹👹"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);
        assert_eq!(ogre_commit_id, Some(expected_commit_id));
@@ -342,7 +344,7 @@ mod last_commit {
    fn root() {
        let repo = Repository::new(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
-
        let rev: Rev = Branch::local("master").into();
+
        let rev = Branch::local("master");
        let root_last_commit_id = repo
            .as_ref()
            .last_commit(Path::root(), &rev)
@@ -567,6 +569,8 @@ mod diff {

#[cfg(test)]
mod threading {
+
    use radicle_surf::git::Glob;
+

    use super::*;
    use std::sync::{Mutex, MutexGuard};

@@ -574,17 +578,20 @@ mod threading {
    fn basic_test() -> Result<(), Error> {
        let shared_repo = Mutex::new(Repository::new(GIT_PLATINUM)?);
        let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
-
        let mut branches = locked_repo.as_ref().list_branches(RefScope::All)?;
+
        let mut branches = locked_repo
+
            .as_ref()
+
            .branches(&Glob::heads("*")?.and_remotes("*")?)?
+
            .collect::<Result<Vec<Branch>, Error>>()?;
        branches.sort();

        assert_eq!(
            branches,
            vec![
                Branch::remote("HEAD", "origin"),
-
                Branch::remote("dev", "origin"),
                Branch::local("dev"),
-
                Branch::remote("master", "origin"),
+
                Branch::remote("dev", "origin"),
                Branch::local("master"),
+
                Branch::remote("master", "origin"),
                Branch::remote("orange/pineapple", "banana"),
                Branch::remote("pineapple", "banana"),
            ]
@@ -709,105 +716,52 @@ mod ext {
#[cfg(test)]
mod reference {
    use super::*;
-
    use radicle_surf::vcs::git::{ParseError, Ref};
-
    use std::str::FromStr;
+
    use radicle_surf::vcs::git::{Glob, Tag};

    #[test]
-
    fn parse_ref() -> Result<(), ParseError> {
-
        assert_eq!(
-
            Ref::from_str("refs/remotes/origin/master"),
-
            Ok(Ref::RemoteBranch {
-
                remote: "origin".to_string(),
-
                name: BranchName::new("master")
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/heads/master"),
-
            Ok(Ref::LocalBranch {
-
                name: BranchName::new("master"),
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/heads/i-am-hyphenated"),
-
            Ok(Ref::LocalBranch {
-
                name: BranchName::new("i-am-hyphenated"),
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/heads/prefix/i-am-hyphenated"),
-
            Ok(Ref::LocalBranch {
-
                name: BranchName::new("prefix/i-am-hyphenated"),
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/tags/v0.0.1"),
-
            Ok(Ref::Tag {
-
                name: TagName::new("v0.0.1")
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/moi/refs/remotes/origin/master"),
-
            Ok(Ref::Namespace {
-
                namespace: "moi".to_string(),
-
                reference: Box::new(Ref::RemoteBranch {
-
                    remote: "origin".to_string(),
-
                    name: BranchName::new("master")
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/moi/refs/namespaces/toi/refs/tags/v1.0.0"),
-
            Ok(Ref::Namespace {
-
                namespace: "moi".to_string(),
-
                reference: Box::new(Ref::Namespace {
-
                    namespace: "toi".to_string(),
-
                    reference: Box::new(Ref::Tag {
-
                        name: TagName::new("v1.0.0")
-
                    })
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/me/refs/heads/feature/#1194"),
-
            Ok(Ref::Namespace {
-
                namespace: "me".to_string(),
-
                reference: Box::new(Ref::LocalBranch {
-
                    name: BranchName::new("feature/#1194"),
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/me/refs/remotes/fein/heads/feature/#1194"),
-
            Ok(Ref::Namespace {
-
                namespace: "me".to_string(),
-
                reference: Box::new(Ref::RemoteBranch {
-
                    remote: "fein".to_string(),
-
                    name: BranchName::new("heads/feature/#1194"),
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/remotes/master"),
-
            Err(ParseError::MalformedRef("refs/remotes/master".to_owned())),
-
        );
+
    fn test_branches() {
+
        let repo = Repository::new(GIT_PLATINUM).unwrap();
+
        let repo = repo.as_ref();
+
        let branches = repo.branches(&Glob::heads("*").unwrap()).unwrap();
+
        for b in branches {
+
            println!("{}", b.unwrap().name);
+
        }
+
        let branches = repo
+
            .branches(&Glob::heads("*").unwrap().and_remotes("banana/*").unwrap())
+
            .unwrap();
+
        for b in branches {
+
            println!("{}", b.unwrap().refname());
+
        }
+
    }

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/refs/remotes/origin/master"),
-
            Err(ParseError::MalformedRef(
-
                "refs/namespaces/refs/remotes/origin/master".to_owned()
-
            )),
-
        );
+
    #[test]
+
    fn test_tag_snapshot() {
+
        let repo = Repository::new(GIT_PLATINUM).unwrap();
+
        let repo_ref = repo.as_ref();
+
        let tags = repo_ref
+
            .tags(&Glob::tags("*").unwrap())
+
            .unwrap()
+
            .collect::<Result<Vec<Tag>, Error>>()
+
            .unwrap();
+
        assert_eq!(tags.len(), 6);
+
        let root_dir = repo_ref.snapshot(&tags[0]).unwrap();
+
        assert_eq!(root_dir.contents().count(), 1);
+
    }

-
        Ok(())
+
    #[test]
+
    fn test_namespaces() {
+
        let repo = Repository::new(GIT_PLATINUM).unwrap();
+
        let repo = repo.as_ref();
+
        let namespaces = repo.namespaces(&Glob::namespaces("*").unwrap()).unwrap();
+
        assert_eq!(namespaces.count(), 3);
+
        let namespaces = repo
+
            .namespaces(&Glob::namespaces("golden/*").unwrap())
+
            .unwrap();
+
        assert_eq!(namespaces.count(), 2);
+
        let namespaces = repo
+
            .namespaces(&Glob::namespaces("golden/*").unwrap().and("me/*").unwrap())
+
            .unwrap();
+
        assert_eq!(namespaces.count(), 3);
    }
}

@@ -841,16 +795,6 @@ mod code_browsing {
    }

    #[test]
-
    fn test_tag_snapshot() {
-
        let repo = Repository::new(GIT_PLATINUM).unwrap();
-
        let repo_ref = repo.as_ref();
-
        let tags = repo_ref.list_tags(RefScope::Local).unwrap();
-
        assert_eq!(tags.len(), 6);
-
        let root_dir = repo_ref.snapshot(&tags[0]).unwrap();
-
        assert_eq!(root_dir.contents().count(), 1);
-
    }
-

-
    #[test]
    fn test_file_history() {
        let repo = Repository::new(GIT_PLATINUM).unwrap();
        let repo = repo.as_ref();