Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'origin/surf/git-ref-format'
Fintan Halpenny committed 3 years ago
commit 0d1bfb89a21981c95f592b9cd9a5074e96304655
parent 46257a8
36 files changed +2046 -2040
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,15 +16,16 @@
// 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},
-
    vcs::git::{Branch, Repository},
+
    git::{Branch, Repository},
};

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/examples/diff.rs
@@ -20,7 +20,7 @@ extern crate radicle_surf;
use std::{env::Args, str::FromStr, time::Instant};

use radicle_git_ext::Oid;
-
use radicle_surf::{diff::Diff, vcs::git};
+
use radicle_surf::{diff::Diff, git};

fn main() {
    let options = get_options_or_exit();
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,10 +27,9 @@ use serde::{
use crate::{
    diff,
    file_system,
-
    git::Glob,
+
    git::{self, Glob, RepositoryRef},
    person::Person,
    revision::Revision,
-
    vcs::git::{self, BranchName, RepositoryRef},
};

use radicle_git_ext::Oid;
@@ -55,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.
@@ -196,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 {
@@ -227,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/diff.rs
@@ -24,7 +24,7 @@ use serde::{ser, Serialize, Serializer};

use crate::{
    file_system::{Directory, Path},
-
    vcs::git::{Error, RepositoryRef},
+
    git::{Error, RepositoryRef},
};

pub mod git;
modified radicle-surf/src/file_system/directory.rs
@@ -22,7 +22,7 @@

use crate::{
    file_system::{error::LabelError, path::*, Error},
-
    vcs::git::{self, RepositoryRef, Revision},
+
    git::{self, RepositoryRef, Revision},
};
use git2::Blob;
use radicle_git_ext::Oid;
added radicle-surf/src/git.rs
@@ -0,0 +1,175 @@
+
// 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 nonempty::NonEmpty;
+
//! use radicle_surf::file_system::{Directory, File, Label, Path, SystemType};
+
//! use radicle_surf::file_system::unsound;
+
//! use radicle_surf::vcs::git::*;
+
//! use std::collections::HashMap;
+
//! use std::str::FromStr;
+
//! # use std::error::Error;
+
//!
+
//! # fn main() -> Result<(), Box<dyn Error>> {
+
//! let repo = Repository::new("./data/git-platinum")?;
+
//!
+
//! // Pin the browser to a parituclar commit.
+
//! let pin_commit = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
//! let mut browser = Browser::new(&repo, Branch::local("master"))?;
+
//! browser.commit(pin_commit)?;
+
//!
+
//! let directory = browser.get_directory()?;
+
//! let mut directory_contents = directory.list_directory();
+
//! directory_contents.sort();
+
//!
+
//! assert_eq!(directory_contents, vec![
+
//!     SystemType::file(unsound::label::new(".i-am-well-hidden")),
+
//!     SystemType::file(unsound::label::new(".i-too-am-hidden")),
+
//!     SystemType::file(unsound::label::new("README.md")),
+
//!     SystemType::directory(unsound::label::new("bin")),
+
//!     SystemType::directory(unsound::label::new("src")),
+
//!     SystemType::directory(unsound::label::new("text")),
+
//!     SystemType::directory(unsound::label::new("this")),
+
//! ]);
+
//!
+
//! // find src directory in the Git directory and the in-memory directory
+
//! let src_directory = directory
+
//!     .find_directory(Path::new(unsound::label::new("src")))
+
//!     .expect("failed to find src");
+
//! let mut src_directory_contents = src_directory.list_directory();
+
//! src_directory_contents.sort();
+
//!
+
//! assert_eq!(src_directory_contents, vec![
+
//!     SystemType::file(unsound::label::new("Eval.hs")),
+
//!     SystemType::file(unsound::label::new("Folder.svelte")),
+
//!     SystemType::file(unsound::label::new("memory.rs")),
+
//! ]);
+
//! #
+
//! # Ok(())
+
//! # }
+
//! ```
+

+
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;
+
pub use repo::{Repository, RepositoryRef};
+

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

+
mod history;
+
pub use history::History;
+

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

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

+
/// Provides the data for talking about tags.
+
pub mod tag;
+
pub use tag::Tag;
+

+
/// Provides the data for talking about commits.
+
pub mod commit;
+
pub use commit::{Author, Commit};
+

+
/// Provides the data for talking about namespaces.
+
pub mod namespace;
+
pub use namespace::Namespace;
+

+
/// Provides the data for talking about repository statistics.
+
pub mod stats;
+
pub use stats::Stats;
+

+
pub use crate::diff::Diff;
+

+
/// The signature of a commit
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+
pub struct Signature(Vec<u8>);
+

+
impl From<git2::Buf> for Signature {
+
    fn from(other: git2::Buf) -> Self {
+
        Signature((*other).into())
+
    }
+
}
+

+
/// Supports various ways to specify a revision used in Git.
+
pub trait Revision {
+
    /// Returns the object id of this revision in `repo`.
+
    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)
+
    }
+
}
+

+
impl Revision for &str {
+
    fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
+
        Oid::from_str(self).map_err(Error::Git)
+
    }
+
}
+

+
impl Revision for &Branch {
+
    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)?)
+
    }
+
}
+

+
impl Revision for &Tag {
+
    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>()
+
}
added radicle-surf/src/git/branch.rs
@@ -0,0 +1,306 @@
+
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 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)
+
    }
+
}
+

+
impl TryFrom<&str> for Branch {
+
    type Error = error::Branch;
+

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

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

+
    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(),
+
        };
+

+
        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()))
+
        }
+
    }
+
}
+

+
/// 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(),
+
                    }
+
                }
+
            },
+
        }
+
    }
+

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

+
impl TryFrom<&git2::Reference<'_>> for Local {
+
    type Error = error::Local;
+

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

+
impl TryFrom<&str> for Local {
+
    type Error = error::Local;
+

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

+
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,
+
}
+

+
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: name.as_ref().to_ref_string(),
+
            remote: remote.to_ref_string(),
+
        }
+
    }
+

+
    /// 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 TryFrom<&git2::Reference<'_>> for Remote {
+
    type Error = error::Remote;
+

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

+
impl TryFrom<&str> for Remote {
+
    type Error = error::Remote;
+

+
    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 {
+
            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),
+
    }
+
}
added radicle-surf/src/git/commit.rs
@@ -0,0 +1,230 @@
+
// 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::{
+
    file_system::{self, directory},
+
    git::{error::Error, Branch, RepositoryRef, Tag},
+
};
+
use git_ref_format::{Qualified, RefString};
+
use radicle_git_ext::Oid;
+
use std::{convert::TryFrom, str};
+

+
#[cfg(feature = "serialize")]
+
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
+

+
/// `Author` is the static information of a [`git2::Signature`].
+
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
+
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
+
pub struct Author {
+
    /// Name of the author.
+
    pub name: String,
+
    /// Email of the author.
+
    pub email: String,
+
    /// Time the action was taken, e.g. time of commit.
+
    #[cfg_attr(
+
        feature = "serialize",
+
        serde(
+
            serialize_with = "serialize_time",
+
            deserialize_with = "deserialize_time"
+
        )
+
    )]
+
    pub time: git2::Time,
+
}
+

+
#[cfg(feature = "serialize")]
+
fn deserialize_time<'de, D>(deserializer: D) -> Result<git2::Time, D::Error>
+
where
+
    D: Deserializer<'de>,
+
{
+
    let seconds: i64 = Deserialize::deserialize(deserializer)?;
+
    Ok(git2::Time::new(seconds, 0))
+
}
+

+
#[cfg(feature = "serialize")]
+
fn serialize_time<S>(t: &git2::Time, serializer: S) -> Result<S::Ok, S::Error>
+
where
+
    S: Serializer,
+
{
+
    serializer.serialize_i64(t.seconds())
+
}
+

+
impl std::fmt::Debug for Author {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        use std::cmp::Ordering;
+
        let time = match self.time.offset_minutes().cmp(&0) {
+
            Ordering::Equal => format!("{}", self.time.seconds()),
+
            Ordering::Greater => format!("{}+{}", self.time.seconds(), self.time.offset_minutes()),
+
            Ordering::Less => format!("{}{}", self.time.seconds(), self.time.offset_minutes()),
+
        };
+
        f.debug_struct("Author")
+
            .field("name", &self.name)
+
            .field("email", &self.email)
+
            .field("time", &time)
+
            .finish()
+
    }
+
}
+

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

+
    fn try_from(signature: git2::Signature) -> Result<Self, Self::Error> {
+
        let name = str::from_utf8(signature.name_bytes())?.into();
+
        let email = str::from_utf8(signature.email_bytes())?.into();
+
        let time = signature.when();
+

+
        Ok(Author { name, email, time })
+
    }
+
}
+

+
/// `Commit` is the static information of a [`git2::Commit`]. To get back the
+
/// original `Commit` in the repository we can use the [`Oid`] to retrieve
+
/// it.
+
#[cfg_attr(feature = "serialize", derive(Deserialize))]
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+
pub struct Commit {
+
    /// Object Id
+
    pub id: Oid,
+
    /// The author of the commit.
+
    pub author: Author,
+
    /// The actor who committed this commit.
+
    pub committer: Author,
+
    /// The long form message of the commit.
+
    pub message: String,
+
    /// The summary message of the commit.
+
    pub summary: String,
+
    /// The parents of this commit.
+
    pub parents: Vec<Oid>,
+
}
+

+
impl Commit {
+
    /// Returns the commit description text. This is the text after the one-line
+
    /// summary.
+
    #[must_use]
+
    pub fn description(&self) -> &str {
+
        self.message
+
            .strip_prefix(&self.summary)
+
            .unwrap_or(&self.message)
+
            .trim()
+
    }
+

+
    /// Retrieves the file with `path` in this commit.
+
    pub fn get_file<'a>(
+
        &'a self,
+
        repo: &'a RepositoryRef,
+
        path: file_system::Path,
+
    ) -> Result<directory::FileContent, Error> {
+
        let git2_commit = repo.get_git2_commit(self.id)?;
+
        repo.get_commit_file(&git2_commit, path)
+
    }
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for Commit {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Commit", 7)?;
+
        state.serialize_field("id", &self.id.to_string())?;
+
        state.serialize_field("author", &self.author)?;
+
        state.serialize_field("committer", &self.committer)?;
+
        state.serialize_field("summary", &self.summary)?;
+
        state.serialize_field("message", &self.message)?;
+
        state.serialize_field("description", &self.description())?;
+
        state.serialize_field(
+
            "parents",
+
            &self
+
                .parents
+
                .iter()
+
                .map(|oid| oid.to_string())
+
                .collect::<Vec<String>>(),
+
        )?;
+
        state.end()
+
    }
+
}
+

+
impl<'repo> TryFrom<git2::Commit<'repo>> for Commit {
+
    type Error = Error;
+

+
    fn try_from(commit: git2::Commit) -> Result<Self, Self::Error> {
+
        let id = commit.id().into();
+
        let author = Author::try_from(commit.author())?;
+
        let committer = Author::try_from(commit.committer())?;
+
        let message_raw = commit.message_bytes();
+
        let message = str::from_utf8(message_raw)?.into();
+
        let summary_raw = commit.summary_bytes().ok_or(Error::MissingSummary)?;
+
        let summary = str::from_utf8(summary_raw)?.into();
+
        let parents = commit.parent_ids().map(|oid| oid.into()).collect();
+

+
        Ok(Commit {
+
            id,
+
            author,
+
            committer,
+
            message,
+
            summary,
+
            parents,
+
        })
+
    }
+
}
+

+
/// A common trait for anything that can convert to a `Commit`.
+
pub trait ToCommit {
+
    /// Converts to a commit in `repo`.
+
    fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error>;
+
}
+

+
impl ToCommit for Commit {
+
    fn to_commit(self, _repo: &RepositoryRef) -> Result<Commit, Error> {
+
        Ok(self)
+
    }
+
}
+

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

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

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

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

+
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)
+
    }
+
}
added radicle-surf/src/git/error.rs
@@ -0,0 +1,81 @@
+
// 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/>.
+

+
//! Collection of errors and helper instances that can occur when performing
+
//! operations from [`crate::git`].
+

+
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, Error)]
+
#[non_exhaustive]
+
pub enum Error {
+
    #[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")]
+
    RevParseFailure {
+
        /// The provided revspec that failed to parse.
+
        rev: String,
+
    },
+
    /// A `revspec` was provided that could not be found in the given
+
    /// `namespace`.
+
    #[error("provided revspec '{rev}' could not be parsed into a git object in the namespace '{namespace}'")]
+
    NamespaceRevParseFailure {
+
        /// The namespace we are in when attempting to fetch the `rev`.
+
        namespace: Namespace,
+
        /// The provided revspec that failed to parse.
+
        rev: String,
+
    },
+
    /// When parsing a namespace we may come across one that was an empty
+
    /// string.
+
    #[error("tried parsing the namespace but it was empty")]
+
    EmptyNamespace,
+
    /// A [`str::Utf8Error`] error, which usually occurs when a git object's
+
    /// name is not in UTF-8 form and parsing of it as such fails.
+
    #[error(transparent)]
+
    Utf8Error(#[from] str::Utf8Error),
+
    /// When trying to get the summary for a [`git2::Commit`] some action
+
    /// failed.
+
    #[error("an error occurred trying to get a commit's summary")]
+
    MissingSummary,
+
    /// An error that comes from performing a [`crate::file_system`] operation.
+
    #[error(transparent)]
+
    FileSystem(#[from] file_system::Error),
+
    /// While attempting to calculate a diff for retrieving the
+
    /// [`crate::vcs::git::Browser.last_commit()`], the file path was returned
+
    /// as an `Option::None`.
+
    #[error("last commit has an invalid file path")]
+
    LastCommitException,
+
    /// The requested file was not found.
+
    #[error("path not found for: {0}")]
+
    PathNotFound(file_system::Path),
+
    /// An error that comes from performing a *diff* operations.
+
    #[error(transparent)]
+
    Diff(#[from] diff::git::error::Diff),
+
    /// 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/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::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)
+
    }
+
}
added radicle-surf/src/git/history.rs
@@ -0,0 +1,103 @@
+
// This file is part of radicle-surf
+
// <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::{
+
    file_system,
+
    git::{commit::ToCommit, Commit, Error, RepositoryRef},
+
};
+
use std::convert::TryFrom;
+

+
/// An iterator that produces the history of commits for a given `head`,
+
/// in the `repo`.
+
pub struct History<'a> {
+
    repo: RepositoryRef<'a>,
+
    head: Commit,
+
    revwalk: git2::Revwalk<'a>,
+
    filter_by: Option<FilterBy>,
+
}
+

+
/// Internal implementation, subject to refactoring.
+
enum FilterBy {
+
    File { path: file_system::Path },
+
}
+

+
impl<'a> History<'a> {
+
    /// Creates a new history starting from `head`, in `repo`.
+
    pub fn new<C: ToCommit>(repo: RepositoryRef<'a>, head: C) -> Result<Self, Error> {
+
        let head = head.to_commit(&repo)?;
+
        let mut revwalk = repo.repo_ref.revwalk()?;
+
        revwalk.push(head.id.into())?;
+
        let history = Self {
+
            repo,
+
            head,
+
            revwalk,
+
            filter_by: None,
+
        };
+
        Ok(history)
+
    }
+

+
    /// Returns the first commit (i.e. the head) in the history.
+
    pub fn head(&self) -> &Commit {
+
        &self.head
+
    }
+

+
    /// Returns a modified `History` filtered by `path`.
+
    ///
+
    /// Note that it is possible that a filtered History becomes empty,
+
    /// even though calling `.head()` still returns the original head.
+
    pub fn by_path(mut self, path: file_system::Path) -> Self {
+
        self.filter_by = Some(FilterBy::File { path });
+
        self
+
    }
+
}
+

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

+
    fn next(&mut self) -> Option<Self::Item> {
+
        // Loop through the commits with the optional filtering.
+
        while let Some(oid) = self.revwalk.next() {
+
            let found = oid
+
                .map_err(Error::Git)
+
                .and_then(|oid| {
+
                    let git2_commit = self.repo.repo_ref.find_commit(oid)?;
+

+
                    // Handles the optional filter_by.
+
                    if let Some(FilterBy::File { path }) = &self.filter_by {
+
                        let path_opt = self.repo.diff_commit_and_parents(path, &git2_commit)?;
+
                        if path_opt.is_none() {
+
                            return Ok(None); // Filter out this commit.
+
                        }
+
                    }
+

+
                    let commit = Commit::try_from(git2_commit)?;
+
                    Ok(Some(commit))
+
                })
+
                .transpose();
+
            if found.is_some() {
+
                return found;
+
            }
+
        }
+
        None
+
    }
+
}
+

+
impl<'a> std::fmt::Debug for History<'a> {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        write!(f, "History of {}", self.head.id)
+
    }
+
}
added radicle-surf/src/git/namespace.rs
@@ -0,0 +1,94 @@
+
// 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::error::Error;
+
use nonempty::NonEmpty;
+
pub use radicle_git_ext::Oid;
+
use std::{convert::TryFrom, fmt, str};
+

+
/// A `Namespace` value allows us to switch the git namespace of
+
/// a repo.
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Namespace {
+
    /// Since namespaces can be nested we have a vector of strings.
+
    /// This means that the namespaces `"foo/bar"` is represented as
+
    /// `vec!["foo", "bar"]`.
+
    pub(super) values: NonEmpty<String>,
+
}
+

+
impl Namespace {
+
    /// Appends a `refname` to this namespace, and returns
+
    /// the full reference path.
+
    pub fn append_refname(&self, refname: &str) -> String {
+
        let mut prefix = String::new();
+
        for value in self.values.iter() {
+
            prefix = format!("{}refs/namespaces/{}/", &prefix, value);
+
        }
+
        format!("{}{}", &prefix, refname)
+
    }
+
}
+

+
impl fmt::Display for Namespace {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        let values: Vec<_> = self.values.clone().into();
+
        write!(f, "{}", values.join("/"))
+
    }
+
}
+

+
impl TryFrom<&str> for Namespace {
+
    type Error = Error;
+

+
    fn try_from(namespace: &str) -> Result<Self, Self::Error> {
+
        let values = namespace.split('/').map(|n| n.to_string()).collect();
+
        NonEmpty::from_vec(values)
+
            .map(|values| Self { values })
+
            .ok_or(Error::EmptyNamespace)
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Namespace {
+
    type Error = Error;
+

+
    fn try_from(namespace: &[u8]) -> Result<Self, Self::Error> {
+
        str::from_utf8(namespace)
+
            .map_err(Error::from)
+
            .and_then(Namespace::try_from)
+
    }
+
}
+

+
impl TryFrom<git2::Reference<'_>> for Namespace {
+
    type Error = Error;
+

+
    fn try_from(reference: git2::Reference) -> Result<Self, Self::Error> {
+
        let re = regex::Regex::new(r"refs/namespaces/([^/]+)/").unwrap();
+
        let ref_name = str::from_utf8(reference.name_bytes())?;
+
        let values = re
+
            .find_iter(ref_name)
+
            .map(|m| {
+
                String::from(
+
                    m.as_str()
+
                        .trim_start_matches("refs/namespaces/")
+
                        .trim_end_matches('/'),
+
                )
+
            })
+
            .collect::<Vec<_>>();
+

+
        NonEmpty::from_vec(values)
+
            .map(|values| Self { values })
+
            .ok_or(Error::EmptyNamespace)
+
    }
+
}
added radicle-surf/src/git/repo.rs
@@ -0,0 +1,482 @@
+
// 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::{
+
    diff::*,
+
    file_system,
+
    file_system::{
+
        directory::{self, Directory, DirectoryEntry, FileContent},
+
        Label,
+
    },
+
    git::{error::*, Branch, Commit, Glob, History, Namespace, Revision, Signature, Stats, Tag},
+
};
+
use git_ref_format::RefString;
+
use radicle_git_ext::Oid;
+
use std::{
+
    collections::{BTreeMap, BTreeSet},
+
    convert::TryFrom,
+
    path::PathBuf,
+
    str,
+
};
+

+
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.
+
pub struct Repository(pub(super) git2::Repository);
+

+
/// A reference-only `Repository`. This means that we cannot mutate the
+
/// underlying `Repository`. Not being able to mutate the `Repository` means
+
/// that the functions defined for `RepositoryRef` should be thread-safe.
+
///
+
/// # Construction
+
///
+
/// Use the `From<&'a git2::Repository>` implementation to construct a
+
/// `RepositoryRef`.
+
#[derive(Clone, Copy)]
+
pub struct RepositoryRef<'a> {
+
    pub(crate) repo_ref: &'a git2::Repository,
+
}
+

+
// RepositoryRef should be safe to transfer across thread boundaries since it
+
// only holds a reference to git2::Repository. git2::Repository is also Send
+
// (see: https://docs.rs/git2/0.13.5/src/git2/repo.rs.html#46)
+
unsafe impl<'a> Send for RepositoryRef<'a> {}
+

+
impl<'a> From<&'a git2::Repository> for RepositoryRef<'a> {
+
    fn from(repo_ref: &'a git2::Repository) -> Self {
+
        RepositoryRef { repo_ref }
+
    }
+
}
+

+
impl<'a> RepositoryRef<'a> {
+
    /// What is the current namespace we're browsing in.
+
    pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
+
        self.repo_ref
+
            .namespace_bytes()
+
            .map(Namespace::try_from)
+
            .transpose()
+
    }
+

+
    /// Returns an iterator of branches that match `pattern`.
+
    pub fn branches(&self, pattern: &Glob<Branch>) -> Result<Branches, Error> {
+
        let mut branches = Branches::default();
+
        for glob in pattern.globs().iter() {
+
            let namespaced = self.namespaced_refname(glob)?;
+
            let references = self.repo_ref.references_glob(&namespaced)?;
+
            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::default();
+
        for glob in pattern.globs().iter() {
+
            let namespaced = self.namespaced_refname(glob)?;
+
            let references = self.repo_ref.references_glob(&namespaced)?;
+
            tags.push(references);
+
        }
+
        Ok(tags)
+
    }
+

+
    /// 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::new(set))
+
    }
+

+
    /// Get the [`Diff`] between two commits.
+
    pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
+
        let from_commit = self.get_git2_commit(from.object_id(self)?)?;
+
        let to_commit = self.get_git2_commit(to.object_id(self)?)?;
+
        self.diff_commits(None, Some(&from_commit), &to_commit)
+
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
+
    }
+

+
    /// Get the [`Diff`] of a commit with no parents.
+
    pub fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
+
        let commit = self.get_git2_commit(rev.object_id(self)?)?;
+
        self.diff_commits(None, None, &commit)
+
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
+
    }
+

+
    /// Get the diff introduced by a particlar rev.
+
    pub fn diff_from_parent<C: ToCommit>(&self, commit: C) -> Result<Diff, Error> {
+
        let commit = commit.to_commit(self)?;
+
        match commit.parents.first() {
+
            Some(parent) => self.diff(*parent, commit.id),
+
            None => self.initial_diff(commit.id),
+
        }
+
    }
+

+
    /// Parse an [`Oid`] from the given string.
+
    pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
+
        Ok(self.repo_ref.revparse_single(oid)?.id().into())
+
    }
+

+
    /// Returns a top level `Directory` without nested sub-directories.
+
    ///
+
    /// To visit inside any nested sub-directories, call `directory.get(&repo)`
+
    /// on the sub-directory.
+
    pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
+
        let commit = commit.to_commit(self)?;
+
        let git2_commit = self.repo_ref.find_commit((commit.id).into())?;
+
        let tree = git2_commit.as_object().peel_to_tree()?;
+
        Ok(Directory {
+
            name: Label::root(),
+
            oid: tree.id().into(),
+
        })
+
    }
+

+
    /// Retrieves the content of a directory.
+
    pub(crate) fn directory_get(
+
        &self,
+
        d: &Directory,
+
    ) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
+
        let git2_tree = self.repo_ref.find_tree(d.oid.into())?;
+
        let map = self.tree_first_level(git2_tree)?;
+
        Ok(map)
+
    }
+

+
    /// Returns a map of the first level entries in `tree`.
+
    fn tree_first_level(&self, tree: git2::Tree) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
+
        let mut map = BTreeMap::new();
+

+
        // Walks only the first level of entries.
+
        tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| {
+
            let oid = entry.id().into();
+
            let label = match entry.name() {
+
                Some(name) => match name.parse::<Label>() {
+
                    Ok(label) => label,
+
                    Err(_) => return git2::TreeWalkResult::Abort,
+
                },
+
                None => return git2::TreeWalkResult::Abort,
+
            };
+

+
            match entry.kind() {
+
                Some(git2::ObjectType::Tree) => {
+
                    let dir = Directory::new(label.clone(), oid);
+
                    map.insert(label, DirectoryEntry::Directory(dir));
+
                    return git2::TreeWalkResult::Skip; // Not go into nested
+
                                                       // directories.
+
                },
+
                Some(git2::ObjectType::Blob) => {
+
                    let f = directory::File {
+
                        name: label.clone(),
+
                        oid,
+
                    };
+
                    map.insert(label, DirectoryEntry::File(f));
+
                },
+
                _ => {
+
                    return git2::TreeWalkResult::Skip;
+
                },
+
            }
+

+
            git2::TreeWalkResult::Ok
+
        })?;
+

+
        Ok(map)
+
    }
+

+
    /// Returns the last commit, if exists, for a `path` in the history of
+
    /// `rev`.
+
    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()
+
    }
+

+
    /// Returns a commit for `rev` if exists.
+
    pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
+
        let oid = rev.object_id(self)?;
+
        match self.repo_ref.find_commit(oid.into()) {
+
            Ok(commit) => Commit::try_from(commit),
+
            Err(e) => Err(Error::Git(e)),
+
        }
+
    }
+

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

+
        let contributors = history
+
            .filter_map(|commit| match commit {
+
                Ok(commit) => {
+
                    commits += 1;
+
                    Some((commit.author.name, commit.author.email))
+
                },
+
                Err(_) => None,
+
            })
+
            .collect::<BTreeSet<_>>();
+

+
        Ok(Stats {
+
            branches,
+
            commits,
+
            contributors: contributors.len(),
+
        })
+
    }
+

+
    /// Obtain the file content
+
    pub(crate) fn file_content(&self, object_id: Oid) -> Result<FileContent, Error> {
+
        let blob = self.repo_ref.find_blob(object_id.into())?;
+
        Ok(FileContent::new(blob))
+
    }
+

+
    /// Return the size of a file
+
    pub(crate) fn file_size(&self, oid: Oid) -> Result<usize, Error> {
+
        let blob = self.repo_ref.find_blob(oid.into())?;
+
        Ok(blob.size())
+
    }
+

+
    /// Lists branch names with `filter`.
+
    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<RefString>, Error> {
+
        let mut tags = self
+
            .tags(&Glob::tags("*")?)?
+
            .map(|t| t.map_err(Error::from).map(|t| t.refname().into()))
+
            .collect::<Result<Vec<_>, Error>>()?;
+
        tags.sort();
+

+
        Ok(tags)
+
    }
+

+
    /// Returns the Oid of the current HEAD
+
    pub fn head_oid(&self) -> Result<Oid, Error> {
+
        let head = self.repo_ref.head()?;
+
        let head_commit = head.peel_to_commit()?;
+
        Ok(head_commit.id().into())
+
    }
+

+
    /// Switch to a `namespace`
+
    pub fn switch_namespace(&self, namespace: &str) -> Result<(), Error> {
+
        Ok(self.repo_ref.set_namespace(namespace)?)
+
    }
+

+
    /// Returns a full reference name with namespace(s) included.
+
    pub(crate) fn namespaced_refname(&self, refname: &str) -> Result<String, Error> {
+
        let fullname = match self.which_namespace()? {
+
            Some(namespace) => namespace.append_refname(refname),
+
            None => refname.to_string(),
+
        };
+
        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)
+
    }
+

+
    /// Extract the signature from a commit
+
    ///
+
    /// # Arguments
+
    ///
+
    /// `commit_oid` - The object ID of the commit
+
    /// `field` - the name of the header field containing the signature block;
+
    ///           pass `None` to extract the default 'gpgsig'
+
    pub fn extract_signature(
+
        &self,
+
        commit_oid: &Oid,
+
        field: Option<&str>,
+
    ) -> Result<Option<Signature>, Error> {
+
        // Match is necessary here because according to the documentation for
+
        // git_commit_extract_signature at
+
        // https://libgit2.org/libgit2/#HEAD/group/commit/git_commit_extract_signature
+
        // the return value for a commit without a signature will be GIT_ENOTFOUND
+
        match self.repo_ref.extract_signature(commit_oid, field) {
+
            Err(error) => {
+
                if error.code() == git2::ErrorCode::NotFound {
+
                    Ok(None)
+
                } else {
+
                    Err(error.into())
+
                }
+
            },
+
            Ok(sig) => Ok(Some(Signature::from(sig.0))),
+
        }
+
    }
+

+
    /// Lists branches that are reachable from `oid`.
+
    pub fn revision_branches(&self, oid: &Oid, glob: &Glob<Branch>) -> Result<Vec<Branch>, Error> {
+
        let mut contained_branches = vec![];
+
        for branch in self.branches(glob)? {
+
            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)
+
    }
+

+
    fn reachable_from(&self, reference: &git2::Reference, oid: &Oid) -> Result<bool, Error> {
+
        let git2_oid = (*oid).into();
+
        let other = reference.peel_to_commit()?.id();
+
        let is_descendant = self.repo_ref.graph_descendant_of(other, git2_oid)?;
+

+
        Ok(other == git2_oid || is_descendant)
+
    }
+

+
    pub(crate) fn get_commit_file(
+
        &self,
+
        git2_commit: &git2::Commit,
+
        path: file_system::Path,
+
    ) -> Result<FileContent, Error> {
+
        let git2_tree = git2_commit.tree()?;
+
        let entry = git2_tree.get_path(PathBuf::from(&path).as_ref())?;
+
        let object = entry.to_object(self.repo_ref)?;
+
        let blob = object.into_blob().map_err(|_| Error::PathNotFound(path))?;
+
        Ok(FileContent::new(blob))
+
    }
+

+
    pub(crate) fn diff_commit_and_parents(
+
        &self,
+
        path: &file_system::Path,
+
        commit: &git2::Commit,
+
    ) -> Result<Option<file_system::Path>, Error> {
+
        let mut parents = commit.parents();
+

+
        let diff = self.diff_commits(Some(path), parents.next().as_ref(), commit)?;
+
        if let Some(_delta) = diff.deltas().next() {
+
            Ok(Some(path.clone()))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    fn diff_commits(
+
        &self,
+
        path: Option<&file_system::Path>,
+
        from: Option<&git2::Commit>,
+
        to: &git2::Commit,
+
    ) -> Result<git2::Diff, Error> {
+
        let new_tree = to.tree()?;
+
        let old_tree = from.map_or(Ok(None), |c| c.tree().map(Some))?;
+

+
        let mut opts = git2::DiffOptions::new();
+
        if let Some(path) = path {
+
            opts.pathspec(path);
+
            // We're skipping the binary pass because we won't be inspecting deltas.
+
            opts.skip_binary_check(true);
+
        }
+

+
        let mut diff =
+
            self.repo_ref
+
                .diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(&mut opts))?;
+

+
        // Detect renames by default.
+
        let mut find_opts = git2::DiffFindOptions::new();
+
        find_opts.renames(true);
+
        diff.find_similar(Some(&mut find_opts))?;
+

+
        Ok(diff)
+
    }
+

+
    /// Returns the history with the `head` commit.
+
    pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
+
        History::new(*self, head)
+
    }
+
}
+

+
impl<'a> std::fmt::Debug for RepositoryRef<'a> {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        write!(f, ".git")
+
    }
+
}
+

+
impl Repository {
+
    /// Open a git repository given its exact URI.
+
    ///
+
    /// # Errors
+
    ///
+
    /// * [`Error::Git`]
+
    pub fn open(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
+
        git2::Repository::open(repo_uri)
+
            .map(Repository)
+
            .map_err(Error::from)
+
    }
+

+
    /// Attempt to open a git repository at or above `repo_uri` in the file
+
    /// system.
+
    pub fn discover(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
+
        git2::Repository::discover(repo_uri)
+
            .map(Repository)
+
            .map_err(Error::from)
+
    }
+

+
    /// Since our operations are read-only when it comes to surfing a repository
+
    /// we have a separate struct called [`RepositoryRef`]. This turns an owned
+
    /// [`Repository`] into a [`RepositoryRef`].
+
    pub fn as_ref(&'_ self) -> RepositoryRef<'_> {
+
        RepositoryRef { repo_ref: &self.0 }
+
    }
+
}
+

+
impl<'a> From<&'a Repository> for RepositoryRef<'a> {
+
    fn from(repo: &'a Repository) -> Self {
+
        repo.as_ref()
+
    }
+
}
+

+
impl From<git2::Repository> for Repository {
+
    fn from(repo: git2::Repository) -> Self {
+
        Repository(repo)
+
    }
+
}
+

+
impl std::fmt::Debug for Repository {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        write!(f, ".git")
+
    }
+
}
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),
+
    }
+
}
added radicle-surf/src/git/stats.rs
@@ -0,0 +1,36 @@
+
// 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/>.
+

+
pub use radicle_git_ext::Oid;
+

+
#[cfg(feature = "serialize")]
+
use serde::Serialize;
+

+
/// Stats for a repository
+
#[cfg_attr(
+
    feature = "serialize",
+
    derive(Serialize),
+
    serde(rename_all = "camelCase")
+
)]
+
pub struct Stats {
+
    /// Number of commits
+
    pub commits: usize,
+
    /// Number of local branches
+
    pub branches: usize,
+
    /// Number of contributors
+
    pub contributors: usize,
+
}
added radicle-surf/src/git/tag.rs
@@ -0,0 +1,149 @@
+
use std::{convert::TryFrom, str};
+

+
use git_ext::Oid;
+
use git_ref_format::{component, lit, Qualified, RefStr, RefString};
+

+
use crate::git::{refstr_join, Author};
+

+
/// The static information of a [`git2::Tag`].
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+
pub enum Tag {
+
    /// A light-weight git tag.
+
    Light {
+
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
+
        id: Oid,
+
        /// 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: 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>,
+
    },
+
}
+

+
impl Tag {
+
    /// Get the `Oid` of the tag, regardless of its type.
+
    pub fn id(&self) -> Oid {
+
        match self {
+
            Self::Light { id, .. } => *id,
+
            Self::Annotated { id, .. } => *id,
+
        }
+
    }
+

+
    /// Return the fully qualified `Tag` refname,
+
    /// e.g. `refs/tags/release/v1`.
+
    pub fn refname(&self) -> Qualified {
+
        lit::refs_tags(self.name()).into()
+
    }
+

+
    fn name(&self) -> &RefString {
+
        match &self {
+
            Tag::Light { name, .. } => name,
+
            Tag::Annotated { name, .. } => name,
+
        }
+
    }
+
}
+

+
pub mod error {
+
    use std::str;
+

+
    use git_ref_format::RefString;
+
    use thiserror::Error;
+

+
    #[derive(Debug, Error)]
+
    pub enum FromTag {
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] str::Utf8Error),
+
    }
+

+
    #[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),
+
    }
+
}
+

+
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)
+
            .transpose()?
+
            .map(|message| message.into());
+

+
        Ok(Tag::Annotated {
+
            id,
+
            target,
+
            name,
+
            tagger,
+
            message,
+
        })
+
    }
+
}
+

+
impl TryFrom<&git2::Reference<'_>> for Tag {
+
    type Error = error::FromReference;
+

+
    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()))?
+
        };
+

+
        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.
+
                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: refstr_join(c, cs),
+
                    })
+
                },
+
                Err(err) => Err(err.into()),
+
            }
+
        } else {
+
            Err(error::FromReference::NotTag(name.into()))
+
        }
+
    }
+
}
modified radicle-surf/src/lib.rs
@@ -15,8 +15,6 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

-
#![deny(missing_docs, unused_import_braces, unused_qualifications, warnings)]
-

//! Welcome to `radicle-surf`!
//!
//! `radicle-surf` is a system to describe a file-system in a VCS world.
@@ -82,9 +80,14 @@
//! # Ok(())
//! # }
//! ```
+

+
pub extern crate git_ref_format;
+

+
extern crate radicle_git_ext as git_ext;
+

pub mod diff;
pub mod file_system;
-
pub mod vcs;
+
pub mod git;

pub mod commit;
pub use commit::{commit, commits, Commit};
@@ -105,5 +108,3 @@ pub use syntax::SYNTAX_SET;

// Private modules
mod nonempty;
-

-
pub use crate::vcs::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,10 +25,7 @@ use serde::{Deserialize, Serialize};

use radicle_git_ext::Oid;

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

/// Types of a peer.
pub enum Category<P, U> {
@@ -54,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"))]
@@ -77,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> {
@@ -117,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
deleted radicle-surf/src/vcs.rs
@@ -1,20 +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/>.
-

-
//! A model of a general VCS.
-

-
pub mod git;
deleted radicle-surf/src/vcs/git.rs
@@ -1,184 +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 nonempty::NonEmpty;
-
//! use radicle_surf::file_system::{Directory, File, Label, Path, SystemType};
-
//! use radicle_surf::file_system::unsound;
-
//! use radicle_surf::vcs::git::*;
-
//! use std::collections::HashMap;
-
//! use std::str::FromStr;
-
//! # use std::error::Error;
-
//!
-
//! # fn main() -> Result<(), Box<dyn Error>> {
-
//! let repo = Repository::new("./data/git-platinum")?;
-
//!
-
//! // Pin the browser to a parituclar commit.
-
//! let pin_commit = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
//! let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
//! browser.commit(pin_commit)?;
-
//!
-
//! let directory = browser.get_directory()?;
-
//! let mut directory_contents = directory.list_directory();
-
//! directory_contents.sort();
-
//!
-
//! assert_eq!(directory_contents, vec![
-
//!     SystemType::file(unsound::label::new(".i-am-well-hidden")),
-
//!     SystemType::file(unsound::label::new(".i-too-am-hidden")),
-
//!     SystemType::file(unsound::label::new("README.md")),
-
//!     SystemType::directory(unsound::label::new("bin")),
-
//!     SystemType::directory(unsound::label::new("src")),
-
//!     SystemType::directory(unsound::label::new("text")),
-
//!     SystemType::directory(unsound::label::new("this")),
-
//! ]);
-
//!
-
//! // find src directory in the Git directory and the in-memory directory
-
//! let src_directory = directory
-
//!     .find_directory(Path::new(unsound::label::new("src")))
-
//!     .expect("failed to find src");
-
//! let mut src_directory_contents = src_directory.list_directory();
-
//! src_directory_contents.sort();
-
//!
-
//! assert_eq!(src_directory_contents, vec![
-
//!     SystemType::file(unsound::label::new("Eval.hs")),
-
//!     SystemType::file(unsound::label::new("Folder.svelte")),
-
//!     SystemType::file(unsound::label::new("memory.rs")),
-
//! ]);
-
//! #
-
//! # Ok(())
-
//! # }
-
//! ```
-

-
use std::str::FromStr;
-

-
// Re-export git2 as sub-module
-
pub use git2::{self, Error as Git2Error, Time};
-
pub use radicle_git_ext::Oid;
-

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

-
mod glob;
-
pub use glob::Glob;
-

-
mod history;
-
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};
-

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

-
/// Provides the data for talking about commits.
-
pub mod commit;
-
pub use commit::{Author, Commit};
-

-
/// Provides the data for talking about namespaces.
-
pub mod namespace;
-
pub use namespace::Namespace;
-

-
/// Provides the data for talking about repository statistics.
-
pub mod stats;
-
pub use stats::Stats;
-

-
pub use crate::diff::Diff;
-

-
/// The signature of a commit
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct Signature(Vec<u8>);
-

-
impl From<git2::Buf> for Signature {
-
    fn from(other: git2::Buf) -> Self {
-
        Signature((*other).into())
-
    }
-
}
-

-
/// Determines whether to look for local or remote references or both.
-
pub enum RefScope {
-
    /// List all branches by default.
-
    All,
-
    /// List only local branches.
-
    Local,
-
    /// List only remote branches.
-
    Remote {
-
        /// Name of the remote. If `None`, then get the reference from all
-
        /// remotes.
-
        name: Option<String>,
-
    },
-
}
-

-
/// Turn an `Option<P>` into a [`RefScope`]. If the `P` is present then
-
/// this is set as the remote of the `RefScope`. Otherwise, it's local
-
/// branch.
-
impl<P> From<Option<P>> for RefScope
-
where
-
    P: ToString,
-
{
-
    fn from(peer_id: Option<P>) -> Self {
-
        peer_id.map_or(RefScope::Local, |peer_id| RefScope::Remote {
-
            // We qualify the remotes as the PeerId + heads, otherwise we would grab the tags too.
-
            name: Some(format!("{}/heads", peer_id.to_string())),
-
        })
-
    }
-
}
-

-
/// Supports various ways to specify a revision used in Git.
-
pub trait Revision {
-
    /// Returns the object id of this revision in `repo`.
-
    fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
-
}
-

-
impl Revision for Oid {
-
    fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
-
        Ok(*self)
-
    }
-
}
-

-
impl Revision for &str {
-
    fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
-
        Oid::from_str(self).map_err(Error::Git)
-
    }
-
}
-

-
impl Revision for &Branch {
-
    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)?)
-
    }
-
}
-

-
impl Revision for &Tag {
-
    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)?)
-
    }
-
}
-

-
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)?)
-
    }
-
}
deleted radicle-surf/src/vcs/git/branch.rs
@@ -1,186 +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::vcs::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,
-
        }
-
    }
-
}
-

-
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 },
-
        }
-
    }
-
}
-

-
/// 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 fmt::Display for BranchName {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        self.0.fmt(f)
-
    }
-
}
-

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

-
    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,
-
        };
-
        Ok(Self(short_name))
-
    }
-
}
-

-
impl BranchName {
-
    /// Create a new `BranchName`.
-
    pub fn new(name: &str) -> Self {
-
        Self(name.into())
-
    }
-

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

-
/// 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 PartialOrd for Branch {
-
    fn partial_cmp(&self, other: &Branch) -> Option<Ordering> {
-
        Some(self.cmp(other))
-
    }
-
}
-

-
impl Ord for Branch {
-
    fn cmp(&self, other: &Branch) -> Ordering {
-
        self.name.cmp(&other.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()),
-
            },
-
        }
-
    }
-

-
    /// Helper to create a remote `Branch` with a name
-
    pub fn local(name: &str) -> Self {
-
        Self {
-
            name: BranchName(name.to_string()),
-
            locality: BranchType::Local,
-
        }
-
    }
-

-
    /// 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),
-
            },
-
        }
-
    }
-
}
-

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

-
    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())?;
-

-
        // 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));
-
        }
-

-
        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()),
-
                },
-
            })
-
        } else {
-
            Ok(Self {
-
                name,
-
                locality: BranchType::Local,
-
            })
-
        }
-
    }
-
}
deleted radicle-surf/src/vcs/git/commit.rs
@@ -1,223 +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::{
-
    file_system::{self, directory},
-
    vcs::git::{error::Error, Branch, RepositoryRef, Tag, TagName},
-
};
-
use radicle_git_ext::Oid;
-
use std::{convert::TryFrom, str};
-

-
#[cfg(feature = "serialize")]
-
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
-

-
/// `Author` is the static information of a [`git2::Signature`].
-
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
-
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct Author {
-
    /// Name of the author.
-
    pub name: String,
-
    /// Email of the author.
-
    pub email: String,
-
    /// Time the action was taken, e.g. time of commit.
-
    #[cfg_attr(
-
        feature = "serialize",
-
        serde(
-
            serialize_with = "serialize_time",
-
            deserialize_with = "deserialize_time"
-
        )
-
    )]
-
    pub time: git2::Time,
-
}
-

-
#[cfg(feature = "serialize")]
-
fn deserialize_time<'de, D>(deserializer: D) -> Result<git2::Time, D::Error>
-
where
-
    D: Deserializer<'de>,
-
{
-
    let seconds: i64 = Deserialize::deserialize(deserializer)?;
-
    Ok(git2::Time::new(seconds, 0))
-
}
-

-
#[cfg(feature = "serialize")]
-
fn serialize_time<S>(t: &git2::Time, serializer: S) -> Result<S::Ok, S::Error>
-
where
-
    S: Serializer,
-
{
-
    serializer.serialize_i64(t.seconds())
-
}
-

-
impl std::fmt::Debug for Author {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        use std::cmp::Ordering;
-
        let time = match self.time.offset_minutes().cmp(&0) {
-
            Ordering::Equal => format!("{}", self.time.seconds()),
-
            Ordering::Greater => format!("{}+{}", self.time.seconds(), self.time.offset_minutes()),
-
            Ordering::Less => format!("{}{}", self.time.seconds(), self.time.offset_minutes()),
-
        };
-
        f.debug_struct("Author")
-
            .field("name", &self.name)
-
            .field("email", &self.email)
-
            .field("time", &time)
-
            .finish()
-
    }
-
}
-

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

-
    fn try_from(signature: git2::Signature) -> Result<Self, Self::Error> {
-
        let name = str::from_utf8(signature.name_bytes())?.into();
-
        let email = str::from_utf8(signature.email_bytes())?.into();
-
        let time = signature.when();
-

-
        Ok(Author { name, email, time })
-
    }
-
}
-

-
/// `Commit` is the static information of a [`git2::Commit`]. To get back the
-
/// original `Commit` in the repository we can use the [`Oid`] to retrieve
-
/// it.
-
#[cfg_attr(feature = "serialize", derive(Deserialize))]
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct Commit {
-
    /// Object Id
-
    pub id: Oid,
-
    /// The author of the commit.
-
    pub author: Author,
-
    /// The actor who committed this commit.
-
    pub committer: Author,
-
    /// The long form message of the commit.
-
    pub message: String,
-
    /// The summary message of the commit.
-
    pub summary: String,
-
    /// The parents of this commit.
-
    pub parents: Vec<Oid>,
-
}
-

-
impl Commit {
-
    /// Returns the commit description text. This is the text after the one-line
-
    /// summary.
-
    #[must_use]
-
    pub fn description(&self) -> &str {
-
        self.message
-
            .strip_prefix(&self.summary)
-
            .unwrap_or(&self.message)
-
            .trim()
-
    }
-

-
    /// Retrieves the file with `path` in this commit.
-
    pub fn get_file<'a>(
-
        &'a self,
-
        repo: &'a RepositoryRef,
-
        path: file_system::Path,
-
    ) -> Result<directory::FileContent, Error> {
-
        let git2_commit = repo.get_git2_commit(self.id)?;
-
        repo.get_commit_file(&git2_commit, path)
-
    }
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for Commit {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Commit", 7)?;
-
        state.serialize_field("id", &self.id.to_string())?;
-
        state.serialize_field("author", &self.author)?;
-
        state.serialize_field("committer", &self.committer)?;
-
        state.serialize_field("summary", &self.summary)?;
-
        state.serialize_field("message", &self.message)?;
-
        state.serialize_field("description", &self.description())?;
-
        state.serialize_field(
-
            "parents",
-
            &self
-
                .parents
-
                .iter()
-
                .map(|oid| oid.to_string())
-
                .collect::<Vec<String>>(),
-
        )?;
-
        state.end()
-
    }
-
}
-

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

-
    fn try_from(commit: git2::Commit) -> Result<Self, Self::Error> {
-
        let id = commit.id().into();
-
        let author = Author::try_from(commit.author())?;
-
        let committer = Author::try_from(commit.committer())?;
-
        let message_raw = commit.message_bytes();
-
        let message = str::from_utf8(message_raw)?.into();
-
        let summary_raw = commit.summary_bytes().ok_or(Error::MissingSummary)?;
-
        let summary = str::from_utf8(summary_raw)?.into();
-
        let parents = commit.parent_ids().map(|oid| oid.into()).collect();
-

-
        Ok(Commit {
-
            id,
-
            author,
-
            committer,
-
            message,
-
            summary,
-
            parents,
-
        })
-
    }
-
}
-

-
/// A common trait for anything that can convert to a `Commit`.
-
pub trait ToCommit {
-
    /// Converts to a commit in `repo`.
-
    fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error>;
-
}
-

-
impl ToCommit for Commit {
-
    fn to_commit(self, _repo: &RepositoryRef) -> Result<Commit, Error> {
-
        Ok(self)
-
    }
-
}
-

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

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

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

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

-
impl ToCommit for &TagName {
-
    fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
-
        repo.commit(self)
-
    }
-
}
deleted radicle-surf/src/vcs/git/error.rs
@@ -1,94 +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/>.
-

-
//! Collection of errors and helper instances that can occur when performing
-
//! operations from [`crate::vcs::git`].
-

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

-
/// Enumeration of errors that can occur in operations from [`crate::vcs::git`].
-
#[derive(Debug, PartialEq, 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),
-
    /// 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")]
-
    RevParseFailure {
-
        /// The provided revspec that failed to parse.
-
        rev: String,
-
    },
-
    /// A `revspec` was provided that could not be found in the given
-
    /// `namespace`.
-
    #[error("provided revspec '{rev}' could not be parsed into a git object in the namespace '{namespace}'")]
-
    NamespaceRevParseFailure {
-
        /// The namespace we are in when attempting to fetch the `rev`.
-
        namespace: Namespace,
-
        /// The provided revspec that failed to parse.
-
        rev: String,
-
    },
-
    /// When parsing a namespace we may come across one that was an empty
-
    /// string.
-
    #[error("tried parsing the namespace but it was empty")]
-
    EmptyNamespace,
-
    /// A [`str::Utf8Error`] error, which usually occurs when a git object's
-
    /// name is not in UTF-8 form and parsing of it as such fails.
-
    #[error(transparent)]
-
    Utf8Error(#[from] str::Utf8Error),
-
    /// When trying to get the summary for a [`git2::Commit`] some action
-
    /// failed.
-
    #[error("an error occurred trying to get a commit's summary")]
-
    MissingSummary,
-
    /// An error that comes from performing a [`crate::file_system`] operation.
-
    #[error(transparent)]
-
    FileSystem(#[from] file_system::Error),
-
    /// While attempting to calculate a diff for retrieving the
-
    /// [`crate::vcs::git::Browser.last_commit()`], the file path was returned
-
    /// as an `Option::None`.
-
    #[error("last commit has an invalid file path")]
-
    LastCommitException,
-
    /// The requested file was not found.
-
    #[error("path not found for: {0}")]
-
    PathNotFound(file_system::Path),
-
    /// An error that comes from performing a *diff* operations.
-
    #[error(transparent)]
-
    Diff(#[from] diff::git::error::Diff),
-
    /// 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),
-
}
deleted radicle-surf/src/vcs/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,
-
    }
-
}
deleted radicle-surf/src/vcs/git/glob.rs
@@ -1,107 +0,0 @@
-
// 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/history.rs
@@ -1,103 +0,0 @@
-
// This file is part of radicle-surf
-
// <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::{
-
    file_system,
-
    vcs::git::{commit::ToCommit, Commit, Error, RepositoryRef},
-
};
-
use std::convert::TryFrom;
-

-
/// An iterator that produces the history of commits for a given `head`,
-
/// in the `repo`.
-
pub struct History<'a> {
-
    repo: RepositoryRef<'a>,
-
    head: Commit,
-
    revwalk: git2::Revwalk<'a>,
-
    filter_by: Option<FilterBy>,
-
}
-

-
/// Internal implementation, subject to refactoring.
-
enum FilterBy {
-
    File { path: file_system::Path },
-
}
-

-
impl<'a> History<'a> {
-
    /// Creates a new history starting from `head`, in `repo`.
-
    pub fn new<C: ToCommit>(repo: RepositoryRef<'a>, head: C) -> Result<Self, Error> {
-
        let head = head.to_commit(&repo)?;
-
        let mut revwalk = repo.repo_ref.revwalk()?;
-
        revwalk.push(head.id.into())?;
-
        let history = Self {
-
            repo,
-
            head,
-
            revwalk,
-
            filter_by: None,
-
        };
-
        Ok(history)
-
    }
-

-
    /// Returns the first commit (i.e. the head) in the history.
-
    pub fn head(&self) -> &Commit {
-
        &self.head
-
    }
-

-
    /// Returns a modified `History` filtered by `path`.
-
    ///
-
    /// Note that it is possible that a filtered History becomes empty,
-
    /// even though calling `.head()` still returns the original head.
-
    pub fn by_path(mut self, path: file_system::Path) -> Self {
-
        self.filter_by = Some(FilterBy::File { path });
-
        self
-
    }
-
}
-

-
impl<'a> Iterator for History<'a> {
-
    type Item = Result<Commit, Error>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        // Loop through the commits with the optional filtering.
-
        while let Some(oid) = self.revwalk.next() {
-
            let found = oid
-
                .map_err(Error::Git)
-
                .and_then(|oid| {
-
                    let git2_commit = self.repo.repo_ref.find_commit(oid)?;
-

-
                    // Handles the optional filter_by.
-
                    if let Some(FilterBy::File { path }) = &self.filter_by {
-
                        let path_opt = self.repo.diff_commit_and_parents(path, &git2_commit)?;
-
                        if path_opt.is_none() {
-
                            return Ok(None); // Filter out this commit.
-
                        }
-
                    }
-

-
                    let commit = Commit::try_from(git2_commit)?;
-
                    Ok(Some(commit))
-
                })
-
                .transpose();
-
            if found.is_some() {
-
                return found;
-
            }
-
        }
-
        None
-
    }
-
}
-

-
impl<'a> std::fmt::Debug for History<'a> {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        write!(f, "History of {}", self.head.id)
-
    }
-
}
deleted radicle-surf/src/vcs/git/namespace.rs
@@ -1,94 +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::vcs::git::error::Error;
-
use nonempty::NonEmpty;
-
pub use radicle_git_ext::Oid;
-
use std::{convert::TryFrom, fmt, str};
-

-
/// A `Namespace` value allows us to switch the git namespace of
-
/// a repo.
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct Namespace {
-
    /// Since namespaces can be nested we have a vector of strings.
-
    /// This means that the namespaces `"foo/bar"` is represented as
-
    /// `vec!["foo", "bar"]`.
-
    pub(super) values: NonEmpty<String>,
-
}
-

-
impl Namespace {
-
    /// Appends a `refname` to this namespace, and returns
-
    /// the full reference path.
-
    pub fn append_refname(&self, refname: &str) -> String {
-
        let mut prefix = String::new();
-
        for value in self.values.iter() {
-
            prefix = format!("{}refs/namespaces/{}/", &prefix, value);
-
        }
-
        format!("{}{}", &prefix, refname)
-
    }
-
}
-

-
impl fmt::Display for Namespace {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        let values: Vec<_> = self.values.clone().into();
-
        write!(f, "{}", values.join("/"))
-
    }
-
}
-

-
impl TryFrom<&str> for Namespace {
-
    type Error = Error;
-

-
    fn try_from(namespace: &str) -> Result<Self, Self::Error> {
-
        let values = namespace.split('/').map(|n| n.to_string()).collect();
-
        NonEmpty::from_vec(values)
-
            .map(|values| Self { values })
-
            .ok_or(Error::EmptyNamespace)
-
    }
-
}
-

-
impl TryFrom<&[u8]> for Namespace {
-
    type Error = Error;
-

-
    fn try_from(namespace: &[u8]) -> Result<Self, Self::Error> {
-
        str::from_utf8(namespace)
-
            .map_err(Error::from)
-
            .and_then(Namespace::try_from)
-
    }
-
}
-

-
impl TryFrom<git2::Reference<'_>> for Namespace {
-
    type Error = Error;
-

-
    fn try_from(reference: git2::Reference) -> Result<Self, Self::Error> {
-
        let re = regex::Regex::new(r"refs/namespaces/([^/]+)/").unwrap();
-
        let ref_name = str::from_utf8(reference.name_bytes())?;
-
        let values = re
-
            .find_iter(ref_name)
-
            .map(|m| {
-
                String::from(
-
                    m.as_str()
-
                        .trim_start_matches("refs/namespaces/")
-
                        .trim_end_matches('/'),
-
                )
-
            })
-
            .collect::<Vec<_>>();
-

-
        NonEmpty::from_vec(values)
-
            .map(|values| Self { values })
-
            .ok_or(Error::EmptyNamespace)
-
    }
-
}
deleted radicle-surf/src/vcs/git/repo.rs
@@ -1,557 +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::{
-
    diff::*,
-
    file_system,
-
    file_system::{directory, DirectoryEntry, Label},
-
    vcs::git::{
-
        error::*,
-
        Branch,
-
        BranchName,
-
        Commit,
-
        Glob,
-
        History,
-
        Namespace,
-
        Revision,
-
        Signature,
-
        Stats,
-
        Tag,
-
        TagName,
-
    },
-
};
-
use directory::{Directory, FileContent};
-
use radicle_git_ext::Oid;
-
use std::{
-
    collections::{btree_set, BTreeMap, BTreeSet},
-
    convert::TryFrom,
-
    path::PathBuf,
-
    str,
-
};
-

-
use super::commit::ToCommit;
-

-
/// Wrapper around the `git2`'s `git2::Repository` type.
-
/// This is to to limit the functionality that we can do
-
/// on the underlying object.
-
pub struct Repository(pub(super) git2::Repository);
-

-
/// A reference-only `Repository`. This means that we cannot mutate the
-
/// underlying `Repository`. Not being able to mutate the `Repository` means
-
/// that the functions defined for `RepositoryRef` should be thread-safe.
-
///
-
/// # Construction
-
///
-
/// Use the `From<&'a git2::Repository>` implementation to construct a
-
/// `RepositoryRef`.
-
#[derive(Clone, Copy)]
-
pub struct RepositoryRef<'a> {
-
    pub(crate) repo_ref: &'a git2::Repository,
-
}
-

-
// RepositoryRef should be safe to transfer across thread boundaries since it
-
// only holds a reference to git2::Repository. git2::Repository is also Send
-
// (see: https://docs.rs/git2/0.13.5/src/git2/repo.rs.html#46)
-
unsafe impl<'a> Send for RepositoryRef<'a> {}
-

-
impl<'a> From<&'a git2::Repository> for RepositoryRef<'a> {
-
    fn from(repo_ref: &'a git2::Repository) -> Self {
-
        RepositoryRef { repo_ref }
-
    }
-
}
-

-
// 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> {
-
        self.repo_ref
-
            .namespace_bytes()
-
            .map(Namespace::try_from)
-
            .transpose()
-
    }
-

-
    /// 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)
-
    }
-

-
    /// 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)
-
    }
-

-
    /// 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.
-
    pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
-
        let from_commit = self.get_git2_commit(from.object_id(self)?)?;
-
        let to_commit = self.get_git2_commit(to.object_id(self)?)?;
-
        self.diff_commits(None, Some(&from_commit), &to_commit)
-
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
-
    }
-

-
    /// Get the [`Diff`] of a commit with no parents.
-
    pub fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
-
        let commit = self.get_git2_commit(rev.object_id(self)?)?;
-
        self.diff_commits(None, None, &commit)
-
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
-
    }
-

-
    /// Get the diff introduced by a particlar rev.
-
    pub fn diff_from_parent<C: ToCommit>(&self, commit: C) -> Result<Diff, Error> {
-
        let commit = commit.to_commit(self)?;
-
        match commit.parents.first() {
-
            Some(parent) => self.diff(*parent, commit.id),
-
            None => self.initial_diff(commit.id),
-
        }
-
    }
-

-
    /// Parse an [`Oid`] from the given string.
-
    pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
-
        Ok(self.repo_ref.revparse_single(oid)?.id().into())
-
    }
-

-
    /// Returns a top level `Directory` without nested sub-directories.
-
    ///
-
    /// To visit inside any nested sub-directories, call `directory.get(&repo)`
-
    /// on the sub-directory.
-
    pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
-
        let commit = commit.to_commit(self)?;
-
        let git2_commit = self.repo_ref.find_commit((commit.id).into())?;
-
        let tree = git2_commit.as_object().peel_to_tree()?;
-
        Ok(Directory {
-
            name: Label::root(),
-
            oid: tree.id().into(),
-
        })
-
    }
-

-
    /// Retrieves the content of a directory.
-
    pub(crate) fn directory_get(
-
        &self,
-
        d: &Directory,
-
    ) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
-
        let git2_tree = self.repo_ref.find_tree(d.oid.into())?;
-
        let map = self.tree_first_level(git2_tree)?;
-
        Ok(map)
-
    }
-

-
    /// Returns a map of the first level entries in `tree`.
-
    fn tree_first_level(&self, tree: git2::Tree) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
-
        let mut map = BTreeMap::new();
-

-
        // Walks only the first level of entries.
-
        tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| {
-
            let oid = entry.id().into();
-
            let label = match entry.name() {
-
                Some(name) => match name.parse::<Label>() {
-
                    Ok(label) => label,
-
                    Err(_) => return git2::TreeWalkResult::Abort,
-
                },
-
                None => return git2::TreeWalkResult::Abort,
-
            };
-

-
            match entry.kind() {
-
                Some(git2::ObjectType::Tree) => {
-
                    let dir = Directory::new(label.clone(), oid);
-
                    map.insert(label, DirectoryEntry::Directory(dir));
-
                    return git2::TreeWalkResult::Skip; // Not go into nested
-
                                                       // directories.
-
                },
-
                Some(git2::ObjectType::Blob) => {
-
                    let f = directory::File {
-
                        name: label.clone(),
-
                        oid,
-
                    };
-
                    map.insert(label, DirectoryEntry::File(f));
-
                },
-
                _ => {
-
                    return git2::TreeWalkResult::Skip;
-
                },
-
            }
-

-
            git2::TreeWalkResult::Ok
-
        })?;
-

-
        Ok(map)
-
    }
-

-
    /// Returns the last commit, if exists, for a `path` in the history of
-
    /// `rev`.
-
    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()
-
    }
-

-
    /// Returns a commit for `rev` if exists.
-
    pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
-
        let oid = rev.object_id(self)?;
-
        match self.repo_ref.find_commit(oid.into()) {
-
            Ok(commit) => Commit::try_from(commit),
-
            Err(e) => Err(Error::Git(e)),
-
        }
-
    }
-

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

-
        let contributors = history
-
            .filter_map(|commit| match commit {
-
                Ok(commit) => {
-
                    commits += 1;
-
                    Some((commit.author.name, commit.author.email))
-
                },
-
                Err(_) => None,
-
            })
-
            .collect::<BTreeSet<_>>();
-

-
        Ok(Stats {
-
            branches,
-
            commits,
-
            contributors: contributors.len(),
-
        })
-
    }
-

-
    /// Obtain the file content
-
    pub(crate) fn file_content(&self, object_id: Oid) -> Result<FileContent, Error> {
-
        let blob = self.repo_ref.find_blob(object_id.into())?;
-
        Ok(FileContent::new(blob))
-
    }
-

-
    /// Return the size of a file
-
    pub(crate) fn file_size(&self, oid: Oid) -> Result<usize, Error> {
-
        let blob = self.repo_ref.find_blob(oid.into())?;
-
        Ok(blob.size())
-
    }
-

-
    /// 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?;
-
        branches.sort();
-

-
        Ok(branches)
-
    }
-

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

-
        Ok(tags)
-
    }
-

-
    /// Returns the Oid of the current HEAD
-
    pub fn head_oid(&self) -> Result<Oid, Error> {
-
        let head = self.repo_ref.head()?;
-
        let head_commit = head.peel_to_commit()?;
-
        Ok(head_commit.id().into())
-
    }
-

-
    /// Switch to a `namespace`
-
    pub fn switch_namespace(&self, namespace: &str) -> Result<(), Error> {
-
        Ok(self.repo_ref.set_namespace(namespace)?)
-
    }
-

-
    /// Returns a full reference name with namespace(s) included.
-
    pub(crate) fn namespaced_refname(&self, refname: &str) -> Result<String, Error> {
-
        let fullname = match self.which_namespace()? {
-
            Some(namespace) => namespace.append_refname(refname),
-
            None => refname.to_string(),
-
        };
-
        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)
-
    }
-

-
    /// Extract the signature from a commit
-
    ///
-
    /// # Arguments
-
    ///
-
    /// `commit_oid` - The object ID of the commit
-
    /// `field` - the name of the header field containing the signature block;
-
    ///           pass `None` to extract the default 'gpgsig'
-
    pub fn extract_signature(
-
        &self,
-
        commit_oid: &Oid,
-
        field: Option<&str>,
-
    ) -> Result<Option<Signature>, Error> {
-
        // Match is necessary here because according to the documentation for
-
        // git_commit_extract_signature at
-
        // https://libgit2.org/libgit2/#HEAD/group/commit/git_commit_extract_signature
-
        // the return value for a commit without a signature will be GIT_ENOTFOUND
-
        match self.repo_ref.extract_signature(commit_oid, field) {
-
            Err(error) => {
-
                if error.code() == git2::ErrorCode::NotFound {
-
                    Ok(None)
-
                } else {
-
                    Err(error.into())
-
                }
-
            },
-
            Ok(sig) => Ok(Some(Signature::from(sig.0))),
-
        }
-
    }
-

-
    /// Lists branches that are reachable from `oid`.
-
    pub fn revision_branches(&self, oid: &Oid, glob: &Glob<Branch>) -> Result<Vec<Branch>, Error> {
-
        let mut contained_branches = vec![];
-
        for branch in self.branches(glob)? {
-
            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)
-
    }
-

-
    fn reachable_from(&self, reference: &git2::Reference, oid: &Oid) -> Result<bool, Error> {
-
        let git2_oid = (*oid).into();
-
        let other = reference.peel_to_commit()?.id();
-
        let is_descendant = self.repo_ref.graph_descendant_of(other, git2_oid)?;
-

-
        Ok(other == git2_oid || is_descendant)
-
    }
-

-
    pub(crate) fn get_commit_file(
-
        &self,
-
        git2_commit: &git2::Commit,
-
        path: file_system::Path,
-
    ) -> Result<FileContent, Error> {
-
        let git2_tree = git2_commit.tree()?;
-
        let entry = git2_tree.get_path(PathBuf::from(&path).as_ref())?;
-
        let object = entry.to_object(self.repo_ref)?;
-
        let blob = object.into_blob().map_err(|_| Error::PathNotFound(path))?;
-
        Ok(FileContent::new(blob))
-
    }
-

-
    pub(crate) fn diff_commit_and_parents(
-
        &self,
-
        path: &file_system::Path,
-
        commit: &git2::Commit,
-
    ) -> Result<Option<file_system::Path>, Error> {
-
        let mut parents = commit.parents();
-

-
        let diff = self.diff_commits(Some(path), parents.next().as_ref(), commit)?;
-
        if let Some(_delta) = diff.deltas().next() {
-
            Ok(Some(path.clone()))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-

-
    fn diff_commits(
-
        &self,
-
        path: Option<&file_system::Path>,
-
        from: Option<&git2::Commit>,
-
        to: &git2::Commit,
-
    ) -> Result<git2::Diff, Error> {
-
        let new_tree = to.tree()?;
-
        let old_tree = from.map_or(Ok(None), |c| c.tree().map(Some))?;
-

-
        let mut opts = git2::DiffOptions::new();
-
        if let Some(path) = path {
-
            opts.pathspec(path);
-
            // We're skipping the binary pass because we won't be inspecting deltas.
-
            opts.skip_binary_check(true);
-
        }
-

-
        let mut diff =
-
            self.repo_ref
-
                .diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(&mut opts))?;
-

-
        // Detect renames by default.
-
        let mut find_opts = git2::DiffFindOptions::new();
-
        find_opts.renames(true);
-
        diff.find_similar(Some(&mut find_opts))?;
-

-
        Ok(diff)
-
    }
-

-
    /// Returns the history with the `head` commit.
-
    pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
-
        History::new(*self, head)
-
    }
-
}
-

-
impl<'a> std::fmt::Debug for RepositoryRef<'a> {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        write!(f, ".git")
-
    }
-
}
-

-
impl Repository {
-
    /// Open a git repository given its exact URI.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`Error::Git`]
-
    pub fn open(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
-
        git2::Repository::open(repo_uri)
-
            .map(Repository)
-
            .map_err(Error::from)
-
    }
-

-
    /// Attempt to open a git repository at or above `repo_uri` in the file
-
    /// system.
-
    pub fn discover(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
-
        git2::Repository::discover(repo_uri)
-
            .map(Repository)
-
            .map_err(Error::from)
-
    }
-

-
    /// Since our operations are read-only when it comes to surfing a repository
-
    /// we have a separate struct called [`RepositoryRef`]. This turns an owned
-
    /// [`Repository`] into a [`RepositoryRef`].
-
    pub fn as_ref(&'_ self) -> RepositoryRef<'_> {
-
        RepositoryRef { repo_ref: &self.0 }
-
    }
-
}
-

-
impl<'a> From<&'a Repository> for RepositoryRef<'a> {
-
    fn from(repo: &'a Repository) -> Self {
-
        repo.as_ref()
-
    }
-
}
-

-
impl From<git2::Repository> for Repository {
-
    fn from(repo: git2::Repository) -> Self {
-
        Repository(repo)
-
    }
-
}
-

-
impl std::fmt::Debug for Repository {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        write!(f, ".git")
-
    }
-
}
deleted radicle-surf/src/vcs/git/stats.rs
@@ -1,36 +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/>.
-

-
pub use radicle_git_ext::Oid;
-

-
#[cfg(feature = "serialize")]
-
use serde::Serialize;
-

-
/// Stats for a repository
-
#[cfg_attr(
-
    feature = "serialize",
-
    derive(Serialize),
-
    serde(rename_all = "camelCase")
-
)]
-
pub struct Stats {
-
    /// Number of commits
-
    pub commits: usize,
-
    /// Number of local branches
-
    pub branches: usize,
-
    /// Number of contributors
-
    pub contributors: usize,
-
}
deleted radicle-surf/src/vcs/git/tag.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::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(RefString);
-

-
impl fmt::Display for TagName {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        self.0.fmt(f)
-
    }
-
}
-

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

-
    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())
-
    }
-
}
-

-
/// The static information of a [`git2::Tag`].
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub enum Tag {
-
    /// A light-weight git 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>,
-
    },
-
    /// 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,
-
        /// 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>,
-
    },
-
}
-

-
impl Tag {
-
    /// Get the `Oid` of the tag, regardless of its type.
-
    pub fn id(&self) -> Oid {
-
        match self {
-
            Self::Light { id, .. } => *id,
-
            Self::Annotated { id, .. } => *id,
-
        }
-
    }
-

-
    /// 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(),
-
        }
-
    }
-

-
    /// Returns the full ref name of the tag.
-
    pub fn refname(&self) -> String {
-
        self.name().refname()
-
    }
-
}
-

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

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

-
        let target_id = tag.target_id().into();
-

-
        let name = TagName::try_from(tag.name_bytes())?;
-

-
        let tagger = tag.tagger().map(Author::try_from).transpose()?;
-

-
        let message = tag
-
            .message_bytes()
-
            .map(str::from_utf8)
-
            .transpose()?
-
            .map(|message| message.into());
-

-
        Ok(Tag::Annotated {
-
            id,
-
            target_id,
-
            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())?;
-

-
        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)
-
        };
-

-
        match reference.peel_to_tag() {
-
            Ok(tag) => Ok(Tag::try_from(tag)?),
-
            Err(err) => {
-
                // 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
-
                {
-
                    let commit = reference.peel_to_commit()?;
-
                    Ok(Tag::Light {
-
                        id: commit.id().into(),
-
                        name,
-
                        remote,
-
                    })
-
                } else {
-
                    Err(err.into())
-
                }
-
            },
-
        }
-
    }
-
}
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,82 +670,38 @@ 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)
            })
        ]
    }
}

#[cfg(test)]
-
mod ext {
-
    use radicle_surf::vcs::git::ext::*;
-

-
    #[test]
-
    fn test_try_extract_refname() {
-
        assert_eq!(try_extract_refname("refs/heads/dev"), Ok("dev".to_string()));
-

-
        assert_eq!(
-
            try_extract_refname("refs/heads/master"),
-
            Ok("master".to_string())
-
        );
-

-
        assert_eq!(
-
            try_extract_refname("refs/remotes/banana/pineapple"),
-
            Ok("banana/pineapple".to_string())
-
        );
-

-
        assert_eq!(
-
            try_extract_refname("refs/remotes/origin/master"),
-
            Ok("origin/master".to_string())
-
        );
-

-
        assert_eq!(
-
            try_extract_refname("refs/namespaces/golden/refs/heads/banana"),
-
            Ok("banana".to_string())
-
        );
-

-
        assert_eq!(
-
            try_extract_refname("refs/namespaces/golden/refs/tags/v0.1.0"),
-
            Ok("v0.1.0".to_string())
-
        );
-

-
        assert_eq!(
-
            try_extract_refname("refs/namespaces/golden/refs/namespaces/silver/refs/heads/master"),
-
            Ok("master".to_string())
-
        );
-

-
        assert_eq!(
-
            try_extract_refname("refs/namespaces/golden/refs/remotes/kickflip/heads/heelflip"),
-
            Ok("kickflip/heelflip".to_string())
-
        );
-
    }
-
}
-

-
#[cfg(test)]
mod reference {
    use super::*;
-
    use radicle_surf::vcs::git::{Glob, Tag};
+
    use radicle_surf::git::Glob;

    #[test]
    fn test_branches() {
@@ -724,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())
@@ -741,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();
@@ -767,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`.
@@ -798,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);
@@ -820,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();