Radish alpha
r
Git libraries for Radicle
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle-surf: rework to use git-ref-format
Fintan Halpenny committed 3 years ago
commit 9b5ea4352f5be1846bf3341b40155520fde008cc
parent c6c8fe316490d1f2f95332b00c7fa20f8e0fc80d
18 files changed +683 -575
modified radicle-surf/Cargo.toml
@@ -4,7 +4,7 @@ description = "A code surfing library for VCS file systems"
readme = "README.md"
version = "0.8.0"
authors = ["The Radicle Team <dev@radicle.xyz>"]
-
edition = "2018"
+
edition = "2021"
homepage = "https://github.com/radicle-dev/radicle-surf"
repository = "https://github.com/radicle-dev/radicle-surf"
license = "GPL-3.0-or-later"
@@ -44,6 +44,7 @@ features = ["vendored-libgit2"]
[dependencies.git-ref-format]
version = "0.1.0"
path = "../git-ref-format"
+
features = ["macro", "serde"]

[dependencies.radicle-git-ext]
version = "0.2.0"
@@ -56,9 +57,6 @@ pretty_assertions = "1.3.0"
proptest = "0.9"
serde_json = "1"

-
[dev-dependencies.test-helpers]
-
path = "../test/test-helpers"
-

[build-dependencies]
anyhow = "1.0"
flate2 = "1"
modified radicle-surf/benches/last_commit.rs
@@ -16,6 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
+
use git_ref_format::refname;
use radicle_surf::{
    file_system::{unsound, Path},
    git::{Branch, Repository},
@@ -24,7 +25,7 @@ use radicle_surf::{
fn last_commit_comparison(c: &mut Criterion) {
    let repo = Repository::open("./data/git-platinum")
        .expect("Could not retrieve ./data/git-platinum as git repository");
-
    let rev = Branch::local("master");
+
    let rev = Branch::local(refname!("master"));

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

//! Represents a commit.

+
use git_ref_format::RefString;
#[cfg(feature = "serialize")]
use serde::{
    ser::{SerializeStruct as _, Serializer},
@@ -26,7 +27,7 @@ use serde::{
use crate::{
    diff,
    file_system,
-
    git::{self, BranchName, Glob, RepositoryRef},
+
    git::{self, Glob, RepositoryRef},
    person::Person,
    revision::Revision,
};
@@ -54,7 +55,7 @@ pub struct Commit {
    /// The changeset introduced by this commit.
    pub diff: diff::Diff,
    /// The list of branches this commit belongs to.
-
    pub branches: Vec<BranchName>,
+
    pub branches: Vec<RefString>,
}

/// Representation of a code commit.
@@ -195,7 +196,7 @@ pub fn commit<R: git::Revision>(repo: &RepositoryRef, rev: R) -> Result<Commit,
    let branches = repo
        .revision_branches(&sha1, &Glob::heads("*")?.and_remotes("*")?)?
        .into_iter()
-
        .map(|b| b.name)
+
        .map(|b| b.refname().into())
        .collect();

    Ok(Commit {
@@ -226,13 +227,7 @@ pub fn header(repo: &RepositoryRef, sha1: Oid) -> Result<Header, Error> {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
-
pub fn commits<P>(
-
    repo: &RepositoryRef,
-
    maybe_revision: Option<Revision<P>>,
-
) -> Result<Commits, Error>
-
where
-
    P: ToString,
-
{
+
pub fn commits(repo: &RepositoryRef, maybe_revision: Option<Revision>) -> Result<Commits, Error> {
    let rev = match maybe_revision {
        Some(revision) => revision,
        None => Revision::Sha {
modified radicle-surf/src/git.rs
@@ -67,6 +67,7 @@ use std::str::FromStr;

// Re-export git2 as sub-module
pub use git2::{self, Error as Git2Error, Time};
+
use git_ref_format::{name::Components, Component, Qualified, RefString};
pub use radicle_git_ext::Oid;

mod repo;
@@ -81,15 +82,13 @@ pub use history::History;
pub mod error;
pub use error::Error;

-
pub mod ext;
-

/// Provides the data for talking about branches.
pub mod branch;
-
pub use branch::{Branch, BranchName, BranchType};
+
pub use branch::{Branch, Local, Remote};

/// Provides the data for talking about tags.
pub mod tag;
-
pub use tag::{Tag, TagName};
+
pub use tag::Tag;

/// Provides the data for talking about commits.
pub mod commit;
@@ -121,6 +120,30 @@ pub trait Revision {
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
}

+
impl Revision for RefString {
+
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
+
        repo.refname_to_oid(self.as_str())
+
    }
+
}
+

+
impl Revision for &RefString {
+
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
+
        repo.refname_to_oid(self.as_str())
+
    }
+
}
+

+
impl Revision for Qualified<'_> {
+
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
+
        repo.refname_to_oid(self.as_str())
+
    }
+
}
+

+
impl Revision for &Qualified<'_> {
+
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
+
        repo.refname_to_oid(self.as_str())
+
    }
+
}
+

impl Revision for Oid {
    fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
        Ok(*self)
@@ -147,9 +170,6 @@ impl Revision for &Tag {
    }
}

-
impl Revision for &TagName {
-
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
-
        let refname = repo.namespaced_refname(&self.refname())?;
-
        Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
-
    }
+
pub(crate) fn refstr_join<'a>(c: Component<'a>, cs: Components<'a>) -> RefString {
+
    std::iter::once(c).chain(cs).collect::<RefString>()
}
modified radicle-surf/src/git/branch.rs
@@ -1,186 +1,306 @@
-
// 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::{self, error::Error, ext};
-
#[cfg(feature = "serialize")]
-
use serde::{Deserialize, Serialize};
-
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
-

-
/// The branch type we want to filter on.
-
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
-
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
-
pub enum BranchType {
-
    /// Local branches that are under `refs/heads/*`
-
    Local,
-
    /// Remote branches that are under `refs/remotes/<name>/*` if the name is
-
    /// provided, otherwise `refs/remotes/**/*`.
-
    Remote {
-
        /// Name of the remote.
-
        name: Option<String>,
-
    },
-
}
-

-
impl From<BranchType> for git2::BranchType {
-
    fn from(other: BranchType) -> Self {
-
        match other {
-
            BranchType::Local => git2::BranchType::Local,
-
            BranchType::Remote { .. } => git2::BranchType::Remote,
+
use std::{
+
    convert::TryFrom,
+
    str::{self, FromStr},
+
};
+

+
use crate::git::refstr_join;
+
use git_ref_format::{component, lit, Component, Qualified, RefStr, RefString};
+

+
/// A `Branch` represents any git branch. This can either be a reference
+
/// that is under the `refs/heads` or `refs/remotes` namespace.
+
///
+
/// Note that if a `Branch` is created from a [`git2::Reference`] then
+
/// any `refs/namespaces` will be stripped.
+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+
pub enum Branch {
+
    Local(Local),
+
    Remote(Remote),
+
}
+

+
impl Branch {
+
    /// Construct a [`Local`] branch.
+
    pub fn local<R>(name: R) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        Self::Local(Local::new(name))
+
    }
+

+
    /// Construct a [`Remote`] branch.
+
    /// The `remote` is the remote name of the reference name while
+
    /// the `name` is the suffix, i.e. `refs/remotes/<remote>/<name>`.
+
    pub fn remote<R>(remote: Component<'_>, name: R) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        Self::Remote(Remote::new(remote, name))
+
    }
+

+
    pub fn refname(&self) -> Qualified {
+
        match self {
+
            Branch::Local(local) => local.refname(),
+
            Branch::Remote(remote) => remote.refname(),
        }
    }
}

-
impl From<git2::BranchType> for BranchType {
-
    fn from(other: git2::BranchType) -> Self {
-
        match other {
-
            git2::BranchType::Local => BranchType::Local,
-
            git2::BranchType::Remote => BranchType::Remote { name: None },
-
        }
+
impl TryFrom<&git2::Reference<'_>> for Branch {
+
    type Error = error::Branch;
+

+
    fn try_from(reference: &git2::Reference<'_>) -> Result<Self, Self::Error> {
+
        let name = str::from_utf8(reference.name_bytes())?;
+
        Self::from_str(name)
    }
}

-
/// A newtype wrapper over `String` to separate out the fact that a caller wants
-
/// to fetch a branch.
-
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct BranchName(String);
+
impl TryFrom<&str> for Branch {
+
    type Error = error::Branch;

-
impl fmt::Display for BranchName {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        self.0.fmt(f)
+
    fn try_from(name: &str) -> Result<Self, Self::Error> {
+
        Self::from_str(name)
    }
}

-
impl TryFrom<&[u8]> for BranchName {
-
    type Error = Error;
+
impl FromStr for Branch {
+
    type Err = error::Branch;

-
    fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
-
        let name = str::from_utf8(name)?;
-
        let short_name = match git::ext::try_extract_refname(name) {
-
            Ok(stripped) => stripped,
-
            Err(original) => original,
+
    fn from_str(name: &str) -> Result<Self, Self::Err> {
+
        let name = RefStr::try_from_str(name)?;
+
        let name = match name.to_namespaced() {
+
            None => name
+
                .qualified()
+
                .ok_or_else(|| error::Branch::NotQualified(name.to_string()))?,
+
            Some(name) => name.strip_namespace_recursive(),
        };
-
        Ok(Self(short_name))
+

+
        let (_ref, category, c, cs) = name.non_empty_components();
+

+
        if category == component::HEADS {
+
            Ok(Self::Local(Local::new(refstr_join(c, cs))))
+
        } else if category == component::REMOTES {
+
            Ok(Self::Remote(Remote::new(c, cs.collect::<RefString>())))
+
        } else {
+
            Err(error::Branch::NotBranch(name.into()))
+
        }
    }
}

-
impl BranchName {
-
    /// Create a new `BranchName`.
-
    pub fn new(name: &str) -> Self {
-
        Self(name.into())
+
/// A `Local` represents a local branch, i.e. it is a reference under
+
/// `refs/heads`.
+
///
+
/// Note that if a `Local` is created from a [`git2::Reference`] then
+
/// any `refs/namespaces` will be stripped.
+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+
pub struct Local {
+
    name: RefString,
+
}
+

+
impl Local {
+
    /// Construct a new `Local` with the given `name`.
+
    ///
+
    /// If the name is qualified with `refs/heads`, this will be
+
    /// shortened to the suffix. To get the `Qualified` name again,
+
    /// use [`Local::refname`].
+
    pub fn new<R>(name: R) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        match name.as_ref().qualified() {
+
            None => Self {
+
                name: name.as_ref().to_ref_string(),
+
            },
+
            Some(qualified) => {
+
                let (_refs, heads, c, cs) = qualified.non_empty_components();
+
                if heads == component::HEADS {
+
                    Self {
+
                        name: refstr_join(c, cs),
+
                    }
+
                } else {
+
                    Self {
+
                        name: name.as_ref().to_ref_string(),
+
                    }
+
                }
+
            },
+
        }
    }

-
    /// Access the string value of the `BranchName`.
-
    pub fn name(&self) -> &str {
-
        &self.0
+
    /// Return the fully qualified `Local` refname,
+
    /// e.g. `refs/heads/fix/ref-format`.
+
    pub fn refname(&self) -> Qualified {
+
        lit::refs_heads(&self.name).into()
    }
}

-
/// The static information of a `git2::Branch`.
-
///
-
/// **Note**: The `PartialOrd` and `Ord` implementations compare on `BranchName`
-
/// only.
-
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Branch {
-
    /// Name identifier of the `Branch`.
-
    pub name: BranchName,
-
    /// Whether the `Branch` is `Remote` or `Local`.
-
    pub locality: BranchType,
-
}
+
impl TryFrom<&git2::Reference<'_>> for Local {
+
    type Error = error::Local;

-
impl PartialOrd for Branch {
-
    fn partial_cmp(&self, other: &Branch) -> Option<Ordering> {
-
        Some(self.cmp(other))
+
    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+
        let name = str::from_utf8(reference.name_bytes())?;
+
        Self::from_str(name)
    }
}

-
impl Ord for Branch {
-
    fn cmp(&self, other: &Branch) -> Ordering {
-
        self.name.cmp(&other.name)
+
impl TryFrom<&str> for Local {
+
    type Error = error::Local;
+

+
    fn try_from(name: &str) -> Result<Self, Self::Error> {
+
        Self::from_str(name)
    }
}

-
impl Branch {
-
    /// Helper to create a remote `Branch` with a name
-
    pub fn remote(name: &str, remote: &str) -> Self {
-
        Self {
-
            name: BranchName(name.to_string()),
-
            locality: BranchType::Remote {
-
                name: Some(remote.to_string()),
-
            },
+
impl FromStr for Local {
+
    type Err = error::Local;
+

+
    fn from_str(name: &str) -> Result<Self, Self::Err> {
+
        let name = RefStr::try_from_str(name)?;
+
        let name = match name.to_namespaced() {
+
            None => name
+
                .qualified()
+
                .ok_or_else(|| error::Local::NotQualified(name.to_string()))?,
+
            Some(name) => name.strip_namespace_recursive(),
+
        };
+

+
        let (_ref, heads, c, cs) = name.non_empty_components();
+
        if heads == component::HEADS {
+
            Ok(Self::new(refstr_join(c, cs)))
+
        } else {
+
            Err(error::Local::NotHeads(name.into()))
        }
    }
+
}
+

+
/// A `Remote` represents a remote branch, i.e. it is a reference under
+
/// `refs/remotes`.
+
///
+
/// Note that if a `Remote` is created from a [`git2::Reference`] then
+
/// any `refs/namespaces` will be stripped.
+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+
pub struct Remote {
+
    remote: RefString,
+
    name: RefString,
+
}

-
    /// Helper to create a remote `Branch` with a name
-
    pub fn local(name: &str) -> Self {
+
impl Remote {
+
    /// Construct a new `Remote` with the given `name` and `remote`.
+
    ///
+
    /// ## Note
+
    /// `name` is expected to be in short form, i.e. not begin with
+
    /// `refs`.
+
    ///
+
    /// If you are creating a `Remote` with a name that begins with
+
    /// `refs/remotes`, use [`Remote::from_refs_remotes`] instead.
+
    ///
+
    /// To get the `Qualified` name, use [`Remote::refname`].
+
    pub fn new<R>(remote: Component, name: R) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
        Self {
-
            name: BranchName(name.to_string()),
-
            locality: BranchType::Local,
+
            name: name.as_ref().to_ref_string(),
+
            remote: remote.to_ref_string(),
        }
    }

-
    /// Get the name of the `Branch`.
-
    pub fn refname(&self) -> String {
-
        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.to_string(),
-
                Some(remote_name) => format!("refs/remotes/{}/{}", remote_name, branch_name),
-
            },
-
        }
+
    /// Parse the `name` from the form `refs/remotes/<remote>/<rest>`.
+
    ///
+
    /// If the `name` is not of this form, then `None` is returned.
+
    pub fn from_refs_remotes<R>(name: R) -> Option<Self>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        let qualified = name.as_ref().qualified()?;
+
        let (_refs, remotes, remote, cs) = qualified.non_empty_components();
+
        (remotes == component::REMOTES).then_some(Self {
+
            name: cs.collect(),
+
            remote: remote.to_ref_string(),
+
        })
+
    }
+

+
    /// Give back the fully qualified `Remote` refname,
+
    /// e.g. `refs/remotes/origin/fix/ref-format`.
+
    pub fn refname(&self) -> Qualified {
+
        lit::refs_remotes(self.remote.join(&self.name)).into()
    }
}

-
impl<'repo> TryFrom<git2::Reference<'repo>> for Branch {
-
    type Error = Error;
+
impl TryFrom<&git2::Reference<'_>> for Remote {
+
    type Error = error::Remote;

-
    fn try_from(reference: git2::Reference) -> Result<Self, Self::Error> {
-
        let is_remote = ext::is_remote(&reference);
-
        let is_tag = reference.is_tag();
-
        let is_note = reference.is_note();
-
        let name = BranchName::try_from(reference.name_bytes())?;
+
    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+
        let name = str::from_utf8(reference.name_bytes())?;
+
        Self::from_str(name)
+
    }
+
}

-
        // Best effort to not return tags or notes. Assuming everything after that is a
-
        // branch.
-
        if is_tag || is_note {
-
            return Err(Error::NotBranch(name));
-
        }
+
impl TryFrom<&str> for Remote {
+
    type Error = error::Remote;

-
        if is_remote {
-
            let mut split = name.0.splitn(2, '/');
-
            let remote_name = split
-
                .next()
-
                .ok_or_else(|| Error::ParseRemoteBranch(name.clone()))?;
-
            let name = split
-
                .next()
-
                .ok_or_else(|| Error::ParseRemoteBranch(name.clone()))?;
-

-
            Ok(Self {
-
                name: BranchName(name.to_string()),
-
                locality: BranchType::Remote {
-
                    name: Some(remote_name.to_string()),
-
                },
-
            })
+
    fn try_from(name: &str) -> Result<Self, Self::Error> {
+
        Self::from_str(name)
+
    }
+
}
+

+
impl FromStr for Remote {
+
    type Err = error::Remote;
+

+
    fn from_str(name: &str) -> Result<Self, Self::Err> {
+
        let name = RefStr::try_from_str(name)?;
+
        let name = match name.to_namespaced() {
+
            None => name
+
                .qualified()
+
                .ok_or_else(|| error::Remote::NotQualified(name.to_string()))?,
+
            Some(name) => name.strip_namespace_recursive(),
+
        };
+

+
        let (_ref, remotes, remote, cs) = name.non_empty_components();
+
        if remotes == component::REMOTES {
+
            Ok(Self::new(remote, cs.collect::<RefString>()))
        } else {
-
            Ok(Self {
-
                name,
-
                locality: BranchType::Local,
-
            })
+
            Err(error::Remote::NotRemotes(name.into()))
        }
    }
}
+

+
pub mod error {
+
    use git_ref_format::RefString;
+
    use thiserror::Error;
+

+
    #[derive(Debug, Error)]
+
    pub enum Branch {
+
        #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
+
        NotBranch(RefString),
+
        #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
+
        NotQualified(String),
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] std::str::Utf8Error),
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum Local {
+
        #[error("the refname '{0}' did not begin with 'refs/heads'")]
+
        NotHeads(RefString),
+
        #[error("the refname '{0}' did not begin with 'refs/heads'")]
+
        NotQualified(String),
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] std::str::Utf8Error),
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum Remote {
+
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
+
        NotQualified(String),
+
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
+
        NotRemotes(RefString),
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] std::str::Utf8Error),
+
    }
+
}
modified radicle-surf/src/git/commit.rs
@@ -17,8 +17,9 @@

use crate::{
    file_system::{self, directory},
-
    git::{error::Error, Branch, RepositoryRef, Tag, TagName},
+
    git::{error::Error, Branch, RepositoryRef, Tag},
};
+
use git_ref_format::{Qualified, RefString};
use radicle_git_ext::Oid;
use std::{convert::TryFrom, str};

@@ -216,7 +217,13 @@ impl ToCommit for &Tag {
    }
}

-
impl ToCommit for &TagName {
+
impl ToCommit for &RefString {
+
    fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
+
        repo.commit(self)
+
    }
+
}
+

+
impl ToCommit for &Qualified<'_> {
    fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
        repo.commit(self)
    }
modified radicle-surf/src/git/error.rs
@@ -18,31 +18,18 @@
//! Collection of errors and helper instances that can occur when performing
//! operations from [`crate::git`].

-
use crate::{
-
    diff,
-
    file_system,
-
    git::{BranchName, Namespace, TagName},
-
};
+
use crate::{diff, file_system, git, git::Namespace};
use std::str;
use thiserror::Error;

/// Enumeration of errors that can occur in operations from [`crate::git`].
-
#[derive(Debug, PartialEq, Error)]
+
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
-
    /// The user tried to fetch a branch, but the name provided does not
-
    /// exist as a branch. This could mean that the branch does not exist
-
    /// or that a tag or commit was provided by accident.
-
    #[error("provided branch name does not exist: {0}")]
-
    NotBranch(BranchName),
-
    /// We tried to convert a name into its remote and branch name parts.
-
    #[error("could not parse '{0}' into a remote name and branch name")]
-
    ParseRemoteBranch(BranchName),
-
    /// The user tried to fetch a tag, but the name provided does not
-
    /// exist as a tag. This could mean that the tag does not exist
-
    /// or that a branch or commit was provided by accident.
-
    #[error("provided tag name does not exist: {0}")]
-
    NotTag(TagName),
+
    #[error(transparent)]
+
    BranchIter(#[from] git::repo::iter::error::Branch),
+
    #[error(transparent)]
+
    TagIter(#[from] git::repo::iter::error::Tag),
    /// A `revspec` was provided that could not be parsed into a branch, tag, or
    /// commit object.
    #[error("provided revspec '{rev}' could not be parsed into a git object")]
deleted radicle-surf/src/git/ext.rs
@@ -1,68 +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/>.
-

-
#![allow(missing_docs)]
-

-
/// Try to strip any refs/namespaces, refs/heads, refs/remotes, and
-
/// refs/tags. If this fails we return the original string.
-
pub fn try_extract_refname(spec: &str) -> Result<String, String> {
-
    let re = regex::Regex::new(r"(refs/namespaces/.*?/)*refs/(remotes/(.*?)/)?(heads/|tags/)?(.*)")
-
        .unwrap();
-

-
    re.captures(spec)
-
        .and_then(|c| {
-
            let mut result = String::new();
-
            if let Some(remote) = c.get(3).map(|m| m.as_str()) {
-
                result.push_str(remote);
-
                result.push('/');
-
            }
-
            result.push_str(c.get(5).map(|m| m.as_str())?);
-
            Some(result)
-
        })
-
        .ok_or_else(|| spec.to_string())
-
}
-

-
/// [`git2::Reference::is_tag`] just does a check for the prefix of `tags/`.
-
/// The issue with that is, as soon as we're in 'namespaces' ref that
-
/// is a tag it will say that it's not a tag. Instead we do a regex check on
-
/// `refs/tags/.*`.
-
pub fn is_tag(reference: &git2::Reference) -> bool {
-
    let re = regex::Regex::new(r"refs/tags/.*").unwrap();
-
    // If we couldn't parse the name we say it's not a tag.
-
    match reference.name() {
-
        Some(name) => re.is_match(name),
-
        None => false,
-
    }
-
}
-

-
pub fn is_branch(reference: &git2::Reference) -> bool {
-
    let re = regex::Regex::new(r"refs/heads/.*|refs/remotes/.*/.*").unwrap();
-
    // If we couldn't parse the name we say it's not a branch.
-
    match reference.name() {
-
        Some(name) => re.is_match(name),
-
        None => false,
-
    }
-
}
-

-
pub fn is_remote(reference: &git2::Reference) -> bool {
-
    let re = regex::Regex::new(r"refs/remotes/.*/.*").unwrap();
-
    // If we couldn't parse the name we say it's not a remote branch.
-
    match reference.name() {
-
        Some(name) => re.is_match(name),
-
        None => false,
-
    }
-
}
modified radicle-surf/src/git/repo.rs
@@ -18,26 +18,16 @@
use crate::{
    diff::*,
    file_system,
-
    file_system::{directory, DirectoryEntry, Label},
-
    git::{
-
        error::*,
-
        Branch,
-
        BranchName,
-
        Commit,
-
        Glob,
-
        History,
-
        Namespace,
-
        Revision,
-
        Signature,
-
        Stats,
-
        Tag,
-
        TagName,
+
    file_system::{
+
        directory::{self, Directory, DirectoryEntry, FileContent},
+
        Label,
    },
+
    git::{error::*, Branch, Commit, Glob, History, Namespace, Revision, Signature, Stats, Tag},
};
-
use directory::{Directory, FileContent};
+
use git_ref_format::RefString;
use radicle_git_ext::Oid;
use std::{
-
    collections::{btree_set, BTreeMap, BTreeSet},
+
    collections::{BTreeMap, BTreeSet},
    convert::TryFrom,
    path::PathBuf,
    str,
@@ -45,6 +35,9 @@ use std::{

use super::commit::ToCommit;

+
pub mod iter;
+
pub use iter::{Branches, Namespaces, Tags};
+

/// Wrapper around the `git2`'s `git2::Repository` type.
/// This is to to limit the functionality that we can do
/// on the underlying object.
@@ -74,67 +67,6 @@ 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> {
@@ -146,28 +78,22 @@ 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 {
-
            references: vec![],
-
            current: 0,
-
        };
+
        let mut branches = Branches::default();
        for glob in pattern.globs().iter() {
            let namespaced = self.namespaced_refname(glob)?;
            let references = self.repo_ref.references_glob(&namespaced)?;
-
            branches.references.push(references);
+
            branches.push(references);
        }
        Ok(branches)
    }

    /// 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,
-
        };
+
        let mut tags = Tags::default();
        for glob in pattern.globs().iter() {
            let namespaced = self.namespaced_refname(glob)?;
            let references = self.repo_ref.references_glob(&namespaced)?;
-
            tags.references.push(references);
+
            tags.push(references);
        }
        Ok(tags)
    }
@@ -187,9 +113,7 @@ impl<'a> RepositoryRef<'a> {
                .collect::<Result<BTreeSet<Namespace>, Error>>()?;
            set.extend(new_set);
        }
-
        Ok(Namespaces {
-
            namespaces: set.into_iter(),
-
        })
+
        Ok(Namespaces::new(set))
    }

    /// Get the [`Diff`] between two commits.
@@ -341,21 +265,22 @@ impl<'a> RepositoryRef<'a> {
    }

    /// Lists branch names with `filter`.
-
    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?;
+
    pub fn branch_names(&self, filter: &Glob<Branch>) -> Result<Vec<RefString>, Error> {
+
        let mut branches = self
+
            .branches(filter)?
+
            .map(|b| b.map(|b| b.refname().into()))
+
            .collect::<Result<Vec<_>, _>>()?;
        branches.sort();

        Ok(branches)
    }

    /// Lists tag names in the local RefScope.
-
    pub fn tag_names(&self) -> Result<Vec<TagName>, Error> {
+
    pub fn tag_names(&self) -> Result<Vec<RefString>, Error> {
        let mut tags = self
            .tags(&Glob::tags("*")?)?
-
            .map(|t| t.map(|t| t.name()))
-
            .collect::<Result<Vec<TagName>, Error>>()?;
+
            .map(|t| t.map_err(Error::from).map(|t| t.refname().into()))
+
            .collect::<Result<Vec<_>, Error>>()?;
        tags.sort();

        Ok(tags)
added radicle-surf/src/git/repo/iter.rs
@@ -0,0 +1,122 @@
+
// I think the following `Tags` and `Branches` would be merged
+
// using Generic associated types supported in Rust 1.65.0.
+

+
use std::{
+
    collections::{btree_set, BTreeSet},
+
    convert::TryFrom as _,
+
};
+

+
use crate::git::{Branch, Namespace, Tag};
+

+
/// An iterator for tags.
+
#[derive(Default)]
+
pub struct Tags<'a> {
+
    references: Vec<git2::References<'a>>,
+
    current: usize,
+
}
+

+
impl<'a> Tags<'a> {
+
    pub(super) fn push(&mut self, references: git2::References<'a>) {
+
        self.references.push(references)
+
    }
+
}
+

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

+
    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::Tag::from)
+
                                .and_then(|r| Tag::try_from(&r).map_err(error::Tag::from)),
+
                        );
+
                    },
+
                    None => self.current += 1,
+
                },
+
                None => break,
+
            }
+
        }
+
        None
+
    }
+
}
+

+
/// An iterator for branches.
+
#[derive(Default)]
+
pub struct Branches<'a> {
+
    references: Vec<git2::References<'a>>,
+
    current: usize,
+
}
+

+
impl<'a> Branches<'a> {
+
    pub(super) fn push(&mut self, references: git2::References<'a>) {
+
        self.references.push(references)
+
    }
+
}
+

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

+
    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::Branch::from)
+
                                .and_then(|r| Branch::try_from(&r).map_err(error::Branch::from)),
+
                        )
+
                    },
+
                    None => self.current += 1,
+
                },
+
                None => break,
+
            }
+
        }
+
        None
+
    }
+
}
+

+
// TODO: not sure this buys us much
+
/// An iterator for namespaces.
+
pub struct Namespaces {
+
    namespaces: btree_set::IntoIter<Namespace>,
+
}
+

+
impl Namespaces {
+
    pub(super) fn new(namespaces: BTreeSet<Namespace>) -> Self {
+
        Self {
+
            namespaces: namespaces.into_iter(),
+
        }
+
    }
+
}
+

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

+
pub mod error {
+
    use thiserror::Error;
+

+
    use crate::git::{branch, tag};
+

+
    #[derive(Debug, Error)]
+
    pub enum Branch {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error(transparent)]
+
        Branch(#[from] branch::error::Branch),
+
    }
+

+
    #[derive(Debug, Error)]
+
    pub enum Tag {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error(transparent)]
+
        Tag(#[from] tag::error::FromReference),
+
    }
+
}
modified radicle-surf/src/git/tag.rs
@@ -1,67 +1,9 @@
-
// 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::{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(RefString);
-

-
impl fmt::Display for TagName {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        self.0.fmt(f)
-
    }
-
}
+
use std::{convert::TryFrom, str};

-
impl TryFrom<&[u8]> for TagName {
-
    type Error = Error;
+
use git_ext::Oid;
+
use git_ref_format::{component, lit, Qualified, RefStr, RefString};

-
    fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
-
        let name = str::from_utf8(name)?;
-
        let short_name = match git::ext::try_extract_refname(name) {
-
            Ok(stripped) => stripped,
-
            Err(original) => original,
-
        };
-
        let refstring = RefString::try_from(short_name)?;
-
        Ok(Self(refstring))
-
    }
-
}
-

-
impl TagName {
-
    /// Create a new `TagName`.
-
    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())
-
    }
-
}
+
use crate::git::{refstr_join, Author};

/// The static information of a [`git2::Tag`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
@@ -70,25 +12,21 @@ pub enum Tag {
    Light {
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
        id: Oid,
-
        /// The name that references this `Tag`.
-
        name: TagName,
-
        /// If the tag is provided this holds the remote’s name.
-
        remote: Option<String>,
+
        /// The reference name for this `Tag`.
+
        name: RefString,
    },
    /// An annotated git tag.
    Annotated {
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
        id: Oid,
        /// The Object ID for the object that is tagged.
-
        target_id: Oid,
-
        /// The name that references this `Tag`.
-
        name: TagName,
+
        target: Oid,
+
        /// The reference name for this `Tag`.
+
        name: RefString,
        /// The named author of this `Tag`, if the `Tag` was annotated.
        tagger: Option<Author>,
        /// The message with this `Tag`, if the `Tag` was annotated.
        message: Option<String>,
-
        /// If the tag is provided this holds the remote’s name.
-
        remote: Option<String>,
    },
}

@@ -101,32 +39,62 @@ impl Tag {
        }
    }

-
    /// Get the `TagName` of the tag, regardless of its type.
-
    pub fn name(&self) -> TagName {
-
        match self {
-
            Self::Light { name, .. } => name.clone(),
-
            Self::Annotated { name, .. } => name.clone(),
-
        }
+
    /// Return the fully qualified `Tag` refname,
+
    /// e.g. `refs/tags/release/v1`.
+
    pub fn refname(&self) -> Qualified {
+
        lit::refs_tags(self.name()).into()
    }

-
    /// Returns the full ref name of the tag.
-
    pub fn refname(&self) -> String {
-
        self.name().refname()
+
    fn name(&self) -> &RefString {
+
        match &self {
+
            Tag::Light { name, .. } => name,
+
            Tag::Annotated { name, .. } => name,
+
        }
    }
}

-
impl<'repo> TryFrom<git2::Tag<'repo>> for Tag {
-
    type Error = Error;
+
pub mod error {
+
    use std::str;

-
    fn try_from(tag: git2::Tag) -> Result<Self, Self::Error> {
-
        let id = tag.id().into();
+
    use git_ref_format::RefString;
+
    use thiserror::Error;

-
        let target_id = tag.target_id().into();
+
    #[derive(Debug, Error)]
+
    pub enum FromTag {
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] str::Utf8Error),
+
    }

-
        let name = TagName::try_from(tag.name_bytes())?;
+
    #[derive(Debug, Error)]
+
    pub enum FromReference {
+
        #[error(transparent)]
+
        FromTag(#[from] FromTag),
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
+
        NotQualified(String),
+
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
+
        NotTag(RefString),
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] str::Utf8Error),
+
    }
+
}

-
        let tagger = tag.tagger().map(Author::try_from).transpose()?;
+
impl TryFrom<&git2::Tag<'_>> for Tag {
+
    type Error = error::FromTag;

+
    fn try_from(tag: &git2::Tag) -> Result<Self, Self::Error> {
+
        let id = tag.id().into();
+
        let target = tag.target_id().into();
+
        let name = {
+
            let name = str::from_utf8(tag.name_bytes())?;
+
            RefStr::try_from_str(name)?.to_ref_string()
+
        };
+
        let tagger = tag.tagger().map(Author::try_from).transpose()?;
        let message = tag
            .message_bytes()
            .map(str::from_utf8)
@@ -135,49 +103,47 @@ impl<'repo> TryFrom<git2::Tag<'repo>> for Tag {

        Ok(Tag::Annotated {
            id,
-
            target_id,
+
            target,
            name,
            tagger,
            message,
-
            remote: None,
        })
    }
}

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

-
    fn try_from(reference: git2::Reference) -> Result<Self, Self::Error> {
-
        let name = TagName::try_from(reference.name_bytes())?;
+
impl TryFrom<&git2::Reference<'_>> for Tag {
+
    type Error = error::FromReference;

-
        let (remote, name) = if git::ext::is_remote(&reference) {
-
            let mut split = name.0.splitn(2, '/');
-
            let remote = split.next().map(|x| x.to_owned());
-
            let name = split.next().unwrap();
-
            (remote, TagName::new(name)?)
-
        } else {
-
            (None, name)
+
    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+
        let name = {
+
            let name = str::from_utf8(reference.name_bytes())?;
+
            RefStr::try_from_str(name)?
+
                .qualified()
+
                .ok_or_else(|| error::FromReference::NotQualified(name.to_string()))?
        };

-
        match reference.peel_to_tag() {
-
            Ok(tag) => Ok(Tag::try_from(tag)?),
-
            Err(err) => {
+
        let (_refs, tags, c, cs) = name.non_empty_components();
+

+
        if tags == component::TAGS {
+
            match reference.peel_to_tag() {
+
                Ok(tag) => Tag::try_from(&tag).map_err(error::FromReference::from),
                // If we get an error peeling to a tag _BUT_ we also have confirmed the
                // reference is a tag, that means we have a lightweight tag,
                // i.e. a commit SHA and name.
-
                if err.class() == git2::ErrorClass::Object
-
                    && err.code() == git2::ErrorCode::InvalidSpec
+
                Err(err)
+
                    if err.class() == git2::ErrorClass::Object
+
                        && err.code() == git2::ErrorCode::InvalidSpec =>
                {
                    let commit = reference.peel_to_commit()?;
                    Ok(Tag::Light {
                        id: commit.id().into(),
-
                        name,
-
                        remote,
+
                        name: refstr_join(c, cs),
                    })
-
                } else {
-
                    Err(err.into())
-
                }
-
            },
+
                },
+
                Err(err) => Err(err.into()),
+
            }
+
        } else {
+
            Err(error::FromReference::NotTag(name.into()))
        }
    }
}
modified radicle-surf/src/lib.rs
@@ -80,6 +80,11 @@
//! # Ok(())
//! # }
//! ```
+

+
pub extern crate git_ref_format;
+

+
extern crate radicle_git_ext as git_ext;
+

pub mod diff;
pub mod file_system;
pub mod git;
modified radicle-surf/src/object/blob.rs
@@ -114,25 +114,21 @@ impl Serialize for BlobContent {
///
/// Will return [`Error`] if the project doesn't exist or a surf interaction
/// fails.
-
pub fn blob<P>(
+
pub fn blob(
    repo: &RepositoryRef,
-
    maybe_revision: Option<Revision<P>>,
+
    maybe_revision: Option<Revision>,
    path: &str,
-
) -> Result<Blob, Error>
-
where
-
    P: ToString,
-
{
+
) -> Result<Blob, Error> {
    make_blob(repo, maybe_revision, path, content)
}

-
fn make_blob<P, C>(
+
fn make_blob<C>(
    repo: &RepositoryRef,
-
    maybe_revision: Option<Revision<P>>,
+
    maybe_revision: Option<Revision>,
    path: &str,
    content: C,
) -> Result<Blob, Error>
where
-
    P: ToString,
    C: FnOnce(&[u8]) -> BlobContent,
{
    let revision = maybe_revision.unwrap();
modified radicle-surf/src/object/tree.rs
@@ -20,6 +20,7 @@

use std::str::FromStr as _;

+
use git_ref_format::refname;
#[cfg(feature = "serialize")]
use serde::{
    ser::{SerializeStruct as _, Serializer},
@@ -85,20 +86,17 @@ impl Serialize for TreeEntry {
/// # Errors
///
/// Will return [`Error`] if any of the surf interactions fail.
-
pub fn tree<P>(
+
pub fn tree(
    repo: &RepositoryRef,
-
    maybe_revision: Option<Revision<P>>,
+
    maybe_revision: Option<Revision>,
    maybe_prefix: Option<String>,
-
) -> Result<Tree, Error>
-
where
-
    P: ToString,
-
{
+
) -> Result<Tree, Error> {
    let prefix = maybe_prefix.unwrap_or_default();
    let rev = match maybe_revision {
        Some(r) => r,
        None => Revision::Branch {
-
            name: "main".to_string(),
-
            peer_id: None,
+
            name: refname!("main"),
+
            remote: None,
        },
    };

modified radicle-surf/src/revision.rs
@@ -17,6 +17,7 @@

//! Represents revisions

+
use git_ref_format::{lit, Qualified, RefString};
use nonempty::NonEmpty;

#[cfg(feature = "serialize")]
@@ -24,7 +25,7 @@ use serde::{Deserialize, Serialize};

use radicle_git_ext::Oid;

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

/// Types of a peer.
pub enum Category<P, U> {
@@ -51,20 +52,20 @@ pub enum Category<P, U> {
    serde(rename_all = "camelCase", tag = "type")
)]
#[derive(Debug, Clone)]
-
pub enum Revision<P> {
+
pub enum Revision {
    /// Select a tag under the name provided.
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
    Tag {
        /// Name of the tag.
-
        name: String,
+
        name: RefString,
    },
    /// Select a branch under the name provided.
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
    Branch {
        /// Name of the branch.
-
        name: String,
+
        name: RefString,
        /// The remote peer, if specified.
-
        peer_id: Option<P>,
+
        remote: Option<RefString>,
    },
    /// Select a SHA1 under the name provided.
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
@@ -74,39 +75,31 @@ pub enum Revision<P> {
    },
}

-
impl<P> git::Revision for &Revision<P>
-
where
-
    P: ToString,
-
{
+
impl git::Revision for &Revision {
    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::Tag { name } => match name.qualified() {
+
                None => Qualified::from(lit::refs_tags(name)).object_id(repo),
+
                Some(name) => name.object_id(repo),
            },
-
            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::Branch { name, remote } => match remote {
+
                Some(remote) => {
+
                    Qualified::from(lit::refs_remotes(remote.join(name))).object_id(repo)
+
                },
+
                None => git::Branch::local(name).refname().object_id(repo),
            },
            Revision::Sha { sha } => Ok(*sha),
        }
    }
}

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

-
/// Bundled response to retrieve both [`BranchName`]es and [`TagName`]s for
+
/// Bundled response to retrieve both branches and tags for
/// a user's repo.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Revisions<P, U> {
@@ -114,10 +107,10 @@ pub struct Revisions<P, U> {
    pub peer_id: P,
    /// The user who owns these revisions.
    pub user: U,
-
    /// List of [`git::BranchName`].
-
    pub branches: NonEmpty<BranchName>,
-
    /// List of [`git::TagName`].
-
    pub tags: Vec<TagName>,
+
    /// List of branch reference names.
+
    pub branches: NonEmpty<RefString>,
+
    /// List of tag reference names.
+
    pub tags: Vec<RefString>,
}

/// Provide the [`Revisions`] for the given `peer_id`, looking for the
modified radicle-surf/t/Cargo.toml
@@ -16,7 +16,7 @@ test = []
[dev-dependencies]
nonempty = "0.5"
pretty_assertions = "1.3.0"
-
proptest = "0.9"
+
proptest = "1"
serde_json = "1"

[dev-dependencies.git2]
@@ -24,6 +24,14 @@ version = "0.15.0"
default-features = false
features = ["vendored-libgit2"]

+
[dev-dependencies.git-ref-format]
+
path = "../../git-ref-format"
+
features = ["macro"]
+

+
[dev-dependencies.git-ref-format-test]
+
path = "../../git-ref-format/t"
+
features = ["test"]
+

[dev-dependencies.radicle-git-ext]
path = "../../radicle-git-ext"

modified radicle-surf/t/src/file_system.rs
@@ -30,6 +30,7 @@ mod path {

#[cfg(test)]
mod directory {
+
    use git_ref_format::refname;
    use radicle_surf::{
        file_system::DirectoryEntry,
        git::{Branch, Repository},
@@ -42,7 +43,7 @@ mod directory {
    fn directory_get_path() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let repo = repo.as_ref();
-
        let root = repo.root_dir(&Branch::local("master")).unwrap();
+
        let root = repo.root_dir(&Branch::local(refname!("master"))).unwrap();

        // get_path for a file.
        let path = Path::new("src/memory.rs");
@@ -79,7 +80,7 @@ mod directory {
    fn directory_size() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let repo = repo.as_ref();
-
        let root = repo.root_dir(&Branch::local("master")).unwrap();
+
        let root = repo.root_dir(&Branch::local(refname!("master"))).unwrap();

        /*
        git-platinum (master) $ ls -l src
modified radicle-surf/t/src/git.rs
@@ -1,14 +1,14 @@
// Copyright © 2022 The Radicle Git Contributors
// SPDX-License-Identifier: GPL-3.0-or-later

-
//! Unit tests for radicle_surf::vcs::git and its submodules.
+
//! Unit tests for radicle_surf::git and its submodules.

#[cfg(feature = "serialize")]
-
use radicle_surf::git::{Author, BranchType, Commit};
+
use radicle_surf::git::{Author, Commit};
use radicle_surf::{
    diff::*,
    file_system::{unsound, DirectoryEntry, Path},
-
    git::{error::Error, Branch, Glob, Namespace, Oid, Repository, TagName},
+
    git::{error::Error, Branch, Glob, Namespace, Oid, Repository},
};

const GIT_PLATINUM: &str = "../data/git-platinum";
@@ -17,22 +17,27 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
#[test]
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
fn test_submodule_failure() {
+
    use git_ref_format::refname;
+

    let repo = Repository::discover(".").unwrap();
-
    repo.as_ref().root_dir(&Branch::local("main")).unwrap();
+
    repo.as_ref()
+
        .root_dir(&Branch::local(refname!("main")))
+
        .unwrap();
}

#[cfg(test)]
mod namespace {
    use super::*;
+
    use git_ref_format::{name::component, refname};
    use pretty_assertions::{assert_eq, assert_ne};

    #[test]
    fn switch_to_banana() -> Result<(), Error> {
        let repo = Repository::open(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let history_master = repo.history(&Branch::local("master"))?;
+
        let history_master = repo.history(&Branch::local(refname!("master")))?;
        repo.switch_namespace("golden")?;
-
        let history_banana = repo.history(&Branch::local("banana"))?;
+
        let history_banana = repo.history(&Branch::local(refname!("banana")))?;

        assert_ne!(history_master.head(), history_banana.head());

@@ -43,28 +48,34 @@ mod namespace {
    fn me_namespace() -> Result<(), Error> {
        let repo = Repository::open(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let history = repo.history(&Branch::local("master"))?;
+
        let history = repo.history(&Branch::local(refname!("master")))?;

-
        assert_eq!(repo.which_namespace(), Ok(None));
+
        assert_eq!(repo.which_namespace().unwrap(), None);

        repo.switch_namespace("me")?;
-
        assert_eq!(repo.which_namespace(), Ok(Some(Namespace::try_from("me")?)));
+
        assert_eq!(
+
            repo.which_namespace().unwrap(),
+
            Some(Namespace::try_from("me")?)
+
        );

-
        let history_feature = repo.history(&Branch::local("feature/#1194"))?;
+
        let history_feature = repo.history(&Branch::local(refname!("feature/#1194")))?;
        assert_eq!(history.head(), history_feature.head());

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

        assert_eq!(expected_branches, branches);

-
        let expected_branches: Vec<Branch> = vec![Branch::remote("feature/#1194", "fein")];
+
        let expected_branches: Vec<Branch> = vec![Branch::remote(
+
            component!("fein"),
+
            refname!("heads/feature/#1194"),
+
        )];
        let mut branches = repo
            .branches(&Glob::remotes("fein/*")?)?
-
            .collect::<Result<Vec<Branch>, Error>>()?;
+
            .collect::<Result<Vec<_>, _>>()?;
        branches.sort();

        assert_eq!(expected_branches, branches);
@@ -76,36 +87,42 @@ mod namespace {
    fn golden_namespace() -> Result<(), Error> {
        let repo = Repository::open(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let history = repo.history(&Branch::local("master"))?;
+
        let history = repo.history(&Branch::local(refname!("master")))?;

-
        assert_eq!(repo.which_namespace(), Ok(None));
+
        assert_eq!(repo.which_namespace().unwrap(), None);

        repo.switch_namespace("golden")?;

        assert_eq!(
-
            repo.which_namespace(),
-
            Ok(Some(Namespace::try_from("golden")?))
+
            repo.which_namespace().unwrap(),
+
            Some(Namespace::try_from("golden")?)
        );

-
        let golden_history = repo.history(&Branch::local("master"))?;
+
        let golden_history = repo.history(&Branch::local(refname!("master")))?;
        assert_eq!(history.head(), golden_history.head());

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

        assert_eq!(expected_branches, branches);

+
        // NOTE: these tests used to remove the categories, i.e. heads & tags, but that
+
        // was specialised logic based on the radicle-link storage layout.
+
        let remote = component!("kickflip");
        let expected_branches: Vec<Branch> = vec![
-
            Branch::remote("fakie/bigspin", "kickflip"),
-
            Branch::remote("heelflip", "kickflip"),
-
            Branch::remote("v0.1.0", "kickflip"),
+
            Branch::remote(remote.clone(), refname!("heads/fakie/bigspin")),
+
            Branch::remote(remote.clone(), refname!("heads/heelflip")),
+
            Branch::remote(remote, refname!("tags/v0.1.0")),
        ];
        let mut branches = repo
            .branches(&Glob::remotes("kickflip/*")?)?
-
            .collect::<Result<Vec<Branch>, Error>>()?;
+
            .collect::<Result<Vec<_>, _>>()?;
        branches.sort();

        assert_eq!(expected_branches, branches);
@@ -117,22 +134,22 @@ mod namespace {
    fn silver_namespace() -> Result<(), Error> {
        let repo = Repository::open(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let history = repo.history(&Branch::local("master"))?;
+
        let history = repo.history(&Branch::local(refname!("master")))?;

-
        assert_eq!(repo.which_namespace(), Ok(None));
+
        assert_eq!(repo.which_namespace().unwrap(), None);

        repo.switch_namespace("golden/silver")?;
        assert_eq!(
-
            repo.which_namespace(),
-
            Ok(Some(Namespace::try_from("golden/silver")?))
+
            repo.which_namespace().unwrap(),
+
            Some(Namespace::try_from("golden/silver")?)
        );
-
        let silver_history = repo.history(&Branch::local("master"))?;
+
        let silver_history = repo.history(&Branch::local(refname!("master")))?;
        assert_ne!(history.head(), silver_history.head());

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

        assert_eq!(expected_branches, branches);
@@ -143,6 +160,8 @@ mod namespace {

#[cfg(test)]
mod rev {
+
    use git_ref_format::{name::component, refname};
+

    use super::*;
    use std::str::FromStr;

@@ -161,7 +180,8 @@ mod rev {
    fn _master() -> Result<(), Error> {
        let repo = Repository::open(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let mut history = repo.history(&Branch::remote("master", "origin"))?;
+
        let mut history =
+
            repo.history(&Branch::remote(component!("origin"), refname!("master")))?;

        let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
        assert!(
@@ -228,7 +248,7 @@ mod rev {
    fn tag() -> Result<(), Error> {
        let repo = Repository::open(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let rev = TagName::new("v0.2.0")?;
+
        let rev = refname!("refs/tags/v0.2.0");
        let history = repo.history(&rev)?;

        let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
@@ -240,6 +260,8 @@ mod rev {

#[cfg(test)]
mod last_commit {
+
    use git_ref_format::refname;
+

    use super::*;
    use std::str::FromStr;

@@ -344,7 +366,7 @@ mod last_commit {
    fn root() {
        let repo = Repository::open(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
-
        let rev = Branch::local("master");
+
        let rev = Branch::local(refname!("master"));
        let root_last_commit_id = repo
            .as_ref()
            .last_commit(Path::root(), &rev)
@@ -353,7 +375,7 @@ mod last_commit {

        let expected_oid = repo
            .as_ref()
-
            .history(&Branch::local("master"))
+
            .history(&Branch::local(refname!("master")))
            .unwrap()
            .head()
            .id;
@@ -365,7 +387,7 @@ mod last_commit {
        let repo = Repository::open(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
        let repo = repo.as_ref();
-
        let history = repo.history(&Branch::local("dev")).unwrap();
+
        let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
        let file_commit = history.by_path(unsound::path::new("~/bin/cat")).next();
        assert!(file_commit.is_some());
        println!("file commit: {:?}", &file_commit);
@@ -375,6 +397,7 @@ mod last_commit {
#[cfg(test)]
mod diff {
    use super::*;
+
    use git_ref_format::refname;
    use pretty_assertions::assert_eq;
    use std::str::FromStr;

@@ -463,7 +486,10 @@ mod diff {
    fn test_branch_diff() -> Result<(), Error> {
        let repo = Repository::open(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let diff = repo.diff(&Branch::local("master"), &Branch::local("dev"))?;
+
        let diff = repo.diff(
+
            &Branch::local(refname!("master")),
+
            &Branch::local(refname!("dev")),
+
        )?;

        println!("Diff two branches: master -> dev");
        println!(
@@ -569,6 +595,7 @@ mod diff {

#[cfg(test)]
mod threading {
+
    use git_ref_format::{name::component, refname};
    use radicle_surf::git::Glob;

    use super::*;
@@ -581,19 +608,21 @@ mod threading {
        let mut branches = locked_repo
            .as_ref()
            .branches(&Glob::heads("*")?.and_remotes("*")?)?
-
            .collect::<Result<Vec<Branch>, Error>>()?;
+
            .collect::<Result<Vec<_>, _>>()?;
        branches.sort();

+
        let origin = component!("origin");
+
        let banana = component!("banana");
        assert_eq!(
            branches,
            vec![
-
                Branch::remote("HEAD", "origin"),
-
                Branch::local("dev"),
-
                Branch::remote("dev", "origin"),
-
                Branch::local("master"),
-
                Branch::remote("master", "origin"),
-
                Branch::remote("orange/pineapple", "banana"),
-
                Branch::remote("pineapple", "banana"),
+
                Branch::local(refname!("dev")),
+
                Branch::local(refname!("master")),
+
                Branch::remote(banana.clone(), refname!("orange/pineapple")),
+
                Branch::remote(banana, refname!("pineapple")),
+
                Branch::remote(origin.clone(), refname!("HEAD")),
+
                Branch::remote(origin.clone(), refname!("dev")),
+
                Branch::remote(origin, refname!("master")),
            ]
        );

@@ -641,28 +670,29 @@ mod commit {
#[cfg(feature = "serialize")]
#[cfg(test)]
mod branch {
-
    use super::*;
+
    use git_ref_format::{RefStr, RefString};
+
    use git_ref_format_test::gen;
    use proptest::prelude::*;
    use test_helpers::roundtrip;

+
    use super::*;
+

    proptest! {
        #[test]
-
        fn prop_test_branch(branch in branch_strategy()) {
+
        fn prop_test_branch(branch in gen_branch()) {
            roundtrip::json(branch)
        }
    }

-
    fn branch_strategy() -> impl Strategy<Value = Branch> {
+
    fn gen_branch() -> impl Strategy<Value = Branch> {
        prop_oneof![
-
            any::<String>().prop_map(|name| Branch {
-
                name: BranchName::new(&name),
-
                locality: BranchType::Local
-
            }),
-
            (any::<String>(), any::<String>()).prop_map(|(name, remote_name)| Branch {
-
                name: BranchName::new(&name),
-
                locality: BranchType::Remote {
-
                    name: Some(remote_name),
-
                },
+
            gen::valid().prop_map(|name| Branch::local(RefString::try_from(name).unwrap())),
+
            (gen::valid(), gen::valid()).prop_map(|(remote, name): (String, String)| {
+
                let remote =
+
                    RefStr::try_from_str(&remote).expect("BUG: reference strings should be valid");
+
                let name =
+
                    RefStr::try_from_str(&name).expect("BUG: reference strings should be valid");
+
                Branch::remote(remote.head(), name)
            })
        ]
    }
@@ -671,7 +701,7 @@ mod branch {
#[cfg(test)]
mod reference {
    use super::*;
-
    use radicle_surf::git::{Glob, Tag};
+
    use radicle_surf::git::Glob;

    #[test]
    fn test_branches() {
@@ -679,7 +709,7 @@ mod reference {
        let repo = repo.as_ref();
        let branches = repo.branches(&Glob::heads("*").unwrap()).unwrap();
        for b in branches {
-
            println!("{}", b.unwrap().name);
+
            println!("{}", b.unwrap().refname());
        }
        let branches = repo
            .branches(&Glob::heads("*").unwrap().and_remotes("banana/*").unwrap())
@@ -696,7 +726,7 @@ mod reference {
        let tags = repo_ref
            .tags(&Glob::tags("*").unwrap())
            .unwrap()
-
            .collect::<Result<Vec<Tag>, Error>>()
+
            .collect::<Result<Vec<_>, _>>()
            .unwrap();
        assert_eq!(tags.len(), 6);
        let root_dir = repo_ref.root_dir(&tags[0]).unwrap();
@@ -722,14 +752,18 @@ mod reference {

mod code_browsing {
    use super::*;
+

+
    use git_ref_format::refname;
    use radicle_surf::{file_system::Directory, git::RepositoryRef};

    #[test]
    fn iterate_root_dir_recursive() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let repo = repo.as_ref();
-
        let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
+

+
        let root_dir = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
        let count = println_dir(&root_dir, &repo, 0);
+

        assert_eq!(count, 36); // Check total file count.

        /// Prints items in `dir` with `indent_level`.
@@ -753,7 +787,7 @@ mod code_browsing {
    fn browse_repo_lazily() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let repo = repo.as_ref();
-
        let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
+
        let root_dir = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
        let count = root_dir.contents(&repo).unwrap().iter().count();
        assert_eq!(count, 8);
        let count = traverse(&root_dir, &repo);
@@ -775,7 +809,7 @@ mod code_browsing {
    fn test_file_history() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let repo = repo.as_ref();
-
        let history = repo.history(&Branch::local("dev")).unwrap();
+
        let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
        let path = unsound::path::new("README.md");
        let mut file_history = history.by_path(path);
        let commit = file_history.next().unwrap().unwrap();