Radish alpha
r
Git libraries for Radicle
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle-surf: move vcs::git to git
Fintan Halpenny committed 3 years ago
commit c6c8fe316490d1f2f95332b00c7fa20f8e0fc80d
parent 1649ff3e71f4439c3ef9bb81e1d27df4ec86b28f
31 files changed +1814 -1887
modified radicle-surf/benches/last_commit.rs
@@ -18,7 +18,7 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use radicle_surf::{
    file_system::{unsound, Path},
-
    vcs::git::{Branch, Repository},
+
    git::{Branch, Repository},
};

fn last_commit_comparison(c: &mut Criterion) {
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
@@ -26,10 +26,9 @@ use serde::{
use crate::{
    diff,
    file_system,
-
    git::Glob,
+
    git::{self, BranchName, Glob, RepositoryRef},
    person::Person,
    revision::Revision,
-
    vcs::git::{self, BranchName, RepositoryRef},
};

use radicle_git_ext::Oid;
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,155 @@
+
// 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())
+
    }
+
}
+

+
/// 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)?)
+
    }
+
}
added radicle-surf/src/git/branch.rs
@@ -0,0 +1,186 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
use crate::git::{self, error::Error, ext};
+
#[cfg(feature = "serialize")]
+
use serde::{Deserialize, Serialize};
+
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
+

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

+
impl From<BranchType> for git2::BranchType {
+
    fn from(other: BranchType) -> Self {
+
        match other {
+
            BranchType::Local => git2::BranchType::Local,
+
            BranchType::Remote { .. } => git2::BranchType::Remote,
+
        }
+
    }
+
}
+

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

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

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

+
/// Enumeration of errors that can occur in operations from [`crate::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),
+
}
added radicle-surf/src/git/ext.rs
@@ -0,0 +1,68 @@
+
// 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,
+
    }
+
}
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,557 @@
+
// 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},
+
    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")
+
    }
+
}
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,183 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
use crate::git::{self, error::Error, Author};
+
use git_ref_format::RefString;
+
use radicle_git_ext::Oid;
+
use std::{convert::TryFrom, fmt, str};
+

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

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

+
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/src/lib.rs
@@ -82,7 +82,7 @@
//! ```
pub mod diff;
pub mod file_system;
-
pub mod vcs;
+
pub mod git;

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

// Private modules
mod nonempty;
-

-
pub use crate::vcs::git;
modified radicle-surf/src/revision.rs
@@ -24,10 +24,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, BranchName, Glob, RepositoryRef, TagName};

/// Types of a peer.
pub enum Category<P, U> {
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,157 +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())
-
    }
-
}
-

-
/// 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/src/git.rs
@@ -669,54 +669,9 @@ mod branch {
}

#[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, Tag};

    #[test]
    fn test_branches() {