Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'han/del-git'
Fintan Halpenny committed 3 years ago
commit 2bb35ed99640d960e277befd84b5c54b5dfc893b
parent 575d44e
50 files changed +3361 -3361
modified radicle-surf/examples/browsing.rs
@@ -26,7 +26,7 @@

use radicle_surf::{
    fs::{self, Directory},
-
    git::Repository,
+
    Repository,
};
use std::{env, time::Instant};

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, git};
+
use radicle_surf::{diff::Diff, Repository};

fn main() {
    let options = get_options_or_exit();
@@ -46,8 +46,8 @@ fn get_options_or_exit() -> Options {
    }
}

-
fn init_repository_or_exit(path_to_repo: &str) -> git::Repository {
-
    match git::Repository::open(path_to_repo) {
+
fn init_repository_or_exit(path_to_repo: &str) -> Repository {
+
    match Repository::open(path_to_repo) {
        Ok(repo) => repo,
        Err(e) => {
            println!("Failed to create repository: {:?}", e);
added radicle-surf/src/branch.rs
@@ -0,0 +1,337 @@
+
use std::{
+
    convert::TryFrom,
+
    str::{self, FromStr},
+
};
+

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

+
use crate::refs::refstr_join;
+

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

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

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

+
    /// Return the short `Branch` refname,
+
    /// e.g. `fix/ref-format`.
+
    pub fn short_name(&self) -> &RefString {
+
        match self {
+
            Branch::Local(local) => local.short_name(),
+
            Branch::Remote(remote) => remote.short_name(),
+
        }
+
    }
+

+
    /// Give back the fully qualified `Branch` refname,
+
    /// e.g. `refs/remotes/origin/fix/ref-format`,
+
    /// `refs/heads/fix/ref-format`.
+
    pub fn refname(&self) -> Qualified {
+
        match self {
+
            Branch::Local(local) => local.refname(),
+
            Branch::Remote(remote) => remote.refname(),
+
        }
+
    }
+
}
+

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

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

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

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

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

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

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

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

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

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

+
    /// Return the short `Local` refname,
+
    /// e.g. `fix/ref-format`.
+
    pub fn short_name(&self) -> &RefString {
+
        &self.name
+
    }
+

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

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

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

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

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

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

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

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

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

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

+
    /// Parse the `name` from the form `refs/remotes/<remote>/<rest>`.
+
    ///
+
    /// If the `name` is not of this form, then `None` is returned.
+
    pub fn from_refs_remotes<R>(name: R) -> Option<Self>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        let qualified = name.as_ref().qualified()?;
+
        let (_refs, remotes, remote, cs) = qualified.non_empty_components();
+
        (remotes == component::REMOTES).then_some(Self {
+
            name: cs.collect(),
+
            remote: remote.to_ref_string(),
+
        })
+
    }
+

+
    /// Return the short `Remote` refname,
+
    /// e.g. `fix/ref-format`.
+
    pub fn short_name(&self) -> &RefString {
+
        &self.name
+
    }
+

+
    /// Return the remote of the `Remote`'s refname,
+
    /// e.g. `origin`.
+
    pub fn remote(&self) -> &RefString {
+
        &self.remote
+
    }
+

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

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

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

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

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

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

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

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

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

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

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

+
    #[derive(Debug, Error)]
+
    pub enum Remote {
+
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
+
        NotQualified(String),
+
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
+
        NotRemotes(RefString),
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] std::str::Utf8Error),
+
    }
+
}
added radicle-surf/src/commit.rs
@@ -0,0 +1,179 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
use std::{convert::TryFrom, str};
+

+
use git_ext::Oid;
+
use thiserror::Error;
+

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

+
#[derive(Debug, Error)]
+
pub enum Error {
+
    /// 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,
+
    #[error(transparent)]
+
    Utf8Error(#[from] str::Utf8Error),
+
}
+

+
/// `Author` is the static information of a [`git2::Signature`].
+
#[cfg_attr(feature = "serde", 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 = "serde",
+
        serde(
+
            serialize_with = "serialize_time",
+
            deserialize_with = "deserialize_time"
+
        )
+
    )]
+
    pub time: git2::Time,
+
}
+

+
#[cfg(feature = "serde")]
+
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 = "serde")]
+
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 = "serde", 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()
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
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,
+
        })
+
    }
+
}
modified radicle-surf/src/diff.rs
@@ -29,8 +29,8 @@ pub mod git;
/// The serializable representation of a `git diff`.
///
/// A [`Diff`] can be retrieved by the following functions:
-
///    * [`crate::git::Repository::diff`]
-
///    * [`crate::git::Repository::diff_commit`]
+
///    * [`crate::Repository::diff`]
+
///    * [`crate::Repository::diff_commit`]
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Diff {
modified radicle-surf/src/fs.rs
@@ -31,7 +31,7 @@ use git2::Blob;
use radicle_git_ext::{is_not_found_err, Oid};
use radicle_std_ext::result::ResultExt as _;

-
use crate::git::{Commit, Repository, Revision};
+
use crate::{Commit, Repository, Revision};

pub mod error {
    use thiserror::Error;
deleted radicle-surf/src/git.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 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::{convert::Infallible, str::FromStr};
-

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

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

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

-
mod history;
-
pub use history::History;
-

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

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

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

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

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

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

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

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

-
/// Supports various ways to specify a revision used in Git.
-
pub trait Revision {
-
    type Error: std::error::Error + Send + Sync + 'static;
-

-
    /// Returns the object id of this revision in `repo`.
-
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error>;
-
}
-

-
impl Revision for RefString {
-
    type Error = git2::Error;
-

-
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
-
        repo.refname_to_id(self)
-
    }
-
}
-

-
impl Revision for Qualified<'_> {
-
    type Error = git2::Error;
-

-
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
-
        repo.refname_to_id(self)
-
    }
-
}
-

-
impl Revision for Oid {
-
    type Error = Infallible;
-

-
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
-
        Ok(*self)
-
    }
-
}
-

-
impl Revision for &str {
-
    type Error = git2::Error;
-

-
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
-
        Oid::from_str(self).map(Oid::from)
-
    }
-
}
-

-
impl Revision for Branch {
-
    type Error = Error;
-

-
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
-
        let refname = repo.namespaced_refname(&self.refname())?;
-
        Ok(repo.refname_to_id(&refname)?)
-
    }
-
}
-

-
impl Revision for Tag {
-
    type Error = Infallible;
-

-
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
-
        Ok(self.id())
-
    }
-
}
-

-
impl Revision for String {
-
    type Error = git2::Error;
-

-
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
-
        Oid::from_str(self).map(Oid::from)
-
    }
-
}
-

-
impl<R: Revision> Revision for &R {
-
    type Error = R::Error;
-

-
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
-
        (*self).object_id(repo)
-
    }
-
}
-

-
impl<R: Revision> Revision for Box<R> {
-
    type Error = R::Error;
-

-
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
-
        self.as_ref().object_id(repo)
-
    }
-
}
-

-
/// A common trait for anything that can convert to a `Commit`.
-
pub trait ToCommit {
-
    type Error: std::error::Error + Send + Sync + 'static;
-

-
    /// Converts to a commit in `repo`.
-
    fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error>;
-
}
-

-
impl ToCommit for Commit {
-
    type Error = Infallible;
-

-
    fn to_commit(self, _repo: &Repository) -> Result<Commit, Self::Error> {
-
        Ok(self)
-
    }
-
}
-

-
impl<R: Revision> ToCommit for R {
-
    type Error = Error;
-

-
    fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error> {
-
        let oid = repo.object_id(&self)?;
-
        let commit = repo.find_commit(oid)?;
-
        Ok(Commit::try_from(commit)?)
-
    }
-
}
-

-
pub(crate) fn refstr_join<'a>(c: Component<'a>, cs: Components<'a>) -> RefString {
-
    std::iter::once(c).chain(cs).collect::<RefString>()
-
}
deleted radicle-surf/src/git/branch.rs
@@ -1,336 +0,0 @@
-
use std::{
-
    convert::TryFrom,
-
    str::{self, FromStr},
-
};
-

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

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

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

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

-
    /// Return the short `Branch` refname,
-
    /// e.g. `fix/ref-format`.
-
    pub fn short_name(&self) -> &RefString {
-
        match self {
-
            Branch::Local(local) => local.short_name(),
-
            Branch::Remote(remote) => remote.short_name(),
-
        }
-
    }
-

-
    /// Give back the fully qualified `Branch` refname,
-
    /// e.g. `refs/remotes/origin/fix/ref-format`,
-
    /// `refs/heads/fix/ref-format`.
-
    pub fn refname(&self) -> Qualified {
-
        match self {
-
            Branch::Local(local) => local.refname(),
-
            Branch::Remote(remote) => remote.refname(),
-
        }
-
    }
-
}
-

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

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

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

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

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

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

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

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

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

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

-
    /// Return the short `Local` refname,
-
    /// e.g. `fix/ref-format`.
-
    pub fn short_name(&self) -> &RefString {
-
        &self.name
-
    }
-

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

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

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

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

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

-
impl FromStr for Local {
-
    type Err = error::Local;
-

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

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

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

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

-
    /// Parse the `name` from the form `refs/remotes/<remote>/<rest>`.
-
    ///
-
    /// If the `name` is not of this form, then `None` is returned.
-
    pub fn from_refs_remotes<R>(name: R) -> Option<Self>
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        let qualified = name.as_ref().qualified()?;
-
        let (_refs, remotes, remote, cs) = qualified.non_empty_components();
-
        (remotes == component::REMOTES).then_some(Self {
-
            name: cs.collect(),
-
            remote: remote.to_ref_string(),
-
        })
-
    }
-

-
    /// Return the short `Remote` refname,
-
    /// e.g. `fix/ref-format`.
-
    pub fn short_name(&self) -> &RefString {
-
        &self.name
-
    }
-

-
    /// Return the remote of the `Remote`'s refname,
-
    /// e.g. `origin`.
-
    pub fn remote(&self) -> &RefString {
-
        &self.remote
-
    }
-

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

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

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

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

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

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

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

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

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

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

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

-
    #[derive(Debug, Error)]
-
    pub enum Remote {
-
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
-
        NotQualified(String),
-
        #[error("the refname '{0}' did not begin with 'refs/remotes'")]
-
        NotRemotes(RefString),
-
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
-
        #[error(transparent)]
-
        Utf8(#[from] std::str::Utf8Error),
-
    }
-
}
deleted radicle-surf/src/git/commit.rs
@@ -1,179 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
use std::{convert::TryFrom, str};
-

-
use git_ext::Oid;
-
use thiserror::Error;
-

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

-
#[derive(Debug, Error)]
-
pub enum Error {
-
    /// 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,
-
    #[error(transparent)]
-
    Utf8Error(#[from] str::Utf8Error),
-
}
-

-
/// `Author` is the static information of a [`git2::Signature`].
-
#[cfg_attr(feature = "serde", 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 = "serde",
-
        serde(
-
            serialize_with = "serialize_time",
-
            deserialize_with = "deserialize_time"
-
        )
-
    )]
-
    pub time: git2::Time,
-
}
-

-
#[cfg(feature = "serde")]
-
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 = "serde")]
-
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 = "serde", 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()
-
    }
-
}
-

-
#[cfg(feature = "serde")]
-
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,
-
        })
-
    }
-
}
deleted radicle-surf/src/git/glob.rs
@@ -1,357 +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 std::marker::PhantomData;
-

-
use git_ref_format::{
-
    refname,
-
    refspec::{self, PatternString, QualifiedPattern},
-
    Qualified,
-
    RefStr,
-
    RefString,
-
};
-
use thiserror::Error;
-

-
use crate::git::{Branch, Local, Namespace, Remote, Tag};
-

-
#[derive(Debug, Error)]
-
pub enum Error {
-
    #[error(transparent)]
-
    RefFormat(#[from] git_ref_format::Error),
-
}
-

-
/// A collection of globs for T (a git reference type).
-
#[derive(Clone, Debug)]
-
pub struct Glob<T> {
-
    globs: Vec<QualifiedPattern<'static>>,
-
    glob_type: PhantomData<T>, // To support different methods for different T.
-
}
-

-
impl<T> Default for Glob<T> {
-
    fn default() -> Self {
-
        Self {
-
            globs: Default::default(),
-
            glob_type: PhantomData,
-
        }
-
    }
-
}
-

-
impl<T> Glob<T> {
-
    /// Return the [`QualifiedPattern`] globs of this `Glob`.
-
    pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
-
        self.globs.iter()
-
    }
-

-
    /// Combine two `Glob`s together by combining their glob lists together.
-
    ///
-
    /// Note that the `Glob`s must result in the same type,
-
    /// e.g. `Glob<Tag>` can only combine with `Glob<Tag>`,
-
    /// `Glob<Local>` can combine with `Glob<Remote>`, etc.
-
    pub fn and(mut self, other: impl Into<Self>) -> Self {
-
        self.globs.extend(other.into().globs);
-
        self
-
    }
-
}
-

-
impl Glob<Namespace> {
-
    /// Creates the `Glob` that mathces all `refs/namespaces`.
-
    pub fn all_namespaces() -> Self {
-
        Self::namespaces(refspec::pattern!("*"))
-
    }
-

-
    /// Creates a `Glob` for `refs/namespaces`, starting with `glob`.
-
    pub fn namespaces(glob: PatternString) -> Self {
-
        let globs = vec![Self::qualify(glob)];
-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-

-
    /// Adds a `refs/namespaces` pattern to this `Glob`.
-
    pub fn insert(mut self, glob: PatternString) -> Self {
-
        self.globs.push(Self::qualify(glob));
-
        self
-
    }
-

-
    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
-
        qualify(&refname!("refs/namespaces"), glob).expect("BUG: pattern is qualified")
-
    }
-
}
-

-
impl FromIterator<PatternString> for Glob<Namespace> {
-
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
-
        let globs = iter
-
            .into_iter()
-
            .map(|pat| {
-
                qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
-
            })
-
            .collect();
-

-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-
}
-

-
impl Extend<PatternString> for Glob<Namespace> {
-
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
-
        self.globs.extend(iter.into_iter().map(|pat| {
-
            qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
-
        }))
-
    }
-
}
-

-
impl Glob<Tag> {
-
    /// Creates a `Glob` that matches all `refs/tags`.
-
    pub fn all_tags() -> Self {
-
        Self::tags(refspec::pattern!("*"))
-
    }
-

-
    /// Creates a `Glob` for `refs/tags`, starting with `glob`.
-
    pub fn tags(glob: PatternString) -> Self {
-
        let globs = vec![Self::qualify(glob)];
-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-

-
    /// Adds a `refs/tags` pattern to this `Glob`.
-
    pub fn insert(mut self, glob: PatternString) -> Self {
-
        self.globs.push(Self::qualify(glob));
-
        self
-
    }
-

-
    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
-
        qualify(&refname!("refs/tags"), glob).expect("BUG: pattern is qualified")
-
    }
-
}
-

-
impl FromIterator<PatternString> for Glob<Tag> {
-
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
-
        let globs = iter
-
            .into_iter()
-
            .map(|pat| qualify(&refname!("refs/tags"), pat).expect("BUG: pattern is qualified"))
-
            .collect();
-

-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-
}
-

-
impl Extend<PatternString> for Glob<Tag> {
-
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
-
        self.globs.extend(
-
            iter.into_iter()
-
                .map(|pat| qualify(&refname!("refs/tag"), pat).expect("BUG: pattern is qualified")),
-
        )
-
    }
-
}
-

-
impl Glob<Local> {
-
    /// Creates the `Glob` that mathces all `refs/heads`.
-
    pub fn all_heads() -> Self {
-
        Self::heads(refspec::pattern!("*"))
-
    }
-

-
    /// Creates a `Glob` for `refs/heads`, starting with `glob`.
-
    pub fn heads(glob: PatternString) -> Self {
-
        let globs = vec![Self::qualify_heads(glob)];
-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-

-
    /// Adds a `refs/heads` pattern to this `Glob`.
-
    pub fn insert(mut self, glob: PatternString) -> Self {
-
        self.globs.push(Self::qualify_heads(glob));
-
        self
-
    }
-

-
    /// When chaining `Glob<Local>` with `Glob<Remote>`, use
-
    /// `branches` to convert this `Glob<Local>` into a
-
    /// `Glob<Branch>`.
-
    ///
-
    /// # Example
-
    /// ```no_run
-
    /// Glob::heads(pattern!("features/*"))
-
    ///     .insert(pattern!("qa/*"))
-
    ///     .branches()
-
    ///     .and(Glob::remotes(pattern!("origin/features/*")))
-
    /// ```
-
    pub fn branches(self) -> Glob<Branch> {
-
        self.into()
-
    }
-

-
    fn qualify_heads(glob: PatternString) -> QualifiedPattern<'static> {
-
        qualify(&refname!("refs/heads"), glob).expect("BUG: pattern is qualified")
-
    }
-
}
-

-
impl FromIterator<PatternString> for Glob<Local> {
-
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
-
        let globs = iter
-
            .into_iter()
-
            .map(|pat| qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified"))
-
            .collect();
-

-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-
}
-

-
impl Extend<PatternString> for Glob<Local> {
-
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
-
        self.globs.extend(
-
            iter.into_iter().map(|pat| {
-
                qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified")
-
            }),
-
        )
-
    }
-
}
-

-
impl From<Glob<Local>> for Glob<Branch> {
-
    fn from(Glob { globs, .. }: Glob<Local>) -> Self {
-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-
}
-

-
impl Glob<Remote> {
-
    /// Creates the `Glob` that mathces all `refs/remotes`.
-
    pub fn all_remotes() -> Self {
-
        Self::remotes(refspec::pattern!("*"))
-
    }
-

-
    /// Creates a `Glob` for `refs/remotes`, starting with `glob`.
-
    pub fn remotes(glob: PatternString) -> Self {
-
        let globs = vec![Self::qualify_remotes(glob)];
-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-

-
    /// Adds a `refs/remotes` pattern to this `Glob`.
-
    pub fn insert(mut self, glob: PatternString) -> Self {
-
        self.globs.push(Self::qualify_remotes(glob));
-
        self
-
    }
-

-
    /// When chaining `Glob<Remote>` with `Glob<Local>`, use
-
    /// `branches` to convert this `Glob<Remote>` into a
-
    /// `Glob<Branch>`.
-
    ///
-
    /// # Example
-
    /// ```no_run
-
    /// Glob::remotes(pattern!("origin/features/*"))
-
    ///     .insert(pattern!("origin/qa/*"))
-
    ///     .branches()
-
    ///     .and(Glob::heads(pattern!("features/*")))
-
    /// ```
-
    pub fn branches(self) -> Glob<Branch> {
-
        self.into()
-
    }
-

-
    fn qualify_remotes(glob: PatternString) -> QualifiedPattern<'static> {
-
        qualify(&refname!("refs/remotes"), glob).expect("BUG: pattern is qualified")
-
    }
-
}
-

-
impl FromIterator<PatternString> for Glob<Remote> {
-
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
-
        let globs = iter
-
            .into_iter()
-
            .map(|pat| qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified"))
-
            .collect();
-

-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-
}
-

-
impl Extend<PatternString> for Glob<Remote> {
-
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
-
        self.globs.extend(
-
            iter.into_iter().map(|pat| {
-
                qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified")
-
            }),
-
        )
-
    }
-
}
-

-
impl From<Glob<Remote>> for Glob<Branch> {
-
    fn from(Glob { globs, .. }: Glob<Remote>) -> Self {
-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-
}
-

-
impl Glob<Qualified<'_>> {
-
    pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
-
        Self {
-
            globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
-
            glob_type: PhantomData,
-
        }
-
    }
-

-
    /// Creates a `Glob` for `refs/<category>`, starting with `glob`.
-
    pub fn categories<R>(category: R, glob: PatternString) -> Self
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        let globs = vec![Self::qualify_category(category, glob)];
-
        Self {
-
            globs,
-
            glob_type: PhantomData,
-
        }
-
    }
-

-
    /// Adds a `refs/<category>` pattern to this `Glob`.
-
    pub fn insert<R>(mut self, category: R, glob: PatternString) -> Self
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self.globs.push(Self::qualify_category(category, glob));
-
        self
-
    }
-

-
    fn qualify_category<R>(category: R, glob: PatternString) -> QualifiedPattern<'static>
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        let prefix = refname!("refs").and(category);
-
        qualify(&prefix, glob).expect("BUG: pattern is qualified")
-
    }
-
}
-

-
fn qualify(prefix: &RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
-
    prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
-
}
deleted radicle-surf/src/git/history.rs
@@ -1,111 +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 std::{
-
    convert::TryFrom,
-
    path::{Path, PathBuf},
-
};
-

-
use crate::git::{Commit, Error, Repository, ToCommit};
-

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

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

-
impl<'a> History<'a> {
-
    /// Creates a new history starting from `head`, in `repo`.
-
    pub fn new<C: ToCommit>(repo: &'a Repository, head: C) -> Result<Self, Error> {
-
        let head = head
-
            .to_commit(repo)
-
            .map_err(|err| Error::ToCommit(err.into()))?;
-
        let mut revwalk = repo.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<P>(mut self, path: &P) -> Self
-
    where
-
        P: AsRef<Path>,
-
    {
-
        self.filter_by = Some(FilterBy::File {
-
            path: path.as_ref().to_path_buf(),
-
        });
-
        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 commit = self.repo.find_commit(oid.into())?;
-

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

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

-
use std::{
-
    convert::TryFrom,
-
    fmt,
-
    str::{self, FromStr},
-
};
-

-
use git_ref_format::{
-
    refspec::{NamespacedPattern, PatternString, QualifiedPattern},
-
    Component,
-
    Namespaced,
-
    Qualified,
-
    RefStr,
-
    RefString,
-
};
-
use nonempty::NonEmpty;
-
pub use radicle_git_ext::Oid;
-
use thiserror::Error;
-

-
#[derive(Debug, Error)]
-
pub enum Error {
-
    /// When parsing a namespace we may come across one that was an empty
-
    /// string.
-
    #[error("namespaces must not be empty")]
-
    EmptyNamespace,
-
    #[error(transparent)]
-
    RefFormat(#[from] git_ref_format::Error),
-
    #[error(transparent)]
-
    Utf8(#[from] str::Utf8Error),
-
}
-

-
/// A `Namespace` value allows us to switch the git namespace of
-
/// a repo.
-
///
-
/// A `Namespace` is one or more name components separated by `/`, e.g. `surf`,
-
/// `surf/git`.
-
///
-
/// For each `Namespace`, the reference name will add a single `refs/namespaces`
-
/// prefix, e.g. `refs/namespaces/surf`,
-
/// `refs/namespaces/surf/refs/namespaces/git`.
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct Namespace {
-
    // XXX: we rely on RefString being non-empty here, which
-
    // git-ref-format ensures that there's no way to construct one.
-
    pub(super) namespaces: RefString,
-
}
-

-
impl Namespace {
-
    /// Take a `Qualified` reference name and convert it to a `Namespaced` using
-
    /// this `Namespace`.
-
    ///
-
    /// # Example
-
    ///
-
    /// ```no_run
-
    /// let ns = "surf/git".parse::<Namespace>();
-
    /// let name = ns.to_namespaced(qualified!("refs/heads/main"));
-
    /// assert_eq!(
-
    ///     name.as_str(),
-
    ///     "refs/namespaces/surf/refs/namespaces/git/refs/heads/main"
-
    /// );
-
    /// ```
-
    pub(crate) fn to_namespaced<'a>(&self, name: &Qualified<'a>) -> Namespaced<'a> {
-
        let mut components = self.namespaces.components().rev();
-
        let mut namespaced = name.with_namespace(
-
            components
-
                .next()
-
                .expect("BUG: 'namespaces' cannot be empty"),
-
        );
-
        for ns in components {
-
            let qualified = namespaced.into_qualified();
-
            namespaced = qualified.with_namespace(ns);
-
        }
-
        namespaced
-
    }
-

-
    /// Take a `QualifiedPattern` reference name and convert it to a
-
    /// `NamespacedPattern` using this `Namespace`.
-
    ///
-
    /// # Example
-
    ///
-
    /// ```no_run
-
    /// let ns = "surf/git".parse::<Namespace>();
-
    /// let name = ns.to_namespaced(pattern!("refs/heads/*").to_qualified().unwrap());
-
    /// assert_eq!(
-
    ///     name.as_str(),
-
    ///     "refs/namespaces/surf/refs/namespaces/git/refs/heads/*"
-
    /// );
-
    /// ```
-
    pub(crate) fn to_namespaced_pattern<'a>(
-
        &self,
-
        pat: &QualifiedPattern<'a>,
-
    ) -> NamespacedPattern<'a> {
-
        let pattern = PatternString::from(self.namespaces.clone());
-
        let mut components = pattern.components().rev();
-
        let mut namespaced = pat
-
            .with_namespace(
-
                components
-
                    .next()
-
                    .expect("BUG: 'namespaces' cannot be empty"),
-
            )
-
            .expect("BUG: 'namespace' cannot have globs");
-
        for ns in components {
-
            let qualified = namespaced.into_qualified();
-
            namespaced = qualified
-
                .with_namespace(ns)
-
                .expect("BUG: 'namespaces' cannot have globs");
-
        }
-
        namespaced
-
    }
-
}
-

-
impl fmt::Display for Namespace {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.namespaces)
-
    }
-
}
-

-
impl<'a> From<NonEmpty<Component<'a>>> for Namespace {
-
    fn from(cs: NonEmpty<Component<'a>>) -> Self {
-
        Self {
-
            namespaces: cs.into_iter().collect::<RefString>(),
-
        }
-
    }
-
}
-

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

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

-
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(Self::from_str)
-
    }
-
}
-

-
impl FromStr for Namespace {
-
    type Err = Error;
-

-
    fn from_str(name: &str) -> Result<Self, Self::Err> {
-
        let namespaces = RefStr::try_from_str(name)?.to_ref_string();
-
        Ok(Self { namespaces })
-
    }
-
}
-

-
impl From<Namespaced<'_>> for Namespace {
-
    fn from(namespaced: Namespaced<'_>) -> Self {
-
        let mut namespaces = namespaced.namespace().to_ref_string();
-
        let mut qualified = namespaced.strip_namespace();
-
        while let Some(namespaced) = qualified.to_namespaced() {
-
            namespaces.push(namespaced.namespace());
-
            qualified = namespaced.strip_namespace();
-
        }
-
        Self { namespaces }
-
    }
-
}
-

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

-
    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
-
        let name = RefStr::try_from_str(str::from_utf8(reference.name_bytes())?)?;
-
        name.to_namespaced()
-
            .ok_or(Error::EmptyNamespace)
-
            .map(Self::from)
-
    }
-
}
deleted radicle-surf/src/git/repo.rs
@@ -1,574 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
use std::{
-
    collections::BTreeSet,
-
    convert::TryFrom,
-
    path::{Path, PathBuf},
-
    str,
-
};
-

-
use git_ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString};
-
use radicle_git_ext::Oid;
-
use thiserror::Error;
-

-
use crate::{
-
    diff::{self, *},
-
    fs::{self, Directory, File, FileContent},
-
    git::{
-
        commit,
-
        glob,
-
        namespace,
-
        Branch,
-
        Commit,
-
        Glob,
-
        History,
-
        Namespace,
-
        Revision,
-
        Signature,
-
        Stats,
-
        Tag,
-
        ToCommit,
-
    },
-
    object::{commit::Header, Blob, Tree, TreeEntry},
-
};
-

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

-
use self::iter::{BranchNames, TagNames};
-

-
/// Enumeration of errors that can occur in operations from [`crate::git`].
-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum Error {
-
    #[error(transparent)]
-
    Branches(#[from] iter::error::Branch),
-
    #[error(transparent)]
-
    Categories(#[from] iter::error::Category),
-
    #[error(transparent)]
-
    Commit(#[from] commit::Error),
-
    /// 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)]
-
    Directory(#[from] fs::error::Directory),
-
    #[error(transparent)]
-
    File(#[from] fs::error::File),
-
    #[error(transparent)]
-
    Git(#[from] git2::Error),
-
    #[error(transparent)]
-
    Glob(#[from] glob::Error),
-
    #[error(transparent)]
-
    Namespace(#[from] namespace::Error),
-
    #[error("the reference '{0}' should be of the form 'refs/<category>/<path>'")]
-
    NotQualified(String),
-
    /// The requested file was not found.
-
    #[error("path not found for: {0}")]
-
    PathNotFound(PathBuf),
-
    #[error(transparent)]
-
    RefFormat(#[from] git_ref_format::Error),
-
    #[error(transparent)]
-
    Revision(Box<dyn std::error::Error + Send + Sync + 'static>),
-
    /// A `revspec` was provided that could not be parsed into a branch, tag, or
-
    /// commit object.
-
    #[error("provided revspec '{rev}' could not be parsed into a git object")]
-
    RevParseFailure {
-
        /// The provided revspec that failed to parse.
-
        rev: String,
-
    },
-
    #[error(transparent)]
-
    ToCommit(Box<dyn std::error::Error + Send + Sync + 'static>),
-
    #[error(transparent)]
-
    Tags(#[from] iter::error::Tag),
-
}
-

-
/// 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 {
-
    inner: git2::Repository,
-
}
-

-
////////////////////////////////////////////
-
// Public API, ONLY add `pub fn` in here. //
-
////////////////////////////////////////////
-
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> {
-
        let repo = git2::Repository::open(repo_uri)?;
-
        Ok(Self { inner: repo })
-
    }
-

-
    /// 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> {
-
        let repo = git2::Repository::discover(repo_uri)?;
-
        Ok(Self { inner: repo })
-
    }
-

-
    /// What is the current namespace we're browsing in.
-
    pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
-
        self.inner
-
            .namespace_bytes()
-
            .map(|ns| Namespace::try_from(ns).map_err(Error::from))
-
            .transpose()
-
    }
-

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

-
    pub fn with_namespace<T, F>(&self, namespace: &RefString, f: F) -> Result<T, Error>
-
    where
-
        F: FnOnce() -> Result<T, Error>,
-
    {
-
        self.switch_namespace(namespace)?;
-
        let res = f();
-
        self.inner.remove_namespace()?;
-
        res
-
    }
-

-
    /// Returns an iterator of branches that match `pattern`.
-
    pub fn branches<G>(&self, pattern: G) -> Result<Branches, Error>
-
    where
-
        G: Into<Glob<Branch>>,
-
    {
-
        let pattern = pattern.into();
-
        let mut branches = Branches::default();
-
        for glob in pattern.globs() {
-
            let namespaced = self.namespaced_pattern(glob)?;
-
            let references = self.inner.references_glob(&namespaced)?;
-
            branches.push(references);
-
        }
-
        Ok(branches)
-
    }
-

-
    /// Lists branch names with `filter`.
-
    pub fn branch_names<G>(&self, filter: G) -> Result<BranchNames, Error>
-
    where
-
        G: Into<Glob<Branch>>,
-
    {
-
        Ok(self.branches(filter)?.names())
-
    }
-

-
    /// Returns an iterator of tags that match `pattern`.
-
    pub fn tags(&self, pattern: &Glob<Tag>) -> Result<Tags, Error> {
-
        let mut tags = Tags::default();
-
        for glob in pattern.globs() {
-
            let namespaced = self.namespaced_pattern(glob)?;
-
            let references = self.inner.references_glob(&namespaced)?;
-
            tags.push(references);
-
        }
-
        Ok(tags)
-
    }
-

-
    /// Lists tag names in the local RefScope.
-
    pub fn tag_names(&self, filter: &Glob<Tag>) -> Result<TagNames, Error> {
-
        Ok(self.tags(filter)?.names())
-
    }
-

-
    pub fn categories(&self, pattern: &Glob<Qualified<'_>>) -> Result<Categories, Error> {
-
        let mut cats = Categories::default();
-
        for glob in pattern.globs() {
-
            let namespaced = self.namespaced_pattern(glob)?;
-
            let references = self.inner.references_glob(&namespaced)?;
-
            cats.push(references);
-
        }
-
        Ok(cats)
-
    }
-

-
    /// 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() {
-
            let new_set = self
-
                .inner
-
                .references_glob(glob)?
-
                .map(|reference| {
-
                    reference
-
                        .map_err(Error::Git)
-
                        .and_then(|r| Namespace::try_from(&r).map_err(Error::from))
-
                })
-
                .collect::<Result<BTreeSet<Namespace>, Error>>()?;
-
            set.extend(new_set);
-
        }
-
        Ok(Namespaces::new(set))
-
    }
-

-
    /// Get the [`Diff`] between two commits.
-
    pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
-
        let from_commit = self.find_commit(self.object_id(&from)?)?;
-
        let to_commit = self.find_commit(self.object_id(&to)?)?;
-
        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`.
-
    ///
-
    /// If the `commit` has a parent, then it the diff will be a
-
    /// comparison between itself and that parent. Otherwise, the left
-
    /// hand side of the diff will pass nothing.
-
    pub fn diff_commit(&self, commit: impl ToCommit) -> Result<Diff, Error> {
-
        let commit = commit
-
            .to_commit(self)
-
            .map_err(|err| Error::ToCommit(err.into()))?;
-
        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.inner.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)
-
            .map_err(|err| Error::ToCommit(err.into()))?;
-
        let git2_commit = self.inner.find_commit((commit.id).into())?;
-
        let tree = git2_commit.as_object().peel_to_tree()?;
-
        Ok(Directory::root(tree.id().into(), commit))
-
    }
-

-
    /// Returns a [`Directory`] for `path` in `commit`.
-
    pub fn directory<C: ToCommit, P: AsRef<Path>>(
-
        &self,
-
        commit: C,
-
        path: &P,
-
    ) -> Result<Directory, Error> {
-
        let root = self.root_dir(commit)?;
-
        root.find_directory(path, self)?
-
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
-
    }
-

-
    /// Returns a [`File`] for `path` in `commit`.
-
    pub fn file<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<File, Error> {
-
        let root = self.root_dir(commit)?;
-
        root.find_file(path, self)?
-
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
-
    }
-

-
    /// Returns a [`Tree`] for `path` in `commit`.
-
    pub fn tree<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Tree, Error> {
-
        let commit = commit
-
            .to_commit(self)
-
            .map_err(|e| Error::ToCommit(e.into()))?;
-
        let dir = self.directory(commit.id, path)?;
-
        let mut entries = dir
-
            .entries(self)?
-
            .map(|en| {
-
                let name = en.name().to_string();
-
                let path = en.path();
-
                let commit = self
-
                    .last_commit(&path, commit.id)?
-
                    .ok_or(Error::PathNotFound(path))?;
-
                let commit_header = Header::from(commit);
-
                Ok(TreeEntry::new(name, en.into(), commit_header))
-
            })
-
            .collect::<Result<Vec<TreeEntry>, Error>>()?;
-
        entries.sort();
-

-
        let last_commit = self
-
            .last_commit(path, commit)?
-
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
-
        let header = Header::from(last_commit);
-
        Ok(Tree::new(dir.id(), entries, header))
-
    }
-

-
    /// Returns a [`Blob`] for `path` in `commit`.
-
    pub fn blob<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Blob, Error> {
-
        let commit = commit
-
            .to_commit(self)
-
            .map_err(|e| Error::ToCommit(e.into()))?;
-
        let file = self.file(commit.id, path)?;
-
        let last_commit = self
-
            .last_commit(path, commit)?
-
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
-
        let header = Header::from(last_commit);
-

-
        let content = file.content(self)?;
-
        Ok(Blob::new(file.id(), content.as_bytes(), header))
-
    }
-

-
    /// Returns the last commit, if exists, for a `path` in the history of
-
    /// `rev`.
-
    pub fn last_commit<P, C>(&self, path: &P, rev: C) -> Result<Option<Commit>, Error>
-
    where
-
        P: AsRef<Path>,
-
        C: ToCommit,
-
    {
-
        let history = self.history(rev)?;
-
        history.by_path(path).next().transpose()
-
    }
-

-
    /// Returns a commit for `rev`, if it exists.
-
    pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
-
        rev.to_commit(self)
-
    }
-

-
    /// Gets the [`Stats`] of this repository starting from the
-
    /// `HEAD` (see [`Repository::head`]) of the repository.
-
    pub fn stats(&self) -> Result<Stats, Error> {
-
        self.stats_from(&self.head()?)
-
    }
-

-
    /// Gets the [`Stats`] of this repository starting from the given
-
    /// `rev`.
-
    pub fn stats_from<R>(&self, rev: &R) -> Result<Stats, Error>
-
    where
-
        R: Revision,
-
    {
-
        let branches = self.branches(Glob::all_heads())?.count();
-
        let mut history = self.history(rev)?;
-
        let (commits, contributors) = history.try_fold(
-
            (0, BTreeSet::new()),
-
            |(commits, mut contributors), commit| {
-
                let commit = commit?;
-
                contributors.insert((commit.author.name, commit.author.email));
-
                Ok::<_, Error>((commits + 1, contributors))
-
            },
-
        )?;
-
        Ok(Stats {
-
            branches,
-
            commits,
-
            contributors: contributors.len(),
-
        })
-
    }
-

-
    // TODO(finto): I think this can be removed in favour of using
-
    // `source::Blob::new`
-
    /// Retrieves the file with `path` in this commit.
-
    pub fn get_commit_file<P, R>(&self, rev: &R, path: &P) -> Result<FileContent, crate::git::Error>
-
    where
-
        P: AsRef<Path>,
-
        R: Revision,
-
    {
-
        let path = path.as_ref();
-
        let id = self.object_id(rev)?;
-
        let commit = self.find_commit(id)?;
-
        let tree = commit.tree()?;
-
        let entry = tree.get_path(path)?;
-
        let object = entry.to_object(&self.inner)?;
-
        let blob = object
-
            .into_blob()
-
            .map_err(|_| Error::PathNotFound(path.to_path_buf()))?;
-
        Ok(FileContent::new(blob))
-
    }
-

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

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

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

-
////////////////////////////////////////////////////////////
-
// Private API, ONLY add `pub(crate) fn` or `fn` in here. //
-
////////////////////////////////////////////////////////////
-
impl Repository {
-
    /// Lists branches that are reachable from `oid`.
-
    pub(crate) 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.inner.find_reference(namespaced.as_str())?;
-
            if self.reachable_from(&reference, oid)? {
-
                contained_branches.push(branch);
-
            }
-
        }
-

-
        Ok(contained_branches)
-
    }
-

-
    pub(crate) fn find_blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git2::Error> {
-
        self.inner.find_blob(oid.into())
-
    }
-

-
    pub(crate) fn find_commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git2::Error> {
-
        self.inner.find_commit(oid.into())
-
    }
-

-
    pub(crate) fn find_tree(&self, oid: Oid) -> Result<git2::Tree<'_>, git2::Error> {
-
        self.inner.find_tree(oid.into())
-
    }
-

-
    pub(crate) fn refname_to_id<R>(&self, name: &R) -> Result<Oid, git2::Error>
-
    where
-
        R: AsRef<RefStr>,
-
    {
-
        self.inner
-
            .refname_to_id(name.as_ref().as_str())
-
            .map(Oid::from)
-
    }
-

-
    pub(crate) fn revwalk(&self) -> Result<git2::Revwalk<'_>, git2::Error> {
-
        self.inner.revwalk()
-
    }
-

-
    pub(super) fn object_id<R: Revision>(&self, r: &R) -> Result<Oid, Error> {
-
        r.object_id(self).map_err(|err| Error::Revision(err.into()))
-
    }
-

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

-
    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.inner.graph_descendant_of(other, git2_oid)?;
-

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

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

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

-
    fn diff_commits(
-
        &self,
-
        path: Option<&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.to_string_lossy().to_string());
-
            // We're skipping the binary pass because we won't be inspecting deltas.
-
            opts.skip_binary_check(true);
-
        }
-

-
        let mut diff =
-
            self.inner
-
                .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 a full reference name with namespace(s) included.
-
    pub(crate) fn namespaced_refname<'a>(
-
        &'a self,
-
        refname: &Qualified<'a>,
-
    ) -> Result<Qualified<'a>, Error> {
-
        let fullname = match self.which_namespace()? {
-
            Some(namespace) => namespace.to_namespaced(refname).into_qualified(),
-
            None => refname.clone(),
-
        };
-
        Ok(fullname)
-
    }
-

-
    /// Returns a full reference name with namespace(s) included.
-
    fn namespaced_pattern<'a>(
-
        &'a self,
-
        refname: &QualifiedPattern<'a>,
-
    ) -> Result<QualifiedPattern<'a>, Error> {
-
        let fullname = match self.which_namespace()? {
-
            Some(namespace) => namespace.to_namespaced_pattern(refname).into_qualified(),
-
            None => refname.clone(),
-
        };
-
        Ok(fullname)
-
    }
-
}
-

-
impl From<git2::Repository> for Repository {
-
    fn from(repo: git2::Repository) -> Self {
-
        Repository { inner: 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/git/repo/iter.rs
@@ -1,242 +0,0 @@
-
// I think the following `Tags` and `Branches` would be merged
-
// using Generic associated types supported in Rust 1.65.0.
-

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

-
use git_ref_format::{lit, Qualified, RefString};
-

-
use crate::git::{refstr_join, tag, Branch, Namespace, Tag};
-

-
/// Iterator over [`Tag`]s.
-
#[derive(Default)]
-
pub struct Tags<'a> {
-
    references: Vec<git2::References<'a>>,
-
    current: usize,
-
}
-

-
/// Iterator over the [`Qualified`] names of [`Tag`]s.
-
pub struct TagNames<'a> {
-
    inner: Tags<'a>,
-
}
-

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

-
    pub fn names(self) -> TagNames<'a> {
-
        TagNames { inner: self }
-
    }
-
}
-

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

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

-
impl<'a> Iterator for TagNames<'a> {
-
    type Item = Result<Qualified<'static>, error::Tag>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        while self.inner.current < self.inner.references.len() {
-
            match self.inner.references.get_mut(self.inner.current) {
-
                Some(refs) => match refs.next() {
-
                    Some(res) => {
-
                        return Some(res.map_err(error::Tag::from).and_then(|r| {
-
                            tag::reference_name(&r)
-
                                .map(|name| lit::refs_tags(name).into())
-
                                .map_err(error::Tag::from)
-
                        }))
-
                    },
-
                    None => self.inner.current += 1,
-
                },
-
                None => break,
-
            }
-
        }
-
        None
-
    }
-
}
-

-
/// Iterator over [`Branch`]es.
-
#[derive(Default)]
-
pub struct Branches<'a> {
-
    references: Vec<git2::References<'a>>,
-
    current: usize,
-
}
-

-
/// Iterator over the [`Qualified`] names of [`Branch`]es.
-
pub struct BranchNames<'a> {
-
    inner: Branches<'a>,
-
}
-

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

-
    pub fn names(self) -> BranchNames<'a> {
-
        BranchNames { inner: self }
-
    }
-
}
-

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

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

-
impl<'a> Iterator for BranchNames<'a> {
-
    type Item = Result<Qualified<'static>, error::Branch>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        while self.inner.current < self.inner.references.len() {
-
            match self.inner.references.get_mut(self.inner.current) {
-
                Some(refs) => match refs.next() {
-
                    Some(res) => {
-
                        return Some(res.map_err(error::Branch::from).and_then(|r| {
-
                            Branch::try_from(&r)
-
                                .map(|branch| branch.refname().into_owned())
-
                                .map_err(error::Branch::from)
-
                        }))
-
                    },
-
                    None => self.inner.current += 1,
-
                },
-
                None => break,
-
            }
-
        }
-
        None
-
    }
-
}
-

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

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

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

-
#[derive(Default)]
-
pub struct Categories<'a> {
-
    references: Vec<git2::References<'a>>,
-
    current: usize,
-
}
-

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

-
impl<'a> Iterator for Categories<'a> {
-
    type Item = Result<(RefString, RefString), error::Category>;
-

-
    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::Category::from).and_then(|r| {
-
                            let name = std::str::from_utf8(r.name_bytes())?;
-
                            let name = git_ref_format::RefStr::try_from_str(name)?;
-
                            let name = name.qualified().ok_or_else(|| {
-
                                error::Category::NotQualified(name.to_ref_string())
-
                            })?;
-
                            let (_refs, category, c, cs) = name.non_empty_components();
-
                            Ok((category.to_ref_string(), refstr_join(c, cs)))
-
                        }));
-
                    },
-
                    None => self.current += 1,
-
                },
-
                None => break,
-
            }
-
        }
-
        None
-
    }
-
}
-

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

-
    use git_ref_format::RefString;
-
    use thiserror::Error;
-

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

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

-
    #[derive(Debug, Error)]
-
    pub enum Category {
-
        #[error(transparent)]
-
        Git(#[from] git2::Error),
-
        #[error("the reference '{0}' was expected to be qualified, i.e. 'refs/<category>/<path>'")]
-
        NotQualified(RefString),
-
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
-
        #[error(transparent)]
-
        Utf8(#[from] str::Utf8Error),
-
    }
-

-
    #[derive(Debug, Error)]
-
    pub enum Tag {
-
        #[error(transparent)]
-
        Git(#[from] git2::Error),
-
        #[error(transparent)]
-
        Tag(#[from] tag::error::FromReference),
-
    }
-
}
deleted radicle-surf/src/git/stats.rs
@@ -1,33 +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 = "serde")]
-
use serde::Serialize;
-

-
/// Stats for a repository
-
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-
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/git/tag.rs
@@ -1,156 +0,0 @@
-
use std::{convert::TryFrom, str};
-

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

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

-
/// The static information of a [`git2::Tag`].
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub enum Tag {
-
    /// A light-weight git tag.
-
    Light {
-
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
-
        id: Oid,
-
        /// The reference name for this `Tag`.
-
        name: RefString,
-
    },
-
    /// An annotated git tag.
-
    Annotated {
-
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
-
        id: Oid,
-
        /// The Object ID for the object that is tagged.
-
        target: Oid,
-
        /// The reference name for this `Tag`.
-
        name: RefString,
-
        /// The named author of this `Tag`, if the `Tag` was annotated.
-
        tagger: Option<Author>,
-
        /// The message with this `Tag`, if the `Tag` was annotated.
-
        message: Option<String>,
-
    },
-
}
-

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

-
    /// Return the short `Tag` refname,
-
    /// e.g. `release/v1`.
-
    pub fn short_name(&self) -> &RefString {
-
        match &self {
-
            Tag::Light { name, .. } => name,
-
            Tag::Annotated { name, .. } => name,
-
        }
-
    }
-

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

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

-
    use git_ref_format::RefString;
-
    use thiserror::Error;
-

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

-
    #[derive(Debug, Error)]
-
    pub enum FromReference {
-
        #[error(transparent)]
-
        FromTag(#[from] FromTag),
-
        #[error(transparent)]
-
        Git(#[from] git2::Error),
-
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
-
        NotQualified(String),
-
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
-
        NotTag(RefString),
-
        #[error(transparent)]
-
        RefFormat(#[from] git_ref_format::Error),
-
        #[error(transparent)]
-
        Utf8(#[from] str::Utf8Error),
-
    }
-
}
-

-
impl TryFrom<&git2::Tag<'_>> for Tag {
-
    type Error = error::FromTag;
-

-
    fn try_from(tag: &git2::Tag) -> Result<Self, Self::Error> {
-
        let id = tag.id().into();
-
        let target = tag.target_id().into();
-
        let name = {
-
            let name = str::from_utf8(tag.name_bytes())?;
-
            RefStr::try_from_str(name)?.to_ref_string()
-
        };
-
        let tagger = tag.tagger().map(Author::try_from).transpose()?;
-
        let message = tag
-
            .message_bytes()
-
            .map(str::from_utf8)
-
            .transpose()?
-
            .map(|message| message.into());
-

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

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

-
    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
-
        let name = reference_name(reference)?;
-
        match reference.peel_to_tag() {
-
            Ok(tag) => Tag::try_from(&tag).map_err(error::FromReference::from),
-
            // If we get an error peeling to a tag _BUT_ we also have confirmed the
-
            // reference is a tag, that means we have a lightweight tag,
-
            // i.e. a commit SHA and name.
-
            Err(err)
-
                if err.class() == git2::ErrorClass::Object
-
                    && err.code() == git2::ErrorCode::InvalidSpec =>
-
            {
-
                let commit = reference.peel_to_commit()?;
-
                Ok(Tag::Light {
-
                    id: commit.id().into(),
-
                    name,
-
                })
-
            },
-
            Err(err) => Err(err.into()),
-
        }
-
    }
-
}
-

-
pub(crate) fn reference_name(
-
    reference: &git2::Reference,
-
) -> Result<RefString, error::FromReference> {
-
    let name = str::from_utf8(reference.name_bytes())?;
-
    let name = RefStr::try_from_str(name)?
-
        .qualified()
-
        .ok_or_else(|| error::FromReference::NotQualified(name.to_string()))?;
-

-
    let (_refs, tags, c, cs) = name.non_empty_components();
-

-
    if tags == component::TAGS {
-
        Ok(refstr_join(c, cs))
-
    } else {
-
        Err(error::FromReference::NotTag(name.into()))
-
    }
-
}
added radicle-surf/src/glob.rs
@@ -0,0 +1,357 @@
+
// 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 std::marker::PhantomData;
+

+
use git_ref_format::{
+
    refname,
+
    refspec::{self, PatternString, QualifiedPattern},
+
    Qualified,
+
    RefStr,
+
    RefString,
+
};
+
use thiserror::Error;
+

+
use crate::{Branch, Local, Namespace, Remote, Tag};
+

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

+
/// A collection of globs for T (a git reference type).
+
#[derive(Clone, Debug)]
+
pub struct Glob<T> {
+
    globs: Vec<QualifiedPattern<'static>>,
+
    glob_type: PhantomData<T>, // To support different methods for different T.
+
}
+

+
impl<T> Default for Glob<T> {
+
    fn default() -> Self {
+
        Self {
+
            globs: Default::default(),
+
            glob_type: PhantomData,
+
        }
+
    }
+
}
+

+
impl<T> Glob<T> {
+
    /// Return the [`QualifiedPattern`] globs of this `Glob`.
+
    pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
+
        self.globs.iter()
+
    }
+

+
    /// Combine two `Glob`s together by combining their glob lists together.
+
    ///
+
    /// Note that the `Glob`s must result in the same type,
+
    /// e.g. `Glob<Tag>` can only combine with `Glob<Tag>`,
+
    /// `Glob<Local>` can combine with `Glob<Remote>`, etc.
+
    pub fn and(mut self, other: impl Into<Self>) -> Self {
+
        self.globs.extend(other.into().globs);
+
        self
+
    }
+
}
+

+
impl Glob<Namespace> {
+
    /// Creates the `Glob` that mathces all `refs/namespaces`.
+
    pub fn all_namespaces() -> Self {
+
        Self::namespaces(refspec::pattern!("*"))
+
    }
+

+
    /// Creates a `Glob` for `refs/namespaces`, starting with `glob`.
+
    pub fn namespaces(glob: PatternString) -> Self {
+
        let globs = vec![Self::qualify(glob)];
+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+

+
    /// Adds a `refs/namespaces` pattern to this `Glob`.
+
    pub fn insert(mut self, glob: PatternString) -> Self {
+
        self.globs.push(Self::qualify(glob));
+
        self
+
    }
+

+
    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
+
        qualify(&refname!("refs/namespaces"), glob).expect("BUG: pattern is qualified")
+
    }
+
}
+

+
impl FromIterator<PatternString> for Glob<Namespace> {
+
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+
        let globs = iter
+
            .into_iter()
+
            .map(|pat| {
+
                qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
+
            })
+
            .collect();
+

+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+
}
+

+
impl Extend<PatternString> for Glob<Namespace> {
+
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+
        self.globs.extend(iter.into_iter().map(|pat| {
+
            qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
+
        }))
+
    }
+
}
+

+
impl Glob<Tag> {
+
    /// Creates a `Glob` that matches all `refs/tags`.
+
    pub fn all_tags() -> Self {
+
        Self::tags(refspec::pattern!("*"))
+
    }
+

+
    /// Creates a `Glob` for `refs/tags`, starting with `glob`.
+
    pub fn tags(glob: PatternString) -> Self {
+
        let globs = vec![Self::qualify(glob)];
+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+

+
    /// Adds a `refs/tags` pattern to this `Glob`.
+
    pub fn insert(mut self, glob: PatternString) -> Self {
+
        self.globs.push(Self::qualify(glob));
+
        self
+
    }
+

+
    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
+
        qualify(&refname!("refs/tags"), glob).expect("BUG: pattern is qualified")
+
    }
+
}
+

+
impl FromIterator<PatternString> for Glob<Tag> {
+
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+
        let globs = iter
+
            .into_iter()
+
            .map(|pat| qualify(&refname!("refs/tags"), pat).expect("BUG: pattern is qualified"))
+
            .collect();
+

+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+
}
+

+
impl Extend<PatternString> for Glob<Tag> {
+
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+
        self.globs.extend(
+
            iter.into_iter()
+
                .map(|pat| qualify(&refname!("refs/tag"), pat).expect("BUG: pattern is qualified")),
+
        )
+
    }
+
}
+

+
impl Glob<Local> {
+
    /// Creates the `Glob` that mathces all `refs/heads`.
+
    pub fn all_heads() -> Self {
+
        Self::heads(refspec::pattern!("*"))
+
    }
+

+
    /// Creates a `Glob` for `refs/heads`, starting with `glob`.
+
    pub fn heads(glob: PatternString) -> Self {
+
        let globs = vec![Self::qualify_heads(glob)];
+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+

+
    /// Adds a `refs/heads` pattern to this `Glob`.
+
    pub fn insert(mut self, glob: PatternString) -> Self {
+
        self.globs.push(Self::qualify_heads(glob));
+
        self
+
    }
+

+
    /// When chaining `Glob<Local>` with `Glob<Remote>`, use
+
    /// `branches` to convert this `Glob<Local>` into a
+
    /// `Glob<Branch>`.
+
    ///
+
    /// # Example
+
    /// ```no_run
+
    /// Glob::heads(pattern!("features/*"))
+
    ///     .insert(pattern!("qa/*"))
+
    ///     .branches()
+
    ///     .and(Glob::remotes(pattern!("origin/features/*")))
+
    /// ```
+
    pub fn branches(self) -> Glob<Branch> {
+
        self.into()
+
    }
+

+
    fn qualify_heads(glob: PatternString) -> QualifiedPattern<'static> {
+
        qualify(&refname!("refs/heads"), glob).expect("BUG: pattern is qualified")
+
    }
+
}
+

+
impl FromIterator<PatternString> for Glob<Local> {
+
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+
        let globs = iter
+
            .into_iter()
+
            .map(|pat| qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified"))
+
            .collect();
+

+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+
}
+

+
impl Extend<PatternString> for Glob<Local> {
+
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+
        self.globs.extend(
+
            iter.into_iter().map(|pat| {
+
                qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified")
+
            }),
+
        )
+
    }
+
}
+

+
impl From<Glob<Local>> for Glob<Branch> {
+
    fn from(Glob { globs, .. }: Glob<Local>) -> Self {
+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+
}
+

+
impl Glob<Remote> {
+
    /// Creates the `Glob` that mathces all `refs/remotes`.
+
    pub fn all_remotes() -> Self {
+
        Self::remotes(refspec::pattern!("*"))
+
    }
+

+
    /// Creates a `Glob` for `refs/remotes`, starting with `glob`.
+
    pub fn remotes(glob: PatternString) -> Self {
+
        let globs = vec![Self::qualify_remotes(glob)];
+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+

+
    /// Adds a `refs/remotes` pattern to this `Glob`.
+
    pub fn insert(mut self, glob: PatternString) -> Self {
+
        self.globs.push(Self::qualify_remotes(glob));
+
        self
+
    }
+

+
    /// When chaining `Glob<Remote>` with `Glob<Local>`, use
+
    /// `branches` to convert this `Glob<Remote>` into a
+
    /// `Glob<Branch>`.
+
    ///
+
    /// # Example
+
    /// ```no_run
+
    /// Glob::remotes(pattern!("origin/features/*"))
+
    ///     .insert(pattern!("origin/qa/*"))
+
    ///     .branches()
+
    ///     .and(Glob::heads(pattern!("features/*")))
+
    /// ```
+
    pub fn branches(self) -> Glob<Branch> {
+
        self.into()
+
    }
+

+
    fn qualify_remotes(glob: PatternString) -> QualifiedPattern<'static> {
+
        qualify(&refname!("refs/remotes"), glob).expect("BUG: pattern is qualified")
+
    }
+
}
+

+
impl FromIterator<PatternString> for Glob<Remote> {
+
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+
        let globs = iter
+
            .into_iter()
+
            .map(|pat| qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified"))
+
            .collect();
+

+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+
}
+

+
impl Extend<PatternString> for Glob<Remote> {
+
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+
        self.globs.extend(
+
            iter.into_iter().map(|pat| {
+
                qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified")
+
            }),
+
        )
+
    }
+
}
+

+
impl From<Glob<Remote>> for Glob<Branch> {
+
    fn from(Glob { globs, .. }: Glob<Remote>) -> Self {
+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+
}
+

+
impl Glob<Qualified<'_>> {
+
    pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
+
        Self {
+
            globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
+
            glob_type: PhantomData,
+
        }
+
    }
+

+
    /// Creates a `Glob` for `refs/<category>`, starting with `glob`.
+
    pub fn categories<R>(category: R, glob: PatternString) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        let globs = vec![Self::qualify_category(category, glob)];
+
        Self {
+
            globs,
+
            glob_type: PhantomData,
+
        }
+
    }
+

+
    /// Adds a `refs/<category>` pattern to this `Glob`.
+
    pub fn insert<R>(mut self, category: R, glob: PatternString) -> Self
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self.globs.push(Self::qualify_category(category, glob));
+
        self
+
    }
+

+
    fn qualify_category<R>(category: R, glob: PatternString) -> QualifiedPattern<'static>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        let prefix = refname!("refs").and(category);
+
        qualify(&prefix, glob).expect("BUG: pattern is qualified")
+
    }
+
}
+

+
fn qualify(prefix: &RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
+
    prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
+
}
added radicle-surf/src/history.rs
@@ -0,0 +1,111 @@
+
// 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 std::{
+
    convert::TryFrom,
+
    path::{Path, PathBuf},
+
};
+

+
use crate::{Commit, Error, Repository, ToCommit};
+

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

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

+
impl<'a> History<'a> {
+
    /// Creates a new history starting from `head`, in `repo`.
+
    pub fn new<C: ToCommit>(repo: &'a Repository, head: C) -> Result<Self, Error> {
+
        let head = head
+
            .to_commit(repo)
+
            .map_err(|err| Error::ToCommit(err.into()))?;
+
        let mut revwalk = repo.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<P>(mut self, path: &P) -> Self
+
    where
+
        P: AsRef<Path>,
+
    {
+
        self.filter_by = Some(FilterBy::File {
+
            path: path.as_ref().to_path_buf(),
+
        });
+
        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 commit = self.repo.find_commit(oid.into())?;
+

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

+
                    let commit = Commit::try_from(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)
+
    }
+
}
modified radicle-surf/src/lib.rs
@@ -20,7 +20,7 @@
//! of files and directories for any given revision. It also allows the user to
//! diff any two different revisions.
//!
-
//! The main entry point of the API is [git::Repository].
+
//! The main entry point of the API is [Repository].
//!
//! Let's start surfing!

@@ -30,5 +30,37 @@ extern crate radicle_git_ext as git_ext;

pub mod diff;
pub mod fs;
-
pub mod git;
pub mod object;
+

+
// 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::{Error, Repository};
+

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

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

+
mod branch;
+
pub use branch::{Branch, Local, Remote};
+

+
mod tag;
+
pub use tag::Tag;
+

+
mod commit;
+
pub use commit::{Author, Commit};
+

+
mod namespace;
+
pub use namespace::Namespace;
+

+
mod stats;
+
pub use stats::Stats;
+

+
mod revision;
+
pub use revision::{Revision, Signature, ToCommit};
+

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

+
use std::{
+
    convert::TryFrom,
+
    fmt,
+
    str::{self, FromStr},
+
};
+

+
use git_ref_format::{
+
    refspec::{NamespacedPattern, PatternString, QualifiedPattern},
+
    Component,
+
    Namespaced,
+
    Qualified,
+
    RefStr,
+
    RefString,
+
};
+
use nonempty::NonEmpty;
+
pub use radicle_git_ext::Oid;
+
use thiserror::Error;
+

+
#[derive(Debug, Error)]
+
pub enum Error {
+
    /// When parsing a namespace we may come across one that was an empty
+
    /// string.
+
    #[error("namespaces must not be empty")]
+
    EmptyNamespace,
+
    #[error(transparent)]
+
    RefFormat(#[from] git_ref_format::Error),
+
    #[error(transparent)]
+
    Utf8(#[from] str::Utf8Error),
+
}
+

+
/// A `Namespace` value allows us to switch the git namespace of
+
/// a repo.
+
///
+
/// A `Namespace` is one or more name components separated by `/`, e.g. `surf`,
+
/// `surf/git`.
+
///
+
/// For each `Namespace`, the reference name will add a single `refs/namespaces`
+
/// prefix, e.g. `refs/namespaces/surf`,
+
/// `refs/namespaces/surf/refs/namespaces/git`.
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Namespace {
+
    // XXX: we rely on RefString being non-empty here, which
+
    // git-ref-format ensures that there's no way to construct one.
+
    pub(super) namespaces: RefString,
+
}
+

+
impl Namespace {
+
    /// Take a `Qualified` reference name and convert it to a `Namespaced` using
+
    /// this `Namespace`.
+
    ///
+
    /// # Example
+
    ///
+
    /// ```no_run
+
    /// let ns = "surf/git".parse::<Namespace>();
+
    /// let name = ns.to_namespaced(qualified!("refs/heads/main"));
+
    /// assert_eq!(
+
    ///     name.as_str(),
+
    ///     "refs/namespaces/surf/refs/namespaces/git/refs/heads/main"
+
    /// );
+
    /// ```
+
    pub(crate) fn to_namespaced<'a>(&self, name: &Qualified<'a>) -> Namespaced<'a> {
+
        let mut components = self.namespaces.components().rev();
+
        let mut namespaced = name.with_namespace(
+
            components
+
                .next()
+
                .expect("BUG: 'namespaces' cannot be empty"),
+
        );
+
        for ns in components {
+
            let qualified = namespaced.into_qualified();
+
            namespaced = qualified.with_namespace(ns);
+
        }
+
        namespaced
+
    }
+

+
    /// Take a `QualifiedPattern` reference name and convert it to a
+
    /// `NamespacedPattern` using this `Namespace`.
+
    ///
+
    /// # Example
+
    ///
+
    /// ```no_run
+
    /// let ns = "surf/git".parse::<Namespace>();
+
    /// let name = ns.to_namespaced(pattern!("refs/heads/*").to_qualified().unwrap());
+
    /// assert_eq!(
+
    ///     name.as_str(),
+
    ///     "refs/namespaces/surf/refs/namespaces/git/refs/heads/*"
+
    /// );
+
    /// ```
+
    pub(crate) fn to_namespaced_pattern<'a>(
+
        &self,
+
        pat: &QualifiedPattern<'a>,
+
    ) -> NamespacedPattern<'a> {
+
        let pattern = PatternString::from(self.namespaces.clone());
+
        let mut components = pattern.components().rev();
+
        let mut namespaced = pat
+
            .with_namespace(
+
                components
+
                    .next()
+
                    .expect("BUG: 'namespaces' cannot be empty"),
+
            )
+
            .expect("BUG: 'namespace' cannot have globs");
+
        for ns in components {
+
            let qualified = namespaced.into_qualified();
+
            namespaced = qualified
+
                .with_namespace(ns)
+
                .expect("BUG: 'namespaces' cannot have globs");
+
        }
+
        namespaced
+
    }
+
}
+

+
impl fmt::Display for Namespace {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{}", self.namespaces)
+
    }
+
}
+

+
impl<'a> From<NonEmpty<Component<'a>>> for Namespace {
+
    fn from(cs: NonEmpty<Component<'a>>) -> Self {
+
        Self {
+
            namespaces: cs.into_iter().collect::<RefString>(),
+
        }
+
    }
+
}
+

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

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

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

+
impl FromStr for Namespace {
+
    type Err = Error;
+

+
    fn from_str(name: &str) -> Result<Self, Self::Err> {
+
        let namespaces = RefStr::try_from_str(name)?.to_ref_string();
+
        Ok(Self { namespaces })
+
    }
+
}
+

+
impl From<Namespaced<'_>> for Namespace {
+
    fn from(namespaced: Namespaced<'_>) -> Self {
+
        let mut namespaces = namespaced.namespace().to_ref_string();
+
        let mut qualified = namespaced.strip_namespace();
+
        while let Some(namespaced) = qualified.to_namespaced() {
+
            namespaces.push(namespaced.namespace());
+
            qualified = namespaced.strip_namespace();
+
        }
+
        Self { namespaces }
+
    }
+
}
+

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

+
    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+
        let name = RefStr::try_from_str(str::from_utf8(reference.name_bytes())?)?;
+
        name.to_namespaced()
+
            .ok_or(Error::EmptyNamespace)
+
            .map(Self::from)
+
    }
+
}
modified radicle-surf/src/object/commit.rs
@@ -26,10 +26,7 @@ use serde::{
    Serialize,
};

-
use crate::{
-
    diff,
-
    git::{self, glob, Glob, Repository},
-
};
+
use crate::{diff, glob, Glob, Repository, Revision};

use radicle_git_ext::Oid;

@@ -76,8 +73,8 @@ impl Header {
    }
}

-
impl From<&git::Commit> for Header {
-
    fn from(commit: &git::Commit) -> Self {
+
impl From<&crate::Commit> for Header {
+
    fn from(commit: &crate::Commit) -> Self {
        Self {
            sha1: commit.id,
            author: Person {
@@ -95,8 +92,8 @@ impl From<&git::Commit> for Header {
    }
}

-
impl From<git::Commit> for Header {
-
    fn from(commit: git::Commit) -> Self {
+
impl From<crate::Commit> for Header {
+
    fn from(commit: crate::Commit) -> Self {
        Self::from(&commit)
    }
}
@@ -124,7 +121,7 @@ pub struct Commits {
    /// The commit headers
    pub headers: Vec<Header>,
    /// The statistics for the commit headers
-
    pub stats: git::Stats,
+
    pub stats: crate::Stats,
}

/// Retrieves a [`Commit`].
@@ -133,7 +130,7 @@ pub struct Commits {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
-
pub fn commit<R: git::Revision>(repo: &Repository, rev: R) -> Result<Commit, Error> {
+
pub fn commit<R: Revision>(repo: &Repository, rev: R) -> Result<Commit, Error> {
    let commit = repo.commit(rev)?;
    let sha1 = commit.id;
    let header = Header::from(&commit);
@@ -171,7 +168,7 @@ pub fn header(repo: &Repository, sha1: Oid) -> Result<Header, Error> {
/// fails.
pub fn commits<R>(repo: &Repository, revision: &R) -> Result<Commits, Error>
where
-
    R: git::Revision,
+
    R: Revision,
{
    let stats = repo.stats_from(revision)?;
    let commits = repo.history(revision)?.collect::<Result<Vec<_>, _>>()?;
@@ -184,7 +181,7 @@ where
pub enum Error {
    /// An error occurred during a git operation.
    #[error(transparent)]
-
    Git(#[from] git::Error),
+
    Git(#[from] crate::Error),

    #[error(transparent)]
    Glob(#[from] glob::Error),
added radicle-surf/src/refs.rs
@@ -0,0 +1,246 @@
+
// I think the following `Tags` and `Branches` would be merged
+
// using Generic associated types supported in Rust 1.65.0.
+

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

+
use git_ref_format::{lit, name::Components, Component, Qualified, RefString};
+

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

+
/// Iterator over [`Tag`]s.
+
#[derive(Default)]
+
pub struct Tags<'a> {
+
    references: Vec<git2::References<'a>>,
+
    current: usize,
+
}
+

+
/// Iterator over the [`Qualified`] names of [`Tag`]s.
+
pub struct TagNames<'a> {
+
    inner: Tags<'a>,
+
}
+

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

+
    pub fn names(self) -> TagNames<'a> {
+
        TagNames { inner: self }
+
    }
+
}
+

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

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

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

+
    fn next(&mut self) -> Option<Self::Item> {
+
        while self.inner.current < self.inner.references.len() {
+
            match self.inner.references.get_mut(self.inner.current) {
+
                Some(refs) => match refs.next() {
+
                    Some(res) => {
+
                        return Some(res.map_err(error::Tag::from).and_then(|r| {
+
                            tag::reference_name(&r)
+
                                .map(|name| lit::refs_tags(name).into())
+
                                .map_err(error::Tag::from)
+
                        }))
+
                    },
+
                    None => self.inner.current += 1,
+
                },
+
                None => break,
+
            }
+
        }
+
        None
+
    }
+
}
+

+
/// Iterator over [`Branch`]es.
+
#[derive(Default)]
+
pub struct Branches<'a> {
+
    references: Vec<git2::References<'a>>,
+
    current: usize,
+
}
+

+
/// Iterator over the [`Qualified`] names of [`Branch`]es.
+
pub struct BranchNames<'a> {
+
    inner: Branches<'a>,
+
}
+

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

+
    pub fn names(self) -> BranchNames<'a> {
+
        BranchNames { inner: self }
+
    }
+
}
+

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

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

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

+
    fn next(&mut self) -> Option<Self::Item> {
+
        while self.inner.current < self.inner.references.len() {
+
            match self.inner.references.get_mut(self.inner.current) {
+
                Some(refs) => match refs.next() {
+
                    Some(res) => {
+
                        return Some(res.map_err(error::Branch::from).and_then(|r| {
+
                            Branch::try_from(&r)
+
                                .map(|branch| branch.refname().into_owned())
+
                                .map_err(error::Branch::from)
+
                        }))
+
                    },
+
                    None => self.inner.current += 1,
+
                },
+
                None => break,
+
            }
+
        }
+
        None
+
    }
+
}
+

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

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

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

+
#[derive(Default)]
+
pub struct Categories<'a> {
+
    references: Vec<git2::References<'a>>,
+
    current: usize,
+
}
+

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

+
impl<'a> Iterator for Categories<'a> {
+
    type Item = Result<(RefString, RefString), error::Category>;
+

+
    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::Category::from).and_then(|r| {
+
                            let name = std::str::from_utf8(r.name_bytes())?;
+
                            let name = git_ref_format::RefStr::try_from_str(name)?;
+
                            let name = name.qualified().ok_or_else(|| {
+
                                error::Category::NotQualified(name.to_ref_string())
+
                            })?;
+
                            let (_refs, category, c, cs) = name.non_empty_components();
+
                            Ok((category.to_ref_string(), refstr_join(c, cs)))
+
                        }));
+
                    },
+
                    None => self.current += 1,
+
                },
+
                None => break,
+
            }
+
        }
+
        None
+
    }
+
}
+

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

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

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

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

+
    #[derive(Debug, Error)]
+
    pub enum Category {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error("the reference '{0}' was expected to be qualified, i.e. 'refs/<category>/<path>'")]
+
        NotQualified(RefString),
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] str::Utf8Error),
+
    }
+

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

+
pub(crate) fn refstr_join<'a>(c: Component<'a>, cs: Components<'a>) -> RefString {
+
    std::iter::once(c).chain(cs).collect::<RefString>()
+
}
added radicle-surf/src/repo.rs
@@ -0,0 +1,568 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
use std::{
+
    collections::BTreeSet,
+
    convert::TryFrom,
+
    path::{Path, PathBuf},
+
    str,
+
};
+

+
use git_ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString};
+
use radicle_git_ext::Oid;
+
use thiserror::Error;
+

+
use crate::{
+
    commit,
+
    diff::{self, *},
+
    fs::{self, Directory, File, FileContent},
+
    glob,
+
    namespace,
+
    object::{commit::Header, Blob, Tree, TreeEntry},
+
    refs::{self, BranchNames, Branches, Categories, Namespaces, TagNames, Tags},
+
    Branch,
+
    Commit,
+
    Glob,
+
    History,
+
    Namespace,
+
    Revision,
+
    Signature,
+
    Stats,
+
    Tag,
+
    ToCommit,
+
};
+

+
/// Enumeration of errors that can occur in repo operations.
+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum Error {
+
    #[error(transparent)]
+
    Branches(#[from] refs::error::Branch),
+
    #[error(transparent)]
+
    Categories(#[from] refs::error::Category),
+
    #[error(transparent)]
+
    Commit(#[from] commit::Error),
+
    /// 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)]
+
    Directory(#[from] fs::error::Directory),
+
    #[error(transparent)]
+
    File(#[from] fs::error::File),
+
    #[error(transparent)]
+
    Git(#[from] git2::Error),
+
    #[error(transparent)]
+
    Glob(#[from] glob::Error),
+
    #[error(transparent)]
+
    Namespace(#[from] namespace::Error),
+
    #[error("the reference '{0}' should be of the form 'refs/<category>/<path>'")]
+
    NotQualified(String),
+
    /// The requested file was not found.
+
    #[error("path not found for: {0}")]
+
    PathNotFound(PathBuf),
+
    #[error(transparent)]
+
    RefFormat(#[from] git_ref_format::Error),
+
    #[error(transparent)]
+
    Revision(Box<dyn std::error::Error + Send + Sync + 'static>),
+
    /// A `revspec` was provided that could not be parsed into a branch, tag, or
+
    /// commit object.
+
    #[error("provided revspec '{rev}' could not be parsed into a git object")]
+
    RevParseFailure {
+
        /// The provided revspec that failed to parse.
+
        rev: String,
+
    },
+
    #[error(transparent)]
+
    ToCommit(Box<dyn std::error::Error + Send + Sync + 'static>),
+
    #[error(transparent)]
+
    Tags(#[from] refs::error::Tag),
+
}
+

+
/// 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 {
+
    inner: git2::Repository,
+
}
+

+
////////////////////////////////////////////
+
// Public API, ONLY add `pub fn` in here. //
+
////////////////////////////////////////////
+
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> {
+
        let repo = git2::Repository::open(repo_uri)?;
+
        Ok(Self { inner: repo })
+
    }
+

+
    /// 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> {
+
        let repo = git2::Repository::discover(repo_uri)?;
+
        Ok(Self { inner: repo })
+
    }
+

+
    /// What is the current namespace we're browsing in.
+
    pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
+
        self.inner
+
            .namespace_bytes()
+
            .map(|ns| Namespace::try_from(ns).map_err(Error::from))
+
            .transpose()
+
    }
+

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

+
    pub fn with_namespace<T, F>(&self, namespace: &RefString, f: F) -> Result<T, Error>
+
    where
+
        F: FnOnce() -> Result<T, Error>,
+
    {
+
        self.switch_namespace(namespace)?;
+
        let res = f();
+
        self.inner.remove_namespace()?;
+
        res
+
    }
+

+
    /// Returns an iterator of branches that match `pattern`.
+
    pub fn branches<G>(&self, pattern: G) -> Result<Branches, Error>
+
    where
+
        G: Into<Glob<Branch>>,
+
    {
+
        let pattern = pattern.into();
+
        let mut branches = Branches::default();
+
        for glob in pattern.globs() {
+
            let namespaced = self.namespaced_pattern(glob)?;
+
            let references = self.inner.references_glob(&namespaced)?;
+
            branches.push(references);
+
        }
+
        Ok(branches)
+
    }
+

+
    /// Lists branch names with `filter`.
+
    pub fn branch_names<G>(&self, filter: G) -> Result<BranchNames, Error>
+
    where
+
        G: Into<Glob<Branch>>,
+
    {
+
        Ok(self.branches(filter)?.names())
+
    }
+

+
    /// Returns an iterator of tags that match `pattern`.
+
    pub fn tags(&self, pattern: &Glob<Tag>) -> Result<Tags, Error> {
+
        let mut tags = Tags::default();
+
        for glob in pattern.globs() {
+
            let namespaced = self.namespaced_pattern(glob)?;
+
            let references = self.inner.references_glob(&namespaced)?;
+
            tags.push(references);
+
        }
+
        Ok(tags)
+
    }
+

+
    /// Lists tag names in the local RefScope.
+
    pub fn tag_names(&self, filter: &Glob<Tag>) -> Result<TagNames, Error> {
+
        Ok(self.tags(filter)?.names())
+
    }
+

+
    pub fn categories(&self, pattern: &Glob<Qualified<'_>>) -> Result<Categories, Error> {
+
        let mut cats = Categories::default();
+
        for glob in pattern.globs() {
+
            let namespaced = self.namespaced_pattern(glob)?;
+
            let references = self.inner.references_glob(&namespaced)?;
+
            cats.push(references);
+
        }
+
        Ok(cats)
+
    }
+

+
    /// 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() {
+
            let new_set = self
+
                .inner
+
                .references_glob(glob)?
+
                .map(|reference| {
+
                    reference
+
                        .map_err(Error::Git)
+
                        .and_then(|r| Namespace::try_from(&r).map_err(Error::from))
+
                })
+
                .collect::<Result<BTreeSet<Namespace>, Error>>()?;
+
            set.extend(new_set);
+
        }
+
        Ok(Namespaces::new(set))
+
    }
+

+
    /// Get the [`Diff`] between two commits.
+
    pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
+
        let from_commit = self.find_commit(self.object_id(&from)?)?;
+
        let to_commit = self.find_commit(self.object_id(&to)?)?;
+
        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`.
+
    ///
+
    /// If the `commit` has a parent, then it the diff will be a
+
    /// comparison between itself and that parent. Otherwise, the left
+
    /// hand side of the diff will pass nothing.
+
    pub fn diff_commit(&self, commit: impl ToCommit) -> Result<Diff, Error> {
+
        let commit = commit
+
            .to_commit(self)
+
            .map_err(|err| Error::ToCommit(err.into()))?;
+
        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.inner.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)
+
            .map_err(|err| Error::ToCommit(err.into()))?;
+
        let git2_commit = self.inner.find_commit((commit.id).into())?;
+
        let tree = git2_commit.as_object().peel_to_tree()?;
+
        Ok(Directory::root(tree.id().into(), commit))
+
    }
+

+
    /// Returns a [`Directory`] for `path` in `commit`.
+
    pub fn directory<C: ToCommit, P: AsRef<Path>>(
+
        &self,
+
        commit: C,
+
        path: &P,
+
    ) -> Result<Directory, Error> {
+
        let root = self.root_dir(commit)?;
+
        root.find_directory(path, self)?
+
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
+
    }
+

+
    /// Returns a [`File`] for `path` in `commit`.
+
    pub fn file<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<File, Error> {
+
        let root = self.root_dir(commit)?;
+
        root.find_file(path, self)?
+
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
+
    }
+

+
    /// Returns a [`Tree`] for `path` in `commit`.
+
    pub fn tree<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Tree, Error> {
+
        let commit = commit
+
            .to_commit(self)
+
            .map_err(|e| Error::ToCommit(e.into()))?;
+
        let dir = self.directory(commit.id, path)?;
+
        let mut entries = dir
+
            .entries(self)?
+
            .map(|en| {
+
                let name = en.name().to_string();
+
                let path = en.path();
+
                let commit = self
+
                    .last_commit(&path, commit.id)?
+
                    .ok_or(Error::PathNotFound(path))?;
+
                let commit_header = Header::from(commit);
+
                Ok(TreeEntry::new(name, en.into(), commit_header))
+
            })
+
            .collect::<Result<Vec<TreeEntry>, Error>>()?;
+
        entries.sort();
+

+
        let last_commit = self
+
            .last_commit(path, commit)?
+
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
+
        let header = Header::from(last_commit);
+
        Ok(Tree::new(dir.id(), entries, header))
+
    }
+

+
    /// Returns a [`Blob`] for `path` in `commit`.
+
    pub fn blob<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Blob, Error> {
+
        let commit = commit
+
            .to_commit(self)
+
            .map_err(|e| Error::ToCommit(e.into()))?;
+
        let file = self.file(commit.id, path)?;
+
        let last_commit = self
+
            .last_commit(path, commit)?
+
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
+
        let header = Header::from(last_commit);
+

+
        let content = file.content(self)?;
+
        Ok(Blob::new(file.id(), content.as_bytes(), header))
+
    }
+

+
    /// Returns the last commit, if exists, for a `path` in the history of
+
    /// `rev`.
+
    pub fn last_commit<P, C>(&self, path: &P, rev: C) -> Result<Option<Commit>, Error>
+
    where
+
        P: AsRef<Path>,
+
        C: ToCommit,
+
    {
+
        let history = self.history(rev)?;
+
        history.by_path(path).next().transpose()
+
    }
+

+
    /// Returns a commit for `rev`, if it exists.
+
    pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
+
        rev.to_commit(self)
+
    }
+

+
    /// Gets the [`Stats`] of this repository starting from the
+
    /// `HEAD` (see [`Repository::head`]) of the repository.
+
    pub fn stats(&self) -> Result<Stats, Error> {
+
        self.stats_from(&self.head()?)
+
    }
+

+
    /// Gets the [`Stats`] of this repository starting from the given
+
    /// `rev`.
+
    pub fn stats_from<R>(&self, rev: &R) -> Result<Stats, Error>
+
    where
+
        R: Revision,
+
    {
+
        let branches = self.branches(Glob::all_heads())?.count();
+
        let mut history = self.history(rev)?;
+
        let (commits, contributors) = history.try_fold(
+
            (0, BTreeSet::new()),
+
            |(commits, mut contributors), commit| {
+
                let commit = commit?;
+
                contributors.insert((commit.author.name, commit.author.email));
+
                Ok::<_, Error>((commits + 1, contributors))
+
            },
+
        )?;
+
        Ok(Stats {
+
            branches,
+
            commits,
+
            contributors: contributors.len(),
+
        })
+
    }
+

+
    // TODO(finto): I think this can be removed in favour of using
+
    // `source::Blob::new`
+
    /// Retrieves the file with `path` in this commit.
+
    pub fn get_commit_file<P, R>(&self, rev: &R, path: &P) -> Result<FileContent, crate::Error>
+
    where
+
        P: AsRef<Path>,
+
        R: Revision,
+
    {
+
        let path = path.as_ref();
+
        let id = self.object_id(rev)?;
+
        let commit = self.find_commit(id)?;
+
        let tree = commit.tree()?;
+
        let entry = tree.get_path(path)?;
+
        let object = entry.to_object(&self.inner)?;
+
        let blob = object
+
            .into_blob()
+
            .map_err(|_| Error::PathNotFound(path.to_path_buf()))?;
+
        Ok(FileContent::new(blob))
+
    }
+

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

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

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

+
////////////////////////////////////////////////////////////
+
// Private API, ONLY add `pub(crate) fn` or `fn` in here. //
+
////////////////////////////////////////////////////////////
+
impl Repository {
+
    /// Lists branches that are reachable from `oid`.
+
    pub(crate) 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.inner.find_reference(namespaced.as_str())?;
+
            if self.reachable_from(&reference, oid)? {
+
                contained_branches.push(branch);
+
            }
+
        }
+

+
        Ok(contained_branches)
+
    }
+

+
    pub(crate) fn find_blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git2::Error> {
+
        self.inner.find_blob(oid.into())
+
    }
+

+
    pub(crate) fn find_commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git2::Error> {
+
        self.inner.find_commit(oid.into())
+
    }
+

+
    pub(crate) fn find_tree(&self, oid: Oid) -> Result<git2::Tree<'_>, git2::Error> {
+
        self.inner.find_tree(oid.into())
+
    }
+

+
    pub(crate) fn refname_to_id<R>(&self, name: &R) -> Result<Oid, git2::Error>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self.inner
+
            .refname_to_id(name.as_ref().as_str())
+
            .map(Oid::from)
+
    }
+

+
    pub(crate) fn revwalk(&self) -> Result<git2::Revwalk<'_>, git2::Error> {
+
        self.inner.revwalk()
+
    }
+

+
    pub(super) fn object_id<R: Revision>(&self, r: &R) -> Result<Oid, Error> {
+
        r.object_id(self).map_err(|err| Error::Revision(err.into()))
+
    }
+

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

+
    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.inner.graph_descendant_of(other, git2_oid)?;
+

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

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

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

+
    fn diff_commits(
+
        &self,
+
        path: Option<&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.to_string_lossy().to_string());
+
            // We're skipping the binary pass because we won't be inspecting deltas.
+
            opts.skip_binary_check(true);
+
        }
+

+
        let mut diff =
+
            self.inner
+
                .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 a full reference name with namespace(s) included.
+
    pub(crate) fn namespaced_refname<'a>(
+
        &'a self,
+
        refname: &Qualified<'a>,
+
    ) -> Result<Qualified<'a>, Error> {
+
        let fullname = match self.which_namespace()? {
+
            Some(namespace) => namespace.to_namespaced(refname).into_qualified(),
+
            None => refname.clone(),
+
        };
+
        Ok(fullname)
+
    }
+

+
    /// Returns a full reference name with namespace(s) included.
+
    fn namespaced_pattern<'a>(
+
        &'a self,
+
        refname: &QualifiedPattern<'a>,
+
    ) -> Result<QualifiedPattern<'a>, Error> {
+
        let fullname = match self.which_namespace()? {
+
            Some(namespace) => namespace.to_namespaced_pattern(refname).into_qualified(),
+
            None => refname.clone(),
+
        };
+
        Ok(fullname)
+
    }
+
}
+

+
impl From<git2::Repository> for Repository {
+
    fn from(repo: git2::Repository) -> Self {
+
        Repository { inner: 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/revision.rs
@@ -0,0 +1,188 @@
+
// 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::{convert::Infallible, str::FromStr};
+

+
use git_ref_format::{Qualified, RefString};
+
use radicle_git_ext::Oid;
+

+
use crate::{Branch, Commit, Error, Repository, Tag};
+

+
/// 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 {
+
    type Error: std::error::Error + Send + Sync + 'static;
+

+
    /// Returns the object id of this revision in `repo`.
+
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error>;
+
}
+

+
impl Revision for RefString {
+
    type Error = git2::Error;
+

+
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+
        repo.refname_to_id(self)
+
    }
+
}
+

+
impl Revision for Qualified<'_> {
+
    type Error = git2::Error;
+

+
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+
        repo.refname_to_id(self)
+
    }
+
}
+

+
impl Revision for Oid {
+
    type Error = Infallible;
+

+
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+
        Ok(*self)
+
    }
+
}
+

+
impl Revision for &str {
+
    type Error = git2::Error;
+

+
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+
        Oid::from_str(self).map(Oid::from)
+
    }
+
}
+

+
impl Revision for Branch {
+
    type Error = Error;
+

+
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+
        let refname = repo.namespaced_refname(&self.refname())?;
+
        Ok(repo.refname_to_id(&refname)?)
+
    }
+
}
+

+
impl Revision for Tag {
+
    type Error = Infallible;
+

+
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+
        Ok(self.id())
+
    }
+
}
+

+
impl Revision for String {
+
    type Error = git2::Error;
+

+
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+
        Oid::from_str(self).map(Oid::from)
+
    }
+
}
+

+
impl<R: Revision> Revision for &R {
+
    type Error = R::Error;
+

+
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+
        (*self).object_id(repo)
+
    }
+
}
+

+
impl<R: Revision> Revision for Box<R> {
+
    type Error = R::Error;
+

+
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+
        self.as_ref().object_id(repo)
+
    }
+
}
+

+
/// A common trait for anything that can convert to a `Commit`.
+
pub trait ToCommit {
+
    type Error: std::error::Error + Send + Sync + 'static;
+

+
    /// Converts to a commit in `repo`.
+
    fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error>;
+
}
+

+
impl ToCommit for Commit {
+
    type Error = Infallible;
+

+
    fn to_commit(self, _repo: &Repository) -> Result<Commit, Self::Error> {
+
        Ok(self)
+
    }
+
}
+

+
impl<R: Revision> ToCommit for R {
+
    type Error = Error;
+

+
    fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error> {
+
        let oid = repo.object_id(&self)?;
+
        let commit = repo.find_commit(oid)?;
+
        Ok(Commit::try_from(commit)?)
+
    }
+
}
added radicle-surf/src/stats.rs
@@ -0,0 +1,33 @@
+
// 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 = "serde")]
+
use serde::Serialize;
+

+
/// Stats for a repository
+
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+
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/tag.rs
@@ -0,0 +1,156 @@
+
use std::{convert::TryFrom, str};
+

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

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

+
/// The static information of a [`git2::Tag`].
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+
pub enum Tag {
+
    /// A light-weight git tag.
+
    Light {
+
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
+
        id: Oid,
+
        /// The reference name for this `Tag`.
+
        name: RefString,
+
    },
+
    /// An annotated git tag.
+
    Annotated {
+
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
+
        id: Oid,
+
        /// The Object ID for the object that is tagged.
+
        target: Oid,
+
        /// The reference name for this `Tag`.
+
        name: RefString,
+
        /// The named author of this `Tag`, if the `Tag` was annotated.
+
        tagger: Option<Author>,
+
        /// The message with this `Tag`, if the `Tag` was annotated.
+
        message: Option<String>,
+
    },
+
}
+

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

+
    /// Return the short `Tag` refname,
+
    /// e.g. `release/v1`.
+
    pub fn short_name(&self) -> &RefString {
+
        match &self {
+
            Tag::Light { name, .. } => name,
+
            Tag::Annotated { name, .. } => name,
+
        }
+
    }
+

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

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

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

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

+
    #[derive(Debug, Error)]
+
    pub enum FromReference {
+
        #[error(transparent)]
+
        FromTag(#[from] FromTag),
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
+
        NotQualified(String),
+
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
+
        NotTag(RefString),
+
        #[error(transparent)]
+
        RefFormat(#[from] git_ref_format::Error),
+
        #[error(transparent)]
+
        Utf8(#[from] str::Utf8Error),
+
    }
+
}
+

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

+
    fn try_from(tag: &git2::Tag) -> Result<Self, Self::Error> {
+
        let id = tag.id().into();
+
        let target = tag.target_id().into();
+
        let name = {
+
            let name = str::from_utf8(tag.name_bytes())?;
+
            RefStr::try_from_str(name)?.to_ref_string()
+
        };
+
        let tagger = tag.tagger().map(Author::try_from).transpose()?;
+
        let message = tag
+
            .message_bytes()
+
            .map(str::from_utf8)
+
            .transpose()?
+
            .map(|message| message.into());
+

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

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

+
    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+
        let name = reference_name(reference)?;
+
        match reference.peel_to_tag() {
+
            Ok(tag) => Tag::try_from(&tag).map_err(error::FromReference::from),
+
            // If we get an error peeling to a tag _BUT_ we also have confirmed the
+
            // reference is a tag, that means we have a lightweight tag,
+
            // i.e. a commit SHA and name.
+
            Err(err)
+
                if err.class() == git2::ErrorClass::Object
+
                    && err.code() == git2::ErrorCode::InvalidSpec =>
+
            {
+
                let commit = reference.peel_to_commit()?;
+
                Ok(Tag::Light {
+
                    id: commit.id().into(),
+
                    name,
+
                })
+
            },
+
            Err(err) => Err(err.into()),
+
        }
+
    }
+
}
+

+
pub(crate) fn reference_name(
+
    reference: &git2::Reference,
+
) -> Result<RefString, error::FromReference> {
+
    let name = str::from_utf8(reference.name_bytes())?;
+
    let name = RefStr::try_from_str(name)?
+
        .qualified()
+
        .ok_or_else(|| error::FromReference::NotQualified(name.to_string()))?;
+

+
    let (_refs, tags, c, cs) = name.non_empty_components();
+

+
    if tags == component::TAGS {
+
        Ok(refstr_join(c, cs))
+
    } else {
+
        Err(error::FromReference::NotTag(name.into()))
+
    }
+
}
added radicle-surf/t/src/branch.rs
@@ -0,0 +1,25 @@
+
use git_ref_format::{RefStr, RefString};
+
use git_ref_format_test::gen;
+
use proptest::prelude::*;
+
use test_helpers::roundtrip;
+

+
use radicle_surf::Branch;
+

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

+
fn gen_branch() -> impl Strategy<Value = Branch> {
+
    prop_oneof![
+
        gen::valid().prop_map(|name| Branch::local(RefString::try_from(name).unwrap())),
+
        (gen::valid(), gen::valid()).prop_map(|(remote, name): (String, String)| {
+
            let remote =
+
                RefStr::try_from_str(&remote).expect("BUG: reference strings should be valid");
+
            let name = RefStr::try_from_str(&name).expect("BUG: reference strings should be valid");
+
            Branch::remote(remote.head(), name)
+
        })
+
    ]
+
}
added radicle-surf/t/src/code_browsing.rs
@@ -0,0 +1,82 @@
+
use std::path::Path;
+

+
use git_ref_format::refname;
+
use radicle_surf::{
+
    fs::{self, Directory},
+
    Branch,
+
    Repository,
+
};
+

+
use super::GIT_PLATINUM;
+

+
#[test]
+
fn iterate_root_dir_recursive() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+

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

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

+
    /// Prints items in `dir` with `indent_level`.
+
    /// For sub-directories, will do Depth-First-Search and print
+
    /// recursively.
+
    /// Returns the number of items visited (i.e. printed)
+
    fn println_dir(dir: &Directory, repo: &Repository) -> i32 {
+
        dir.traverse::<fs::error::Directory, _, _>(
+
            repo,
+
            (0, 0),
+
            &mut |(count, indent_level), entry| {
+
                println!("> {}{}", " ".repeat(indent_level * 4), entry.name());
+
                match entry {
+
                    fs::Entry::File(_) => Ok((count + 1, indent_level)),
+
                    fs::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
+
                }
+
            },
+
        )
+
        .unwrap()
+
        .0
+
    }
+
}
+

+
#[test]
+
fn browse_repo_lazily() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+

+
    let root_dir = repo.root_dir(Branch::local(refname!("master"))).unwrap();
+
    let count = root_dir.entries(&repo).unwrap().entries().count();
+
    assert_eq!(count, 8);
+
    let count = traverse(&root_dir, &repo);
+
    assert_eq!(count, 36);
+

+
    fn traverse(dir: &Directory, repo: &Repository) -> i32 {
+
        dir.traverse::<fs::error::Directory, _, _>(repo, 0, &mut |count, _| Ok(count + 1))
+
            .unwrap()
+
    }
+
}
+

+
#[test]
+
fn test_file_history() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+
    let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
+
    let path = Path::new("README.md");
+
    let mut file_history = history.by_path(&path);
+
    let commit = file_history.next().unwrap().unwrap();
+
    let file = repo.get_commit_file(&commit.id, &path).unwrap();
+
    assert_eq!(file.size(), 67);
+
}
+

+
#[test]
+
fn test_commit_history() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+
    let head = "a0dd9122d33dff2a35f564d564db127152c88e02";
+

+
    // verify `&str` works.
+
    let h1 = repo.history(head).unwrap();
+

+
    // verify `&String` works.
+
    let head_string = head.to_string();
+
    let h2 = repo.history(&head_string).unwrap();
+

+
    assert_eq!(h1.head().id, h2.head().id);
+
}
added radicle-surf/t/src/commit.rs
@@ -0,0 +1,32 @@
+
use std::str::FromStr;
+

+
use proptest::prelude::*;
+
use radicle_git_ext::Oid;
+
use radicle_surf::{Author, Commit};
+
use test_helpers::roundtrip;
+

+
proptest! {
+
    #[test]
+
    fn prop_test_commits(commit in commits_strategy()) {
+
        roundtrip::json(commit)
+
    }
+
}
+

+
fn commits_strategy() -> impl Strategy<Value = Commit> {
+
    ("[a-fA-F0-9]{40}", any::<String>(), any::<i64>()).prop_map(|(id, text, time)| Commit {
+
        id: Oid::from_str(&id).unwrap(),
+
        author: Author {
+
            name: text.clone(),
+
            email: text.clone(),
+
            time: git2::Time::new(time, 0),
+
        },
+
        committer: Author {
+
            name: text.clone(),
+
            email: text.clone(),
+
            time: git2::Time::new(time, 0),
+
        },
+
        message: text.clone(),
+
        summary: text,
+
        parents: vec![Oid::from_str(&id).unwrap(), Oid::from_str(&id).unwrap()],
+
    })
+
}
added radicle-surf/t/src/diff.rs
@@ -0,0 +1,342 @@
+
use git_ref_format::refname;
+
use pretty_assertions::assert_eq;
+
use radicle_git_ext::Oid;
+
use radicle_surf::{
+
    diff::{
+
        Added,
+
        Addition,
+
        Deleted,
+
        Deletion,
+
        Diff,
+
        EofNewLine,
+
        FileDiff,
+
        Hunk,
+
        Line,
+
        Modification,
+
        Modified,
+
        Moved,
+
        Stats,
+
    },
+
    Branch,
+
    Error,
+
    Repository,
+
};
+
use std::{path::Path, str::FromStr};
+

+
use super::GIT_PLATINUM;
+

+
#[test]
+
fn test_initial_diff() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
+
    let commit = repo.commit(oid).unwrap();
+
    assert!(commit.parents.is_empty());
+

+
    let diff = repo.diff_commit(oid)?;
+

+
    let expected_diff = Diff {
+
        added: vec![Added {
+
            path: Path::new("README.md").to_path_buf(),
+
            diff: FileDiff::Plain {
+
                hunks: vec![Hunk {
+
                    header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()),
+
                    lines: vec![Addition {
+
                        line:
+
                            b"This repository is a data source for the Upstream front-end tests.\n"
+
                                .to_vec()
+
                                .into(),
+
                        line_no: 1,
+
                    }],
+
                }]
+
                .into(),
+
            },
+
        }],
+
        deleted: vec![],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![],
+
        stats: Stats {
+
            files_changed: 1,
+
            insertions: 1,
+
            deletions: 0,
+
        },
+
    };
+
    assert_eq!(expected_diff, diff);
+

+
    Ok(())
+
}
+

+
#[test]
+
fn test_diff_of_rev() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let diff = repo.diff_commit("80bacafba303bf0cdf6142921f430ff265f25095")?;
+
    assert_eq!(diff.added.len(), 0);
+
    assert_eq!(diff.deleted.len(), 0);
+
    assert_eq!(diff.moved.len(), 0);
+
    assert_eq!(diff.modified.len(), 1);
+
    Ok(())
+
}
+

+
#[test]
+
fn test_diff() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let oid = "80bacafba303bf0cdf6142921f430ff265f25095";
+
    let commit = repo.commit(oid).unwrap();
+
    let parent_oid = commit.parents.get(0).unwrap();
+
    let diff = repo.diff(*parent_oid, oid)?;
+

+
    let expected_diff = Diff {
+
        added: vec![],
+
        deleted: vec![],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![Modified {
+
            path: Path::new("README.md").to_path_buf(),
+
            diff: FileDiff::Plain {
+
                hunks: vec![Hunk {
+
                    header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
+
                    lines: vec![
+
                        Modification::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
+
                        Modification::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
+
                        Modification::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
+
                    ]
+
                }].into()
+
            },
+
            eof: None,
+
        }],
+
        stats: Stats {
+
            files_changed: 1,
+
            insertions: 2,
+
            deletions: 1,
+
        },
+
    };
+
    assert_eq!(expected_diff, diff);
+

+
    Ok(())
+
}
+

+
#[test]
+
fn test_branch_diff() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let diff = repo.diff(
+
        Branch::local(refname!("master")),
+
        Branch::local(refname!("dev")),
+
    )?;
+

+
    println!("Diff two branches: master -> dev");
+
    println!(
+
        "result: added {} deleted {} moved {} modified {}",
+
        diff.added.len(),
+
        diff.deleted.len(),
+
        diff.moved.len(),
+
        diff.modified.len()
+
    );
+
    assert_eq!(diff.added.len(), 1);
+
    assert_eq!(diff.deleted.len(), 11);
+
    assert_eq!(diff.moved.len(), 1);
+
    assert_eq!(diff.modified.len(), 2);
+
    for c in diff.added.iter() {
+
        println!("added: {:?}", &c.path);
+
    }
+
    for d in diff.deleted.iter() {
+
        println!("deleted: {:?}", &d.path);
+
    }
+
    for m in diff.moved.iter() {
+
        println!("moved: {:?} -> {:?}", &m.old_path, &m.new_path);
+
    }
+
    for m in diff.modified.iter() {
+
        println!("modified: {:?}", &m.path);
+
    }
+
    Ok(())
+
}
+

+
#[test]
+
fn test_diff_serde() {
+
    let diff = Diff {
+
        added: vec![ Added {
+
            path: Path::new("LICENSE").to_path_buf(),
+
            diff: FileDiff::Plain {
+
                hunks: vec![Hunk {
+
                    header: Line::from(b"@@ -0,0 +1,1".to_vec()),
+
                    lines: vec![
+
                        Addition { line: Line::from(b"MIT".to_vec()), line_no: 1 }
+
                    ]
+
                }].into()
+
            }
+
        }],
+
        deleted: vec![ Deleted {
+
            path: Path::new("DCO").to_path_buf(),
+
            diff: FileDiff::Plain {
+
                hunks: vec![Hunk {
+
                    header: Line::from(b"@@ -0,0 +1,1".to_vec()),
+
                    lines: vec![
+
                        Deletion { line: Line::from(b"TODO".to_vec()), line_no: 1 }
+
                    ]
+
                }].into()
+
            }
+
        }],
+
        moved: vec![ Moved {
+
            old_path: Path::new("CONTRIBUTING").to_path_buf(),
+
            new_path: Path::new("CONTRIBUTING.md").to_path_buf(),
+
        }],
+
        copied: vec![],
+
        modified: vec![ Modified {
+
            path: Path::new("README.md").to_path_buf(),
+
            diff: FileDiff::Plain {
+
                hunks: vec![Hunk {
+
                header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
+
                lines: vec![
+
                    Modification::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
+
                    Modification::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
+
                    Modification::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
+
                    Modification::context(b"\n".to_vec(), 3, 4),
+
                ]
+
                }].into()
+
            },
+
            eof: None,
+
        }],
+
        stats: Stats {
+
            files_changed: 3,
+
            insertions: 2,
+
            deletions: 1,
+
        },
+
    };
+

+
    let eof: Option<u8> = None;
+
    let json = serde_json::json!({
+
        "added": [{
+
            "path": "LICENSE",
+
            "diff": {
+
                "type": "plain",
+
                "hunks": [{
+
                    "header": "@@ -0,0 +1,1",
+
                    "lines": [{
+
                        "line": "MIT",
+
                        "lineNo": 1,
+
                        "type": "addition",
+
                    }]
+
                }]
+
            },
+
        }],
+
        "deleted": [{
+
            "path": "DCO",
+
            "diff": {
+
                "type": "plain",
+
                "hunks": [{
+
                    "header": "@@ -0,0 +1,1",
+
                    "lines": [{
+
                        "line": "TODO",
+
                        "lineNo": 1,
+
                        "type": "deletion",
+
                    }]
+
                }]
+
            },
+
        }],
+
        "moved": [{ "oldPath": "CONTRIBUTING", "newPath": "CONTRIBUTING.md" }],
+
        "copied": [],
+
        "modified": [{
+
            "path": "README.md",
+
            "diff": {
+
                "type": "plain",
+
                "hunks": [{
+
                    "header": "@@ -1 +1,2 @@\n",
+
                    "lines": [
+
                        { "lineNo": 1,
+
                          "line": "This repository is a data source for the Upstream front-end tests.\n",
+
                          "type": "deletion"
+
                        },
+
                        { "lineNo": 1,
+
                          "line": "This repository is a data source for the Upstream front-end tests and the\n",
+
                          "type": "addition"
+
                        },
+
                        { "lineNo": 2,
+
                          "line": "[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n",
+
                          "type": "addition"
+
                        },
+
                        { "lineNoOld": 3, "lineNoNew": 4,
+
                          "line": "\n",
+
                          "type": "context"
+
                        }
+
                    ]
+
                }]
+
            },
+
            "eof" : eof,
+
        }],
+
        "stats": {
+
            "deletions": 1,
+
            "filesChanged": 3,
+
            "insertions": 2,
+
        }
+
    });
+
    assert_eq!(serde_json::to_value(&diff).unwrap(), json);
+
}
+

+
#[test]
+
fn test_both_missing_eof_newline() {
+
    let buf = r#"
+
diff --git a/.env b/.env
+
index f89e4c0..7c56eb7 100644
+
--- a/.env
+
+++ b/.env
+
@@ -1 +1 @@
+
-hello=123
+
\ No newline at end of file
+
+hello=1234
+
\ No newline at end of file
+
"#;
+
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
    let diff = Diff::try_from(diff).unwrap();
+
    assert_eq!(diff.modified[0].eof, Some(EofNewLine::BothMissing));
+
}
+

+
#[test]
+
fn test_none_missing_eof_newline() {
+
    let buf = r#"
+
diff --git a/.env b/.env
+
index f89e4c0..7c56eb7 100644
+
--- a/.env
+
+++ b/.env
+
@@ -1 +1 @@
+
-hello=123
+
+hello=1234
+
"#;
+
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
    let diff = Diff::try_from(diff).unwrap();
+
    assert_eq!(diff.modified[0].eof, None);
+
}
+

+
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
+
//#[test]
+
//     fn test_old_missing_eof_newline() {
+
//         let buf = r#"
+
// diff --git a/.env b/.env
+
// index f89e4c0..7c56eb7 100644
+
// --- a/.env
+
// +++ b/.env
+
// @@ -1 +1 @@
+
// -hello=123
+
// \ No newline at end of file
+
// +hello=1234
+
// "#;
+
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
//         let diff = Diff::try_from(diff).unwrap();
+
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::OldMissing));
+
//     }
+

+
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
+
//#[test]
+
//     fn test_new_missing_eof_newline() {
+
//         let buf = r#"
+
// diff --git a/.env b/.env
+
// index f89e4c0..7c56eb7 100644
+
// --- a/.env
+
// +++ b/.env
+
// @@ -1 +1 @@
+
// -hello=123
+
// +hello=1234
+
// \ No newline at end of file
+
// "#;
+
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
//         let diff = Diff::try_from(diff).unwrap();
+
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::NewMissing));
+
//     }
modified radicle-surf/t/src/file_system.rs
@@ -7,7 +7,8 @@ mod directory {
    use git_ref_format::refname;
    use radicle_surf::{
        fs::{self, Entry},
-
        git::{Branch, Repository},
+
        Branch,
+
        Repository,
    };
    use std::path::Path;

deleted radicle-surf/t/src/git.rs
@@ -1,27 +0,0 @@
-
// Copyright © 2022 The Radicle Git Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

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

-
const GIT_PLATINUM: &str = "../data/git-platinum";
-

-
#[cfg(test)]
-
mod branch;
-
#[cfg(test)]
-
mod code_browsing;
-
#[cfg(test)]
-
mod commit;
-
#[cfg(test)]
-
mod diff;
-
#[cfg(test)]
-
mod last_commit;
-
#[cfg(test)]
-
mod namespace;
-
#[cfg(test)]
-
mod reference;
-
#[cfg(test)]
-
mod rev;
-
#[cfg(test)]
-
mod submodule;
-
#[cfg(test)]
-
mod threading;
deleted radicle-surf/t/src/git/branch.rs
@@ -1,25 +0,0 @@
-
use git_ref_format::{RefStr, RefString};
-
use git_ref_format_test::gen;
-
use proptest::prelude::*;
-
use test_helpers::roundtrip;
-

-
use radicle_surf::git::Branch;
-

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

-
fn gen_branch() -> impl Strategy<Value = Branch> {
-
    prop_oneof![
-
        gen::valid().prop_map(|name| Branch::local(RefString::try_from(name).unwrap())),
-
        (gen::valid(), gen::valid()).prop_map(|(remote, name): (String, String)| {
-
            let remote =
-
                RefStr::try_from_str(&remote).expect("BUG: reference strings should be valid");
-
            let name = RefStr::try_from_str(&name).expect("BUG: reference strings should be valid");
-
            Branch::remote(remote.head(), name)
-
        })
-
    ]
-
}
deleted radicle-surf/t/src/git/code_browsing.rs
@@ -1,81 +0,0 @@
-
use std::path::Path;
-

-
use git_ref_format::refname;
-
use radicle_surf::{
-
    fs::{self, Directory},
-
    git::{Branch, Repository},
-
};
-

-
use super::GIT_PLATINUM;
-

-
#[test]
-
fn iterate_root_dir_recursive() {
-
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-

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

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

-
    /// Prints items in `dir` with `indent_level`.
-
    /// For sub-directories, will do Depth-First-Search and print
-
    /// recursively.
-
    /// Returns the number of items visited (i.e. printed)
-
    fn println_dir(dir: &Directory, repo: &Repository) -> i32 {
-
        dir.traverse::<fs::error::Directory, _, _>(
-
            repo,
-
            (0, 0),
-
            &mut |(count, indent_level), entry| {
-
                println!("> {}{}", " ".repeat(indent_level * 4), entry.name());
-
                match entry {
-
                    fs::Entry::File(_) => Ok((count + 1, indent_level)),
-
                    fs::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
-
                }
-
            },
-
        )
-
        .unwrap()
-
        .0
-
    }
-
}
-

-
#[test]
-
fn browse_repo_lazily() {
-
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-

-
    let root_dir = repo.root_dir(Branch::local(refname!("master"))).unwrap();
-
    let count = root_dir.entries(&repo).unwrap().entries().count();
-
    assert_eq!(count, 8);
-
    let count = traverse(&root_dir, &repo);
-
    assert_eq!(count, 36);
-

-
    fn traverse(dir: &Directory, repo: &Repository) -> i32 {
-
        dir.traverse::<fs::error::Directory, _, _>(repo, 0, &mut |count, _| Ok(count + 1))
-
            .unwrap()
-
    }
-
}
-

-
#[test]
-
fn test_file_history() {
-
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-
    let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
-
    let path = Path::new("README.md");
-
    let mut file_history = history.by_path(&path);
-
    let commit = file_history.next().unwrap().unwrap();
-
    let file = repo.get_commit_file(&commit.id, &path).unwrap();
-
    assert_eq!(file.size(), 67);
-
}
-

-
#[test]
-
fn test_commit_history() {
-
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-
    let head = "a0dd9122d33dff2a35f564d564db127152c88e02";
-

-
    // verify `&str` works.
-
    let h1 = repo.history(head).unwrap();
-

-
    // verify `&String` works.
-
    let head_string = head.to_string();
-
    let h2 = repo.history(&head_string).unwrap();
-

-
    assert_eq!(h1.head().id, h2.head().id);
-
}
deleted radicle-surf/t/src/git/commit.rs
@@ -1,32 +0,0 @@
-
use std::str::FromStr;
-

-
use proptest::prelude::*;
-
use radicle_git_ext::Oid;
-
use radicle_surf::git::{Author, Commit};
-
use test_helpers::roundtrip;
-

-
proptest! {
-
    #[test]
-
    fn prop_test_commits(commit in commits_strategy()) {
-
        roundtrip::json(commit)
-
    }
-
}
-

-
fn commits_strategy() -> impl Strategy<Value = Commit> {
-
    ("[a-fA-F0-9]{40}", any::<String>(), any::<i64>()).prop_map(|(id, text, time)| Commit {
-
        id: Oid::from_str(&id).unwrap(),
-
        author: Author {
-
            name: text.clone(),
-
            email: text.clone(),
-
            time: git2::Time::new(time, 0),
-
        },
-
        committer: Author {
-
            name: text.clone(),
-
            email: text.clone(),
-
            time: git2::Time::new(time, 0),
-
        },
-
        message: text.clone(),
-
        summary: text,
-
        parents: vec![Oid::from_str(&id).unwrap(), Oid::from_str(&id).unwrap()],
-
    })
-
}
deleted radicle-surf/t/src/git/diff.rs
@@ -1,340 +0,0 @@
-
use git_ref_format::refname;
-
use pretty_assertions::assert_eq;
-
use radicle_git_ext::Oid;
-
use radicle_surf::{
-
    diff::{
-
        Added,
-
        Addition,
-
        Deleted,
-
        Deletion,
-
        Diff,
-
        EofNewLine,
-
        FileDiff,
-
        Hunk,
-
        Line,
-
        Modification,
-
        Modified,
-
        Moved,
-
        Stats,
-
    },
-
    git::{Branch, Error, Repository},
-
};
-
use std::{path::Path, str::FromStr};
-

-
use super::GIT_PLATINUM;
-

-
#[test]
-
fn test_initial_diff() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
-
    let commit = repo.commit(oid).unwrap();
-
    assert!(commit.parents.is_empty());
-

-
    let diff = repo.diff_commit(oid)?;
-

-
    let expected_diff = Diff {
-
        added: vec![Added {
-
            path: Path::new("README.md").to_path_buf(),
-
            diff: FileDiff::Plain {
-
                hunks: vec![Hunk {
-
                    header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()),
-
                    lines: vec![Addition {
-
                        line:
-
                            b"This repository is a data source for the Upstream front-end tests.\n"
-
                                .to_vec()
-
                                .into(),
-
                        line_no: 1,
-
                    }],
-
                }]
-
                .into(),
-
            },
-
        }],
-
        deleted: vec![],
-
        moved: vec![],
-
        copied: vec![],
-
        modified: vec![],
-
        stats: Stats {
-
            files_changed: 1,
-
            insertions: 1,
-
            deletions: 0,
-
        },
-
    };
-
    assert_eq!(expected_diff, diff);
-

-
    Ok(())
-
}
-

-
#[test]
-
fn test_diff_of_rev() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let diff = repo.diff_commit("80bacafba303bf0cdf6142921f430ff265f25095")?;
-
    assert_eq!(diff.added.len(), 0);
-
    assert_eq!(diff.deleted.len(), 0);
-
    assert_eq!(diff.moved.len(), 0);
-
    assert_eq!(diff.modified.len(), 1);
-
    Ok(())
-
}
-

-
#[test]
-
fn test_diff() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let oid = "80bacafba303bf0cdf6142921f430ff265f25095";
-
    let commit = repo.commit(oid).unwrap();
-
    let parent_oid = commit.parents.get(0).unwrap();
-
    let diff = repo.diff(*parent_oid, oid)?;
-

-
    let expected_diff = Diff {
-
        added: vec![],
-
        deleted: vec![],
-
        moved: vec![],
-
        copied: vec![],
-
        modified: vec![Modified {
-
            path: Path::new("README.md").to_path_buf(),
-
            diff: FileDiff::Plain {
-
                hunks: vec![Hunk {
-
                    header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
-
                    lines: vec![
-
                        Modification::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
-
                        Modification::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
-
                        Modification::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
-
                    ]
-
                }].into()
-
            },
-
            eof: None,
-
        }],
-
        stats: Stats {
-
            files_changed: 1,
-
            insertions: 2,
-
            deletions: 1,
-
        },
-
    };
-
    assert_eq!(expected_diff, diff);
-

-
    Ok(())
-
}
-

-
#[test]
-
fn test_branch_diff() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let diff = repo.diff(
-
        Branch::local(refname!("master")),
-
        Branch::local(refname!("dev")),
-
    )?;
-

-
    println!("Diff two branches: master -> dev");
-
    println!(
-
        "result: added {} deleted {} moved {} modified {}",
-
        diff.added.len(),
-
        diff.deleted.len(),
-
        diff.moved.len(),
-
        diff.modified.len()
-
    );
-
    assert_eq!(diff.added.len(), 1);
-
    assert_eq!(diff.deleted.len(), 11);
-
    assert_eq!(diff.moved.len(), 1);
-
    assert_eq!(diff.modified.len(), 2);
-
    for c in diff.added.iter() {
-
        println!("added: {:?}", &c.path);
-
    }
-
    for d in diff.deleted.iter() {
-
        println!("deleted: {:?}", &d.path);
-
    }
-
    for m in diff.moved.iter() {
-
        println!("moved: {:?} -> {:?}", &m.old_path, &m.new_path);
-
    }
-
    for m in diff.modified.iter() {
-
        println!("modified: {:?}", &m.path);
-
    }
-
    Ok(())
-
}
-

-
#[test]
-
fn test_diff_serde() {
-
    let diff = Diff {
-
        added: vec![ Added {
-
            path: Path::new("LICENSE").to_path_buf(),
-
            diff: FileDiff::Plain {
-
                hunks: vec![Hunk {
-
                    header: Line::from(b"@@ -0,0 +1,1".to_vec()),
-
                    lines: vec![
-
                        Addition { line: Line::from(b"MIT".to_vec()), line_no: 1 }
-
                    ]
-
                }].into()
-
            }
-
        }],
-
        deleted: vec![ Deleted {
-
            path: Path::new("DCO").to_path_buf(),
-
            diff: FileDiff::Plain {
-
                hunks: vec![Hunk {
-
                    header: Line::from(b"@@ -0,0 +1,1".to_vec()),
-
                    lines: vec![
-
                        Deletion { line: Line::from(b"TODO".to_vec()), line_no: 1 }
-
                    ]
-
                }].into()
-
            }
-
        }],
-
        moved: vec![ Moved {
-
            old_path: Path::new("CONTRIBUTING").to_path_buf(),
-
            new_path: Path::new("CONTRIBUTING.md").to_path_buf(),
-
        }],
-
        copied: vec![],
-
        modified: vec![ Modified {
-
            path: Path::new("README.md").to_path_buf(),
-
            diff: FileDiff::Plain {
-
                hunks: vec![Hunk {
-
                header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
-
                lines: vec![
-
                    Modification::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
-
                    Modification::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
-
                    Modification::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
-
                    Modification::context(b"\n".to_vec(), 3, 4),
-
                ]
-
                }].into()
-
            },
-
            eof: None,
-
        }],
-
        stats: Stats {
-
            files_changed: 3,
-
            insertions: 2,
-
            deletions: 1,
-
        },
-
    };
-

-
    let eof: Option<u8> = None;
-
    let json = serde_json::json!({
-
        "added": [{
-
            "path": "LICENSE",
-
            "diff": {
-
                "type": "plain",
-
                "hunks": [{
-
                    "header": "@@ -0,0 +1,1",
-
                    "lines": [{
-
                        "line": "MIT",
-
                        "lineNo": 1,
-
                        "type": "addition",
-
                    }]
-
                }]
-
            },
-
        }],
-
        "deleted": [{
-
            "path": "DCO",
-
            "diff": {
-
                "type": "plain",
-
                "hunks": [{
-
                    "header": "@@ -0,0 +1,1",
-
                    "lines": [{
-
                        "line": "TODO",
-
                        "lineNo": 1,
-
                        "type": "deletion",
-
                    }]
-
                }]
-
            },
-
        }],
-
        "moved": [{ "oldPath": "CONTRIBUTING", "newPath": "CONTRIBUTING.md" }],
-
        "copied": [],
-
        "modified": [{
-
            "path": "README.md",
-
            "diff": {
-
                "type": "plain",
-
                "hunks": [{
-
                    "header": "@@ -1 +1,2 @@\n",
-
                    "lines": [
-
                        { "lineNo": 1,
-
                          "line": "This repository is a data source for the Upstream front-end tests.\n",
-
                          "type": "deletion"
-
                        },
-
                        { "lineNo": 1,
-
                          "line": "This repository is a data source for the Upstream front-end tests and the\n",
-
                          "type": "addition"
-
                        },
-
                        { "lineNo": 2,
-
                          "line": "[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n",
-
                          "type": "addition"
-
                        },
-
                        { "lineNoOld": 3, "lineNoNew": 4,
-
                          "line": "\n",
-
                          "type": "context"
-
                        }
-
                    ]
-
                }]
-
            },
-
            "eof" : eof,
-
        }],
-
        "stats": {
-
            "deletions": 1,
-
            "filesChanged": 3,
-
            "insertions": 2,
-
        }
-
    });
-
    assert_eq!(serde_json::to_value(&diff).unwrap(), json);
-
}
-

-
#[test]
-
fn test_both_missing_eof_newline() {
-
    let buf = r#"
-
diff --git a/.env b/.env
-
index f89e4c0..7c56eb7 100644
-
--- a/.env
-
+++ b/.env
-
@@ -1 +1 @@
-
-hello=123
-
\ No newline at end of file
-
+hello=1234
-
\ No newline at end of file
-
"#;
-
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
    let diff = Diff::try_from(diff).unwrap();
-
    assert_eq!(diff.modified[0].eof, Some(EofNewLine::BothMissing));
-
}
-

-
#[test]
-
fn test_none_missing_eof_newline() {
-
    let buf = r#"
-
diff --git a/.env b/.env
-
index f89e4c0..7c56eb7 100644
-
--- a/.env
-
+++ b/.env
-
@@ -1 +1 @@
-
-hello=123
-
+hello=1234
-
"#;
-
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
    let diff = Diff::try_from(diff).unwrap();
-
    assert_eq!(diff.modified[0].eof, None);
-
}
-

-
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
-
//#[test]
-
//     fn test_old_missing_eof_newline() {
-
//         let buf = r#"
-
// diff --git a/.env b/.env
-
// index f89e4c0..7c56eb7 100644
-
// --- a/.env
-
// +++ b/.env
-
// @@ -1 +1 @@
-
// -hello=123
-
// \ No newline at end of file
-
// +hello=1234
-
// "#;
-
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
//         let diff = Diff::try_from(diff).unwrap();
-
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::OldMissing));
-
//     }
-

-
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
-
//#[test]
-
//     fn test_new_missing_eof_newline() {
-
//         let buf = r#"
-
// diff --git a/.env b/.env
-
// index f89e4c0..7c56eb7 100644
-
// --- a/.env
-
// +++ b/.env
-
// @@ -1 +1 @@
-
// -hello=123
-
// +hello=1234
-
// \ No newline at end of file
-
// "#;
-
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
//         let diff = Diff::try_from(diff).unwrap();
-
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::NewMissing));
-
//     }
deleted radicle-surf/t/src/git/last_commit.rs
@@ -1,120 +0,0 @@
-
use std::{path::PathBuf, str::FromStr};
-

-
use git_ref_format::refname;
-
use radicle_git_ext::Oid;
-
use radicle_surf::git::{Branch, Repository};
-

-
use super::GIT_PLATINUM;
-

-
#[test]
-
fn readme_missing_and_memory() {
-
    let repo = Repository::open(GIT_PLATINUM)
-
        .expect("Could not retrieve ./data/git-platinum as git repository");
-
    let oid =
-
        Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");
-

-
    // memory.rs is commited later so it should not exist here.
-
    let memory_last_commit_oid = repo
-
        .last_commit(&"src/memory.rs", oid)
-
        .expect("Failed to get last commit")
-
        .map(|commit| commit.id);
-

-
    assert_eq!(memory_last_commit_oid, None);
-

-
    // README.md exists in this commit.
-
    let readme_last_commit = repo
-
        .last_commit(&"README.md", oid)
-
        .expect("Failed to get last commit")
-
        .map(|commit| commit.id);
-

-
    assert_eq!(readme_last_commit, Some(oid));
-
}
-

-
#[test]
-
fn folder_svelte() {
-
    let repo = Repository::open(GIT_PLATINUM)
-
        .expect("Could not retrieve ./data/git-platinum as git repository");
-
    // Check that last commit is the actual last commit even if head commit differs.
-
    let oid =
-
        Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
-

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

-
    let folder_svelte = repo
-
        .last_commit(&"examples/Folder.svelte", oid)
-
        .expect("Failed to get last commit")
-
        .map(|commit| commit.id);
-

-
    assert_eq!(folder_svelte, Some(expected_commit_id));
-
}
-

-
#[test]
-
fn nest_directory() {
-
    let repo = Repository::open(GIT_PLATINUM)
-
        .expect("Could not retrieve ./data/git-platinum as git repository");
-
    // Check that last commit is the actual last commit even if head commit differs.
-
    let oid =
-
        Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
-

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

-
    let nested_directory_tree_commit_id = repo
-
        .last_commit(&"this/is/a/really/deeply/nested/directory/tree", oid)
-
        .expect("Failed to get last commit")
-
        .map(|commit| commit.id);
-

-
    assert_eq!(nested_directory_tree_commit_id, Some(expected_commit_id));
-
}
-

-
#[test]
-
#[cfg(not(windows))]
-
fn can_get_last_commit_for_special_filenames() {
-
    let repo = Repository::open(GIT_PLATINUM)
-
        .expect("Could not retrieve ./data/git-platinum as git repository");
-

-
    // Check that last commit is the actual last commit even if head commit differs.
-
    let oid =
-
        Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA");
-

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

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

-
    let ogre_commit_id = repo
-
        .last_commit(&"special/👹👹👹", oid)
-
        .expect("Failed to get last commit")
-
        .map(|commit| commit.id);
-
    assert_eq!(ogre_commit_id, Some(expected_commit_id));
-
}
-

-
#[test]
-
fn root() {
-
    let repo = Repository::open(GIT_PLATINUM)
-
        .expect("Could not retrieve ./data/git-platinum as git repository");
-
    let rev = Branch::local(refname!("master"));
-
    let root_last_commit_id = repo
-
        .last_commit(&PathBuf::new(), rev)
-
        .expect("Failed to get last commit")
-
        .map(|commit| commit.id);
-

-
    let expected_oid = repo
-
        .history(&Branch::local(refname!("master")))
-
        .unwrap()
-
        .head()
-
        .id;
-
    assert_eq!(root_last_commit_id, Some(expected_oid));
-
}
-

-
#[test]
-
fn binary_file() {
-
    let repo = Repository::open(GIT_PLATINUM)
-
        .expect("Could not retrieve ./data/git-platinum as git repository");
-
    let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
-
    let file_commit = history.by_path(&"bin/cat").next();
-
    assert!(file_commit.is_some());
-
    println!("file commit: {:?}", &file_commit);
-
}
deleted radicle-surf/t/src/git/namespace.rs
@@ -1,121 +0,0 @@
-
use git_ref_format::{name::component, refname, refspec};
-
use pretty_assertions::{assert_eq, assert_ne};
-
use radicle_surf::git::{Branch, Error, Glob, Repository};
-

-
use super::GIT_PLATINUM;
-

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

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

-
    Ok(())
-
}
-

-
#[test]
-
fn me_namespace() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let history = repo.history(&Branch::local(refname!("master")))?;
-

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

-
    repo.switch_namespace(&refname!("me"))?;
-
    assert_eq!(repo.which_namespace().unwrap(), Some("me".parse()?));
-

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

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

-
    assert_eq!(expected_branches, branches);
-

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

-
    assert_eq!(expected_branches, branches);
-

-
    Ok(())
-
}
-

-
#[test]
-
fn golden_namespace() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let history = repo.history(&Branch::local(refname!("master")))?;
-

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

-
    repo.switch_namespace(&refname!("golden"))?;
-

-
    assert_eq!(repo.which_namespace().unwrap(), Some("golden".parse()?));
-

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

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

-
    assert_eq!(expected_branches, branches);
-

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

-
    assert_eq!(expected_branches, branches);
-

-
    Ok(())
-
}
-

-
#[test]
-
fn silver_namespace() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let history = repo.history(&Branch::local(refname!("master")))?;
-

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

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

-
    let expected_branches: Vec<Branch> = vec![Branch::local(refname!("master"))];
-
    let mut branches = repo
-
        .branches(Glob::all_heads().branches().and(Glob::all_remotes()))?
-
        .collect::<Result<Vec<_>, _>>()?;
-
    branches.sort();
-

-
    assert_eq!(expected_branches, branches);
-

-
    Ok(())
-
}
deleted radicle-surf/t/src/git/reference.rs
@@ -1,55 +0,0 @@
-
use git_ref_format::refspec;
-
use radicle_surf::git::{Glob, Repository};
-

-
use super::GIT_PLATINUM;
-

-
#[test]
-
fn test_branches() {
-
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-
    let heads = Glob::all_heads();
-
    let branches = repo.branches(heads.clone()).unwrap();
-
    for b in branches {
-
        println!("{}", b.unwrap().refname());
-
    }
-
    let branches = repo
-
        .branches(
-
            heads
-
                .branches()
-
                .and(Glob::remotes(refspec::pattern!("banana/*"))),
-
        )
-
        .unwrap();
-
    for b in branches {
-
        println!("{}", b.unwrap().refname());
-
    }
-
}
-

-
#[test]
-
fn test_tag_snapshot() {
-
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-
    let tags = repo
-
        .tags(&Glob::all_tags())
-
        .unwrap()
-
        .collect::<Result<Vec<_>, _>>()
-
        .unwrap();
-
    assert_eq!(tags.len(), 6);
-
    let root_dir = repo.root_dir(&tags[0]).unwrap();
-
    assert_eq!(root_dir.entries(&repo).unwrap().entries().count(), 1);
-
}
-

-
#[test]
-
fn test_namespaces() {
-
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-

-
    let namespaces = repo.namespaces(&Glob::all_namespaces()).unwrap();
-
    assert_eq!(namespaces.count(), 3);
-
    let namespaces = repo
-
        .namespaces(&Glob::namespaces(refspec::pattern!("golden/*")))
-
        .unwrap();
-
    assert_eq!(namespaces.count(), 2);
-
    let namespaces = repo
-
        .namespaces(
-
            &Glob::namespaces(refspec::pattern!("golden/*")).insert(refspec::pattern!("me/*")),
-
        )
-
        .unwrap();
-
    assert_eq!(namespaces.count(), 3);
-
}
deleted radicle-surf/t/src/git/rev.rs
@@ -1,92 +0,0 @@
-
use std::str::FromStr;
-

-
use git_ref_format::{name::component, refname};
-
use radicle_surf::git::{Branch, Error, Oid, Repository};
-

-
use super::GIT_PLATINUM;
-

-
// **FIXME**: This seems to break occasionally on
-
// buildkite. For some reason the commit
-
// 3873745c8f6ffb45c990eb23b491d4b4b6182f95, which is on master
-
// (currently HEAD), is not found. It seems to load the history
-
// with d6880352fc7fda8f521ae9b7357668b17bb5bad5 as the HEAD.
-
//
-
// To temporarily fix this, we need to select "New Build" from the build kite
-
// build page that's failing.
-
// * Under "Message" put whatever you want.
-
// * Under "Branch" put in the branch you're working on.
-
// * Expand "Options" and select "clean checkout".
-
#[test]
-
fn _master() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let mut history = repo.history(&Branch::remote(component!("origin"), refname!("master")))?;
-

-
    let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
    assert!(
-
        history.any(|commit| commit.unwrap().id == commit1),
-
        "commit_id={}, history =\n{:#?}",
-
        commit1,
-
        &history
-
    );
-

-
    let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
-
    assert!(
-
        history.any(|commit| commit.unwrap().id == commit2),
-
        "commit_id={}, history =\n{:#?}",
-
        commit2,
-
        &history
-
    );
-

-
    Ok(())
-
}
-

-
#[test]
-
fn commit() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
    let mut history = repo.history(rev)?;
-

-
    let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
    assert!(history.any(|commit| commit.unwrap().id == commit1));
-

-
    Ok(())
-
}
-

-
#[test]
-
fn commit_parents() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
    let history = repo.history(rev)?;
-
    let commit = history.head();
-

-
    assert_eq!(
-
        commit.parents,
-
        vec![Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?]
-
    );
-

-
    Ok(())
-
}
-

-
#[test]
-
fn commit_short() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let rev = repo.oid("3873745c8")?;
-
    let mut history = repo.history(rev)?;
-

-
    let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
    assert!(history.any(|commit| commit.unwrap().id == commit1));
-

-
    Ok(())
-
}
-

-
#[test]
-
fn tag() -> Result<(), Error> {
-
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let rev = refname!("refs/tags/v0.2.0");
-
    let history = repo.history(&rev)?;
-

-
    let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
-
    assert_eq!(history.head().id, commit1);
-

-
    Ok(())
-
}
deleted radicle-surf/t/src/git/submodule.rs
@@ -1,10 +0,0 @@
-
#[cfg(not(feature = "gh-actions"))]
-
#[test]
-
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
-
fn test_submodule_failure() {
-
    use git_ref_format::refname;
-
    use radicle_surf::git::{Branch, Repository};
-

-
    let repo = Repository::discover(".").unwrap();
-
    repo.root_dir(Branch::local(refname!("main"))).unwrap();
-
}
deleted radicle-surf/t/src/git/threading.rs
@@ -1,33 +0,0 @@
-
use std::sync::{Mutex, MutexGuard};
-

-
use git_ref_format::{name::component, refname};
-
use radicle_surf::git::{Branch, Error, Glob, Repository};
-

-
use super::GIT_PLATINUM;
-

-
#[test]
-
fn basic_test() -> Result<(), Error> {
-
    let shared_repo = Mutex::new(Repository::open(GIT_PLATINUM)?);
-
    let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
-
    let mut branches = locked_repo
-
        .branches(Glob::all_heads().branches().and(Glob::all_remotes()))?
-
        .collect::<Result<Vec<_>, _>>()?;
-
    branches.sort();
-

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

-
    Ok(())
-
}
added radicle-surf/t/src/last_commit.rs
@@ -0,0 +1,120 @@
+
use std::{path::PathBuf, str::FromStr};
+

+
use git_ref_format::refname;
+
use radicle_git_ext::Oid;
+
use radicle_surf::{Branch, Repository};
+

+
use super::GIT_PLATINUM;
+

+
#[test]
+
fn readme_missing_and_memory() {
+
    let repo = Repository::open(GIT_PLATINUM)
+
        .expect("Could not retrieve ./data/git-platinum as git repository");
+
    let oid =
+
        Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");
+

+
    // memory.rs is commited later so it should not exist here.
+
    let memory_last_commit_oid = repo
+
        .last_commit(&"src/memory.rs", oid)
+
        .expect("Failed to get last commit")
+
        .map(|commit| commit.id);
+

+
    assert_eq!(memory_last_commit_oid, None);
+

+
    // README.md exists in this commit.
+
    let readme_last_commit = repo
+
        .last_commit(&"README.md", oid)
+
        .expect("Failed to get last commit")
+
        .map(|commit| commit.id);
+

+
    assert_eq!(readme_last_commit, Some(oid));
+
}
+

+
#[test]
+
fn folder_svelte() {
+
    let repo = Repository::open(GIT_PLATINUM)
+
        .expect("Could not retrieve ./data/git-platinum as git repository");
+
    // Check that last commit is the actual last commit even if head commit differs.
+
    let oid =
+
        Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
+

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

+
    let folder_svelte = repo
+
        .last_commit(&"examples/Folder.svelte", oid)
+
        .expect("Failed to get last commit")
+
        .map(|commit| commit.id);
+

+
    assert_eq!(folder_svelte, Some(expected_commit_id));
+
}
+

+
#[test]
+
fn nest_directory() {
+
    let repo = Repository::open(GIT_PLATINUM)
+
        .expect("Could not retrieve ./data/git-platinum as git repository");
+
    // Check that last commit is the actual last commit even if head commit differs.
+
    let oid =
+
        Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
+

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

+
    let nested_directory_tree_commit_id = repo
+
        .last_commit(&"this/is/a/really/deeply/nested/directory/tree", oid)
+
        .expect("Failed to get last commit")
+
        .map(|commit| commit.id);
+

+
    assert_eq!(nested_directory_tree_commit_id, Some(expected_commit_id));
+
}
+

+
#[test]
+
#[cfg(not(windows))]
+
fn can_get_last_commit_for_special_filenames() {
+
    let repo = Repository::open(GIT_PLATINUM)
+
        .expect("Could not retrieve ./data/git-platinum as git repository");
+

+
    // Check that last commit is the actual last commit even if head commit differs.
+
    let oid =
+
        Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA");
+

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

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

+
    let ogre_commit_id = repo
+
        .last_commit(&"special/👹👹👹", oid)
+
        .expect("Failed to get last commit")
+
        .map(|commit| commit.id);
+
    assert_eq!(ogre_commit_id, Some(expected_commit_id));
+
}
+

+
#[test]
+
fn root() {
+
    let repo = Repository::open(GIT_PLATINUM)
+
        .expect("Could not retrieve ./data/git-platinum as git repository");
+
    let rev = Branch::local(refname!("master"));
+
    let root_last_commit_id = repo
+
        .last_commit(&PathBuf::new(), rev)
+
        .expect("Failed to get last commit")
+
        .map(|commit| commit.id);
+

+
    let expected_oid = repo
+
        .history(&Branch::local(refname!("master")))
+
        .unwrap()
+
        .head()
+
        .id;
+
    assert_eq!(root_last_commit_id, Some(expected_oid));
+
}
+

+
#[test]
+
fn binary_file() {
+
    let repo = Repository::open(GIT_PLATINUM)
+
        .expect("Could not retrieve ./data/git-platinum as git repository");
+
    let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
+
    let file_commit = history.by_path(&"bin/cat").next();
+
    assert!(file_commit.is_some());
+
    println!("file commit: {:?}", &file_commit);
+
}
modified radicle-surf/t/src/lib.rs
@@ -2,10 +2,40 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#[cfg(test)]
-
mod git;
+
const GIT_PLATINUM: &str = "../data/git-platinum";

#[cfg(test)]
mod file_system;

#[cfg(test)]
mod source;
+

+
#[cfg(test)]
+
mod branch;
+

+
#[cfg(test)]
+
mod code_browsing;
+

+
#[cfg(test)]
+
mod commit;
+

+
#[cfg(test)]
+
mod diff;
+

+
#[cfg(test)]
+
mod last_commit;
+

+
#[cfg(test)]
+
mod namespace;
+

+
#[cfg(test)]
+
mod reference;
+

+
#[cfg(test)]
+
mod rev;
+

+
#[cfg(test)]
+
mod submodule;
+

+
#[cfg(test)]
+
mod threading;
added radicle-surf/t/src/namespace.rs
@@ -0,0 +1,121 @@
+
use git_ref_format::{name::component, refname, refspec};
+
use pretty_assertions::{assert_eq, assert_ne};
+
use radicle_surf::{Branch, Error, Glob, Repository};
+

+
use super::GIT_PLATINUM;
+

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

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

+
    Ok(())
+
}
+

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

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

+
    repo.switch_namespace(&refname!("me"))?;
+
    assert_eq!(repo.which_namespace().unwrap(), Some("me".parse()?));
+

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

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

+
    assert_eq!(expected_branches, branches);
+

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

+
    assert_eq!(expected_branches, branches);
+

+
    Ok(())
+
}
+

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

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

+
    repo.switch_namespace(&refname!("golden"))?;
+

+
    assert_eq!(repo.which_namespace().unwrap(), Some("golden".parse()?));
+

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

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

+
    assert_eq!(expected_branches, branches);
+

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

+
    assert_eq!(expected_branches, branches);
+

+
    Ok(())
+
}
+

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

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

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

+
    let expected_branches: Vec<Branch> = vec![Branch::local(refname!("master"))];
+
    let mut branches = repo
+
        .branches(Glob::all_heads().branches().and(Glob::all_remotes()))?
+
        .collect::<Result<Vec<_>, _>>()?;
+
    branches.sort();
+

+
    assert_eq!(expected_branches, branches);
+

+
    Ok(())
+
}
added radicle-surf/t/src/reference.rs
@@ -0,0 +1,55 @@
+
use git_ref_format::refspec;
+
use radicle_surf::{Glob, Repository};
+

+
use super::GIT_PLATINUM;
+

+
#[test]
+
fn test_branches() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+
    let heads = Glob::all_heads();
+
    let branches = repo.branches(heads.clone()).unwrap();
+
    for b in branches {
+
        println!("{}", b.unwrap().refname());
+
    }
+
    let branches = repo
+
        .branches(
+
            heads
+
                .branches()
+
                .and(Glob::remotes(refspec::pattern!("banana/*"))),
+
        )
+
        .unwrap();
+
    for b in branches {
+
        println!("{}", b.unwrap().refname());
+
    }
+
}
+

+
#[test]
+
fn test_tag_snapshot() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+
    let tags = repo
+
        .tags(&Glob::all_tags())
+
        .unwrap()
+
        .collect::<Result<Vec<_>, _>>()
+
        .unwrap();
+
    assert_eq!(tags.len(), 6);
+
    let root_dir = repo.root_dir(&tags[0]).unwrap();
+
    assert_eq!(root_dir.entries(&repo).unwrap().entries().count(), 1);
+
}
+

+
#[test]
+
fn test_namespaces() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+

+
    let namespaces = repo.namespaces(&Glob::all_namespaces()).unwrap();
+
    assert_eq!(namespaces.count(), 3);
+
    let namespaces = repo
+
        .namespaces(&Glob::namespaces(refspec::pattern!("golden/*")))
+
        .unwrap();
+
    assert_eq!(namespaces.count(), 2);
+
    let namespaces = repo
+
        .namespaces(
+
            &Glob::namespaces(refspec::pattern!("golden/*")).insert(refspec::pattern!("me/*")),
+
        )
+
        .unwrap();
+
    assert_eq!(namespaces.count(), 3);
+
}
added radicle-surf/t/src/rev.rs
@@ -0,0 +1,92 @@
+
use std::str::FromStr;
+

+
use git_ref_format::{name::component, refname};
+
use radicle_surf::{Branch, Error, Oid, Repository};
+

+
use super::GIT_PLATINUM;
+

+
// **FIXME**: This seems to break occasionally on
+
// buildkite. For some reason the commit
+
// 3873745c8f6ffb45c990eb23b491d4b4b6182f95, which is on master
+
// (currently HEAD), is not found. It seems to load the history
+
// with d6880352fc7fda8f521ae9b7357668b17bb5bad5 as the HEAD.
+
//
+
// To temporarily fix this, we need to select "New Build" from the build kite
+
// build page that's failing.
+
// * Under "Message" put whatever you want.
+
// * Under "Branch" put in the branch you're working on.
+
// * Expand "Options" and select "clean checkout".
+
#[test]
+
fn _master() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let mut history = repo.history(&Branch::remote(component!("origin"), refname!("master")))?;
+

+
    let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
    assert!(
+
        history.any(|commit| commit.unwrap().id == commit1),
+
        "commit_id={}, history =\n{:#?}",
+
        commit1,
+
        &history
+
    );
+

+
    let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
+
    assert!(
+
        history.any(|commit| commit.unwrap().id == commit2),
+
        "commit_id={}, history =\n{:#?}",
+
        commit2,
+
        &history
+
    );
+

+
    Ok(())
+
}
+

+
#[test]
+
fn commit() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
    let mut history = repo.history(rev)?;
+

+
    let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
    assert!(history.any(|commit| commit.unwrap().id == commit1));
+

+
    Ok(())
+
}
+

+
#[test]
+
fn commit_parents() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
    let history = repo.history(rev)?;
+
    let commit = history.head();
+

+
    assert_eq!(
+
        commit.parents,
+
        vec![Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?]
+
    );
+

+
    Ok(())
+
}
+

+
#[test]
+
fn commit_short() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let rev = repo.oid("3873745c8")?;
+
    let mut history = repo.history(rev)?;
+

+
    let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
    assert!(history.any(|commit| commit.unwrap().id == commit1));
+

+
    Ok(())
+
}
+

+
#[test]
+
fn tag() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let rev = refname!("refs/tags/v0.2.0");
+
    let history = repo.history(&rev)?;
+

+
    let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
+
    assert_eq!(history.head().id, commit1);
+

+
    Ok(())
+
}
modified radicle-surf/t/src/source.rs
@@ -1,7 +1,7 @@
use std::path::PathBuf;

use git_ref_format::refname;
-
use radicle_surf::git::Repository;
+
use radicle_surf::Repository;
use serde_json::json;

const GIT_PLATINUM: &str = "../data/git-platinum";
added radicle-surf/t/src/submodule.rs
@@ -0,0 +1,10 @@
+
#[cfg(not(feature = "gh-actions"))]
+
#[test]
+
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
+
fn test_submodule_failure() {
+
    use git_ref_format::refname;
+
    use radicle_surf::{Branch, Repository};
+

+
    let repo = Repository::discover(".").unwrap();
+
    repo.root_dir(Branch::local(refname!("main"))).unwrap();
+
}
added radicle-surf/t/src/threading.rs
@@ -0,0 +1,33 @@
+
use std::sync::{Mutex, MutexGuard};
+

+
use git_ref_format::{name::component, refname};
+
use radicle_surf::{Branch, Error, Glob, Repository};
+

+
use super::GIT_PLATINUM;
+

+
#[test]
+
fn basic_test() -> Result<(), Error> {
+
    let shared_repo = Mutex::new(Repository::open(GIT_PLATINUM)?);
+
    let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
+
    let mut branches = locked_repo
+
        .branches(Glob::all_heads().branches().and(Glob::all_remotes()))?
+
        .collect::<Result<Vec<_>, _>>()?;
+
    branches.sort();
+

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

+
    Ok(())
+
}