Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge pull request #26 from keepsimple1/new-design
keepsimple1 committed 3 years ago
commit 0cec6b59974c35a86946fed3a2ae2c399fd5f6ab
parent 93d92ed
13 files changed +414 -1387
modified radicle-surf/benches/last_commit.rs
@@ -18,14 +18,13 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use radicle_surf::{
    file_system::{unsound, Path},
-
    vcs::git::{Branch, Browser, Repository},
+
    vcs::git::{Branch, Repository, Rev},
};

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

    let mut group = c.benchmark_group("Last Commit");
    for path in [
@@ -36,7 +35,7 @@ fn last_commit_comparison(c: &mut Criterion) {
    .iter()
    {
        group.bench_with_input(BenchmarkId::new("", path), path, |b, path| {
-
            b.iter(|| browser.last_commit(path.clone()))
+
            b.iter(|| repo.as_ref().last_commit(path.clone(), &rev))
        });
    }
}
added radicle-surf/docs/refactor-design.md
@@ -0,0 +1,47 @@
+
# An updated design for radicle-surf
+

+
Now we have ported the `radicle-surf` crate from its own github repo to be part of the `radicle-git` repo. We are taking this opportunity to refactor its design as well. Intuitively, `radicle-surf` provides an API so that one can use it to create a github-like UI for a git repo:
+

+
- Given a commit (or other types of ref), list the content: i.e. files and directories.
+
- Generate a diff between two commits.
+
- List the history of the commits.
+
- List refs: Branches, Tags.
+

+
The main goals of the changes are:
+

+
- make API simpler whenever possible.
+
- address open issues in the original `radicle-surf` repo as much as possible.
+
- not to shy away from being `git` specific. (i.e. not to consider supporting other VCS systems)
+

+
## Make API simpler
+

+
The current API has quite a bit accidental complexity that is not inherent with the requirements, especially when we can be git specific and don't care about other VCS systems.
+

+
### Remove the `Browser`
+

+
The type `Browser` is awkward as of today:
+

+
- it is not a source of truth of any information. For example, `list_branches` method is just a wrapper of `Repository::list_branches`.
+
- it takes in `History`, but really works at the `Snapshot` level.
+
- it is mutable but its state does not help much.
+

+
Can we just remove `Browser` and implement its functionalities using other types?
+

+
- For iteratoring the history, use `History`.
+
- For generating `Directory`, use `Repository` directly given a `Rev`.
+
- For accessing `Branch`, `Tag` or `Commit`, use `Repository`.
+

+
## Remove the `Snapshot`
+

+
A `Snapshot` should be really just a tree (or `Directory`) of a `Commit` in git. Currently it is a function that returns a `Directory`. Because it is OK to be git specific, we don't need to have this generic function to create a snapshot across different VCS systems.
+

+
The only `snapshot` function defined currently:
+

+
```Rust
+
let snapshot = Box::new(|repository: &RepositoryRef<'a>, history: &History| {
+
            let tree = Self::get_tree(repository.repo_ref, history.0.first())?;
+
            Ok(directory::Directory::from_hash_map(tree))
+
        });
+
```
+

+
The above function can be easily implement as a method of `Repository`.
modified radicle-surf/examples/diff.rs
@@ -19,37 +19,20 @@ extern crate radicle_surf;

use std::{env::Args, str::FromStr, time::Instant};

-
use nonempty::NonEmpty;
-

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

fn main() {
    let options = get_options_or_exit();
    let repo = init_repository_or_exit(&options.path_to_repo);
-
    let mut browser =
-
        git::Browser::new(&repo, git::Branch::local("master")).expect("failed to create browser:");
-

-
    match options.head_revision {
-
        HeadRevision::Head => {
-
            reset_browser_to_head_or_exit(&mut browser);
-
        },
-
        HeadRevision::Commit(id) => {
-
            set_browser_history_or_exit(&mut browser, &id);
-
        },
-
    }
-
    let head_directory = get_directory_or_exit(&browser);
-

-
    set_browser_history_or_exit(&mut browser, &options.base_revision);
-
    let base_directory = get_directory_or_exit(&browser);
-

+
    let head_oid = match options.head_revision {
+
        HeadRevision::Head => repo.as_ref().head_oid().unwrap(),
+
        HeadRevision::Commit(id) => Oid::from_str(&id).unwrap(),
+
    };
+
    let base_oid = Oid::from_str(&options.base_revision).unwrap();
    let now = Instant::now();
    let elapsed_nanos = now.elapsed().as_nanos();
-
    let diff = Diff::diff(base_directory, head_directory);
+
    let diff = repo.as_ref().diff(base_oid, head_oid).unwrap();
    print_diff_summary(&diff, elapsed_nanos);
}

@@ -73,46 +56,6 @@ fn init_repository_or_exit(path_to_repo: &str) -> git::Repository {
    }
}

-
fn reset_browser_to_head_or_exit(browser: &mut git::Browser) {
-
    if let Err(e) = browser.head() {
-
        println!("Failed to set browser to HEAD: {:?}", e);
-
        std::process::exit(1);
-
    }
-
}
-

-
fn set_browser_history_or_exit(browser: &mut git::Browser, commit_id: &str) {
-
    // TODO: Might consider to not require resetting to HEAD when history is not at
-
    // HEAD
-
    reset_browser_to_head_or_exit(browser);
-
    if let Err(e) = set_browser_history(browser, commit_id) {
-
        println!("Failed to set browser history: {:?}", e);
-
        std::process::exit(1);
-
    }
-
}
-

-
fn set_browser_history(browser: &mut git::Browser, commit_id: &str) -> Result<(), String> {
-
    let oid = match Oid::from_str(commit_id) {
-
        Ok(oid) => oid,
-
        Err(e) => return Err(format!("{}", e)),
-
    };
-
    let commit = match browser.get().find_in_history(&oid, |artifact| artifact.id) {
-
        Some(commit) => commit,
-
        None => return Err(format!("Git commit not found: {}", commit_id)),
-
    };
-
    browser.set(History(NonEmpty::new(commit)));
-
    Ok(())
-
}
-

-
fn get_directory_or_exit(browser: &git::Browser) -> Directory {
-
    match browser.get_directory() {
-
        Ok(dir) => dir,
-
        Err(e) => {
-
            println!("Failed to get directory: {:?}", e);
-
            std::process::exit(1)
-
        },
-
    }
-
}
-

fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {
    diff.created.iter().for_each(|created| {
        println!("+++ {}", created.path);
@@ -131,7 +74,7 @@ fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {
        diff.modified.len(),
        diff.created.len() + diff.deleted.len() + diff.modified.len()
    );
-
    println!("diff took {} micros ", elapsed_nanos / 1000);
+
    println!("diff took {} nanos ", elapsed_nanos);
}

struct Options {
modified radicle-surf/src/commit.rs
@@ -30,7 +30,10 @@ use crate::{
    file_system,
    person::Person,
    revision::Revision,
-
    vcs::git::{self, BranchName, Browser, Rev},
+
    vcs::{
+
        git::{self, BranchName, RepositoryRef, Rev},
+
        Vcs,
+
    },
};

use radicle_git_ext::Oid;
@@ -141,16 +144,12 @@ pub struct Commits {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
-
pub fn commit(browser: &mut Browser<'_>, sha1: Oid) -> Result<Commit, Error> {
-
    browser.commit(sha1)?;
-

-
    let history = browser.get();
-
    let commit = history.first();
-

+
pub fn commit(repo: &RepositoryRef, sha1: Oid) -> Result<Commit, Error> {
+
    let commit = repo.get_commit(sha1)?;
    let diff = if let Some(parent) = commit.parents.first() {
-
        browser.diff(*parent, sha1)?
+
        repo.diff(*parent, sha1)?
    } else {
-
        browser.initial_diff(sha1)?
+
        repo.initial_diff(sha1)?
    };

    let mut deletions = 0;
@@ -194,14 +193,14 @@ pub fn commit(browser: &mut Browser<'_>, sha1: Oid) -> Result<Commit, Error> {
        }
    }

-
    let branches = browser
-
        .revision_branches(sha1)?
+
    let branches = repo
+
        .revision_branches(&sha1)?
        .into_iter()
        .map(|b| b.name)
        .collect();

    Ok(Commit {
-
        header: Header::from(commit),
+
        header: Header::from(&commit),
        stats: Stats {
            additions,
            deletions,
@@ -217,13 +216,9 @@ pub fn commit(browser: &mut Browser<'_>, sha1: Oid) -> Result<Commit, Error> {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
-
pub fn header(browser: &mut Browser<'_>, sha1: Oid) -> Result<Header, Error> {
-
    browser.commit(sha1)?;
-

-
    let history = browser.get();
-
    let commit = history.first();
-

-
    Ok(Header::from(commit))
+
pub fn header(repo: &RepositoryRef, sha1: Oid) -> Result<Header, Error> {
+
    let commit = repo.get_commit(sha1)?;
+
    Ok(Header::from(&commit))
}

/// Retrieves the [`Commit`] history for the given `revision`.
@@ -233,7 +228,7 @@ pub fn header(browser: &mut Browser<'_>, sha1: Oid) -> Result<Header, Error> {
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
pub fn commits<P>(
-
    browser: &mut Browser<'_>,
+
    repo: &RepositoryRef,
    maybe_revision: Option<Revision<P>>,
) -> Result<Commits, Error>
where
@@ -241,12 +236,13 @@ where
{
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;

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

-
    let headers = browser.get().iter().map(Header::from).collect();
-
    let stats = browser.get_stats()?;
+
    let stats = repo.get_stats(&rev)?;
+
    let headers = repo.get_history(rev)?.iter().map(Header::from).collect();

    Ok(Commits { headers, stats })
}
modified radicle-surf/src/object.rs
@@ -25,7 +25,7 @@ use serde::{
};

pub mod blob;
-
pub use blob::{blob, Blob, BlobContent};
+
pub use blob::{Blob, BlobContent};

pub mod tree;
pub use tree::{tree, Tree, TreeEntry};
modified radicle-surf/src/object/blob.rs
@@ -32,9 +32,10 @@ use serde::{
use crate::{
    commit,
    file_system,
+
    git::RepositoryRef,
    object::{Error, Info, ObjectType},
    revision::Revision,
-
    vcs::git::{Browser, Rev},
+
    vcs::git::Rev,
};

#[cfg(feature = "syntax")]
@@ -118,18 +119,18 @@ impl Serialize for BlobContent {
/// Will return [`Error`] if the project doesn't exist or a surf interaction
/// fails.
pub fn blob<P>(
-
    browser: &mut Browser,
+
    repo: &RepositoryRef,
    maybe_revision: Option<Revision<P>>,
    path: &str,
) -> Result<Blob, Error>
where
    P: ToString,
{
-
    make_blob(browser, maybe_revision, path, content)
+
    make_blob(repo, maybe_revision, path, content)
}

fn make_blob<P, C>(
-
    browser: &mut Browser,
+
    repo: &RepositoryRef,
    maybe_revision: Option<Revision<P>>,
    path: &str,
    content: C,
@@ -139,11 +140,9 @@ where
    C: FnOnce(&[u8]) -> BlobContent,
{
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
-
    if let Some(revision) = maybe_revision {
-
        browser.rev(revision)?;
-
    }
+
    let revision = maybe_revision.unwrap();

-
    let root = browser.get_directory()?;
+
    let root = repo.snapshot(&revision)?;
    let p = file_system::Path::from_str(path)?;

    let file = root
@@ -153,8 +152,8 @@ where
    let mut commit_path = file_system::Path::root();
    commit_path.append(p.clone());

-
    let last_commit = browser
-
        .last_commit(commit_path)?
+
    let last_commit = repo
+
        .last_commit(commit_path, &revision)?
        .map(|c| commit::Header::from(&c));
    let (_rest, last) = p.split_last();

modified radicle-surf/src/object/tree.rs
@@ -29,9 +29,13 @@ use serde::{
use crate::{
    commit,
    file_system,
+
    git::RepositoryRef,
    object::{Error, Info, ObjectType},
    revision::Revision,
-
    vcs::git::{Browser, Rev},
+
    vcs::{
+
        git::{Branch, Rev},
+
        Vcs,
+
    },
};

/// Result of a directory listing, carries other trees and blobs.
@@ -86,7 +90,7 @@ impl Serialize for TreeEntry {
///
/// Will return [`Error`] if any of the surf interactions fail.
pub fn tree<P>(
-
    browser: &mut Browser<'_>,
+
    repo: &RepositoryRef,
    maybe_revision: Option<Revision<P>>,
    maybe_prefix: Option<String>,
) -> Result<Tree, Error>
@@ -96,9 +100,10 @@ where
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
    let prefix = maybe_prefix.unwrap_or_default();

-
    if let Some(revision) = maybe_revision {
-
        browser.rev(revision)?;
-
    }
+
    let rev = match maybe_revision {
+
        Some(r) => r,
+
        None => Branch::local("main").into(),
+
    };

    let path = if prefix == "/" || prefix.is_empty() {
        file_system::Path::root()
@@ -106,7 +111,7 @@ where
        file_system::Path::from_str(&prefix)?
    };

-
    let root_dir = browser.get_directory()?;
+
    let root_dir = repo.snapshot(&rev)?;
    let prefix_dir = if path.is_root() {
        root_dir
    } else {
@@ -155,7 +160,7 @@ where
    entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));

    let last_commit = if path.is_root() {
-
        Some(commit::Header::from(browser.get().first()))
+
        Some(commit::Header::from(repo.get_history(rev).unwrap().first()))
    } else {
        None
    };
modified radicle-surf/src/revision.rs
@@ -27,8 +27,8 @@ use serde::{Deserialize, Serialize};
use radicle_git_ext::Oid;

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

/// Types of a peer.
@@ -122,14 +122,14 @@ pub struct Revisions<P, U> {
///
///   * If we cannot get the branches from the `Browser`
pub fn remote<P, U>(
-
    browser: &Browser,
+
    repo: &RepositoryRef,
    peer_id: P,
    user: U,
) -> Result<Option<Revisions<P, U>>, Error>
where
    P: Clone + ToString,
{
-
    let remote_branches = browser.branch_names(Some(peer_id.clone()).into())?;
+
    let remote_branches = repo.branch_names(Some(peer_id.clone()).into())?;
    Ok(
        NonEmpty::from_vec(remote_branches).map(|branches| Revisions {
            peer_id,
@@ -150,12 +150,16 @@ where
/// # Errors
///
///   * If we cannot get the branches from the `Browser`
-
pub fn local<P, U>(browser: &Browser, peer_id: P, user: U) -> Result<Option<Revisions<P, U>>, Error>
+
pub fn local<P, U>(
+
    repo: &RepositoryRef,
+
    peer_id: P,
+
    user: U,
+
) -> Result<Option<Revisions<P, U>>, Error>
where
    P: Clone + ToString,
{
-
    let local_branches = browser.branch_names(RefScope::Local)?;
-
    let tags = browser.tag_names()?;
+
    let local_branches = repo.branch_names(RefScope::Local)?;
+
    let tags = repo.tag_names()?;
    Ok(
        NonEmpty::from_vec(local_branches).map(|branches| Revisions {
            peer_id,
@@ -178,14 +182,14 @@ where
///
///   * If we cannot get the branches from the `Browser`
pub fn revisions<P, U>(
-
    browser: &Browser,
+
    repo: &RepositoryRef,
    peer: Category<P, U>,
) -> Result<Option<Revisions<P, U>>, Error>
where
    P: Clone + ToString,
{
    match peer {
-
        Category::Local { peer_id, user } => local(browser, peer_id, user),
-
        Category::Remote { peer_id, user } => remote(browser, peer_id, user),
+
        Category::Local { peer_id, user } => local(repo, peer_id, user),
+
        Category::Remote { peer_id, user } => remote(repo, peer_id, user),
    }
}
modified radicle-surf/src/vcs.rs
@@ -15,10 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

-
//! A model of a general VCS. The components consist of a [`History`], a
-
//! [`Browser`], and a [`Vcs`] trait.
+
//! A model of a general VCS. The components consist of a [`History`]
+
//! and a [`Vcs`] trait.

-
use crate::file_system::directory::Directory;
+
// use crate::file_system::directory::Directory;
use nonempty::NonEmpty;

pub mod git;
@@ -135,82 +135,6 @@ impl<A> History<A> {
    }
}

-
/// A Snapshot is a function that renders a `Directory` given
-
/// the `Repo` object and a `History` of artifacts.
-
type Snapshot<A, Repo, Error> = Box<dyn Fn(&Repo, &History<A>) -> Result<Directory, Error>>;
-

-
/// A `Browser` is a way of rendering a `History` into a
-
/// `Directory` snapshot, and the current `History` it is
-
/// viewing.
-
pub struct Browser<Repo, A, Error> {
-
    snapshot: Snapshot<A, Repo, Error>,
-
    history: History<A>,
-
    repository: Repo,
-
}
-

-
impl<Repo, A, Error> Browser<Repo, A, Error> {
-
    /// Get the current `History` the `Browser` is viewing.
-
    pub fn get(&self) -> History<A>
-
    where
-
        A: Clone,
-
    {
-
        self.history.clone()
-
    }
-

-
    /// Get the current `History` the `Browser` is viewing as a ref.
-
    pub fn as_history(&self) -> &History<A> {
-
        &self.history
-
    }
-

-
    /// Set the `History` the `Browser` should view.
-
    pub fn set(&mut self, history: History<A>) {
-
        self.history = history;
-
    }
-

-
    /// Render the `Directory` for this `Browser`.
-
    pub fn get_directory(&self) -> Result<Directory, Error> {
-
        (self.snapshot)(&self.repository, &self.history)
-
    }
-

-
    /// Modify the `History` in this `Browser`.
-
    pub fn modify<F>(&mut self, f: F)
-
    where
-
        F: Fn(&History<A>) -> History<A>,
-
    {
-
        self.history = f(&self.history)
-
    }
-

-
    /// Change the `Browser`'s view of `History` by modifying it, or
-
    /// using the default `History` provided if the operation fails.
-
    pub fn view_at<F>(&mut self, default_history: History<A>, f: F)
-
    where
-
        A: Clone,
-
        F: Fn(&History<A>) -> Option<History<A>>,
-
    {
-
        self.modify(|history| f(history).unwrap_or_else(|| default_history.clone()))
-
    }
-
}
-

-
impl<Repo, A, Error> Vcs<A, Error> for Browser<Repo, A, Error>
-
where
-
    Repo: Vcs<A, Error>,
-
{
-
    type HistoryId = Repo::HistoryId;
-
    type ArtefactId = Repo::ArtefactId;
-

-
    fn get_history(&self, identifier: Self::HistoryId) -> Result<History<A>, Error> {
-
        self.repository.get_history(identifier)
-
    }
-

-
    fn get_histories(&self) -> Result<Vec<History<A>>, Error> {
-
        self.repository.get_histories()
-
    }
-

-
    fn get_identifier(artifact: &A) -> Self::ArtefactId {
-
        Repo::get_identifier(artifact)
-
    }
-
}
-

pub(crate) trait GetVcs<Error>
where
    Self: Sized,
modified radicle-surf/src/vcs/git.rs
@@ -100,19 +100,6 @@ pub use stats::Stats;

pub use crate::diff::Diff;

-
use crate::{
-
    file_system,
-
    file_system::directory,
-
    vcs,
-
    vcs::{git::error::*, Vcs},
-
};
-
use nonempty::NonEmpty;
-
use std::{
-
    collections::{BTreeSet, HashMap},
-
    convert::TryFrom,
-
    str,
-
};
-

/// The signature of a commit
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Signature(Vec<u8>);
@@ -151,1033 +138,3 @@ where
        })
    }
}
-

-
/// A [`crate::vcs::Browser`] that uses [`Repository`] as the underlying
-
/// repository backend, [`git2::Commit`] as the artifact, and [`Error`] for
-
/// error reporting.
-
pub type Browser<'a> = vcs::Browser<RepositoryRef<'a>, Commit, Error>;
-

-
impl<'a> Browser<'a> {
-
    /// Create a new browser to interact with.
-
    ///
-
    /// The `revspec` provided will be used to kick off the [`History`] for this
-
    /// `Browser`.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Browser, Branch, Repository};
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let browser = Browser::new(&repo, Branch::local("master"))?;
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn new(
-
        repository: impl Into<RepositoryRef<'a>>,
-
        rev: impl Into<Rev>,
-
    ) -> Result<Self, Error> {
-
        let repository = repository.into();
-
        let history = repository.get_history(rev.into())?;
-
        Ok(Self::init(repository, history))
-
    }
-

-
    /// Create a new `Browser` that starts in a given `namespace`.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Browser, Repository, Branch, RefScope, BranchName, Namespace};
-
    /// use std::convert::TryFrom;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let browser = Browser::new_with_namespace(
-
    ///     &repo,
-
    ///     &Namespace::try_from("golden")?,
-
    ///     Branch::local("master")
-
    /// )?;
-
    ///
-
    /// let mut branches = browser.list_branches(RefScope::Local)?;
-
    /// branches.sort();
-
    ///
-
    /// assert_eq!(
-
    ///     branches,
-
    ///     vec![
-
    ///         Branch::local("banana"),
-
    ///         Branch::local("master"),
-
    ///     ]
-
    /// );
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn new_with_namespace(
-
        repository: impl Into<RepositoryRef<'a>>,
-
        namespace: &Namespace,
-
        rev: impl Into<Rev>,
-
    ) -> Result<Self, Error> {
-
        let repository = repository.into();
-
        // This is a bit weird, the references don't seem to all be present unless we
-
        // make a call to `references` o_O.
-
        let _ = repository.repo_ref.references()?;
-
        repository.switch_namespace(&namespace.to_string())?;
-
        let history = repository.get_history(rev.into())?;
-
        Ok(Self::init(repository, history))
-
    }
-

-
    fn init(repository: RepositoryRef<'a>, history: History) -> Self {
-
        let snapshot = Box::new(|repository: &RepositoryRef<'a>, history: &History| {
-
            let tree = Self::get_tree(repository.repo_ref, history.0.first())?;
-
            Ok(directory::Directory::from_hash_map(tree))
-
        });
-
        vcs::Browser {
-
            snapshot,
-
            history,
-
            repository,
-
        }
-
    }
-

-
    /// Switch the namespace you are browsing in. This will consume the previous
-
    /// `Browser` and give you back a new `Browser` for that particular
-
    /// namespace. The `revision` provided will kick-off the history for
-
    /// this `Browser`.
-
    pub fn switch_namespace(
-
        self,
-
        namespace: &Namespace,
-
        rev: impl Into<Ref>,
-
    ) -> Result<Self, Error> {
-
        self.repository.switch_namespace(&namespace.to_string())?;
-
        let history = self.get_history(Rev::from(rev))?;
-
        Ok(Browser {
-
            snapshot: self.snapshot,
-
            repository: self.repository,
-
            history,
-
        })
-
    }
-

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

-
    /// Set the current `Browser` history to the `HEAD` commit of the underlying
-
    /// repository.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Browser, Repository, Branch};
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// // ensure we're at HEAD
-
    /// browser.head();
-
    ///
-
    /// let directory = browser.get_directory();
-
    ///
-
    /// // We are able to render the directory
-
    /// assert!(directory.is_ok());
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn head(&mut self) -> Result<(), Error> {
-
        let history = self.repository.head()?;
-
        self.set(history);
-
        Ok(())
-
    }
-

-
    /// Set the current `Browser`'s [`History`] to the given [`BranchName`]
-
    /// provided.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    /// * [`error::Error::NotBranch`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Repository};
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// // ensure we're on 'master'
-
    /// browser.branch(Branch::local("master"));
-
    ///
-
    /// let directory = browser.get_directory();
-
    ///
-
    /// // We are able to render the directory
-
    /// assert!(directory.is_ok());
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Repository};
-
    /// use radicle_surf::file_system::{Label, Path, SystemType};
-
    /// use radicle_surf::file_system::unsound;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    /// browser.branch(Branch::remote("dev", "origin"))?;
-
    ///
-
    /// let directory = browser.get_directory()?;
-
    /// let mut directory_contents = directory.list_directory();
-
    /// directory_contents.sort();
-
    ///
-
    /// assert!(directory_contents.contains(
-
    ///     &SystemType::file(unsound::label::new("here-we-are-on-a-dev-branch.lol"))
-
    /// ));
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn branch(&mut self, branch: Branch) -> Result<(), Error> {
-
        let name = BranchName(branch.name());
-
        self.set(self.repository.reference(branch, |reference| {
-
            let is_branch = ext::is_branch(reference) || reference.is_remote();
-
            if !is_branch {
-
                Some(Error::NotBranch(name))
-
            } else {
-
                None
-
            }
-
        })?);
-
        Ok(())
-
    }
-

-
    /// Set the current `Browser`'s [`History`] to the [`TagName`] provided.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    /// * [`error::Error::NotTag`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use nonempty::NonEmpty;
-
    /// use radicle_surf::vcs::History;
-
    /// use radicle_surf::vcs::git::{TagName, Branch, Browser, Oid, Repository};
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// // Switch to "v0.3.0"
-
    /// browser.tag(TagName::new("v0.3.0"))?;
-
    ///
-
    /// let expected_history = History(NonEmpty::from((
-
    ///     Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97")?,
-
    ///     vec![
-
    ///         Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2")?,
-
    ///         Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?,
-
    ///         Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?,
-
    ///     ]
-
    /// )));
-
    ///
-
    /// let history_ids = browser.get().map(|commit| commit.id);
-
    ///
-
    /// // We are able to render the directory
-
    /// assert_eq!(history_ids, expected_history);
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn tag(&mut self, tag_name: TagName) -> Result<(), Error> {
-
        let name = tag_name.clone();
-
        self.set(self.repository.reference(tag_name, |reference| {
-
            if !ext::is_tag(reference) {
-
                Some(Error::NotTag(name))
-
            } else {
-
                None
-
            }
-
        })?);
-
        Ok(())
-
    }
-

-
    /// Set the current `Browser`'s [`History`] to the [`Oid`] (SHA digest)
-
    /// provided.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, SystemType};
-
    /// use radicle_surf::file_system::unsound;
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Oid, Repository};
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// // Set to the initial commit
-
    /// let commit = Oid::from_str("e24124b7538658220b5aaf3b6ef53758f0a106dc")?;
-
    ///
-
    /// browser.commit(commit)?;
-
    ///
-
    /// let directory = browser.get_directory()?;
-
    /// let mut directory_contents = directory.list_directory();
-
    ///
-
    /// assert_eq!(
-
    ///     directory_contents,
-
    ///     vec![
-
    ///         SystemType::file(unsound::label::new("README.md")),
-
    ///         SystemType::directory(unsound::label::new("bin")),
-
    ///         SystemType::directory(unsound::label::new("src")),
-
    ///         SystemType::directory(unsound::label::new("this")),
-
    ///     ]
-
    /// );
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn commit(&mut self, oid: Oid) -> Result<(), Error> {
-
        self.set(self.get_history(Rev::Oid(oid))?);
-
        Ok(())
-
    }
-

-
    /// Set a `Browser`'s [`History`] based on a [revspec](https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions).
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    /// * [`error::Error::RevParseFailure`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, SystemType};
-
    /// use radicle_surf::file_system::unsound;
-
    /// use radicle_surf::vcs::git::{Browser, Branch, Oid, Repository};
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// browser.rev(Branch::remote("dev", "origin"))?;
-
    ///
-
    /// let directory = browser.get_directory()?;
-
    /// let mut directory_contents = directory.list_directory();
-
    /// directory_contents.sort();
-
    ///
-
    /// assert!(directory_contents.contains(
-
    ///     &SystemType::file(unsound::label::new("here-we-are-on-a-dev-branch.lol"))
-
    /// ));
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn rev(&mut self, rev: impl Into<Rev>) -> Result<(), Error> {
-
        let history = self.get_history(rev.into())?;
-
        self.set(history);
-
        Ok(())
-
    }
-

-
    /// Parse an [`Oid`] from the given string. This is useful if we have a
-
    /// shorthand version of the `Oid`, as opposed to the full one.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Oid, Repository};
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// // Set to the initial commit
-
    /// let commit = Oid::from_str("e24124b7538658220b5aaf3b6ef53758f0a106dc")?;
-
    ///
-
    /// assert_eq!(
-
    ///     commit,
-
    ///     browser.oid("e24124b")?,
-
    /// );
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
-
        self.repository.oid(oid)
-
    }
-

-
    /// Get the [`Diff`] between two commits.
-
    pub fn diff(&self, from: Oid, to: Oid) -> Result<Diff, Error> {
-
        self.repository.diff(from, to)
-
    }
-

-
    /// Get the [`Diff`] of a commit with no parents.
-
    pub fn initial_diff(&self, oid: Oid) -> Result<Diff, Error> {
-
        self.repository.initial_diff(oid)
-
    }
-

-
    /// List the names of the _branches_ that are contained in the underlying
-
    /// [`Repository`].
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, RefScope, BranchName, Browser, Namespace, Repository};
-
    /// use std::convert::TryFrom;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// let branches = browser.list_branches(RefScope::All)?;
-
    ///
-
    /// // 'master' exists in the list of branches
-
    /// assert!(branches.contains(&Branch::local("master")));
-
    ///
-
    /// // Filter the branches by `Remote` 'origin'.
-
    /// let mut branches = browser.list_branches(RefScope::Remote {
-
    ///     name: Some("origin".to_string())
-
    /// })?;
-
    /// branches.sort();
-
    ///
-
    /// assert_eq!(branches, vec![
-
    ///     Branch::remote("HEAD", "origin"),
-
    ///     Branch::remote("dev", "origin"),
-
    ///     Branch::remote("master", "origin"),
-
    /// ]);
-
    ///
-
    /// // Filter the branches by all `Remote`s.
-
    /// let mut branches = browser.list_branches(RefScope::Remote {
-
    ///     name: None
-
    /// })?;
-
    /// branches.sort();
-
    ///
-
    /// assert_eq!(branches, vec![
-
    ///     Branch::remote("HEAD", "origin"),
-
    ///     Branch::remote("dev", "origin"),
-
    ///     Branch::remote("master", "origin"),
-
    ///     Branch::remote("orange/pineapple", "banana"),
-
    ///     Branch::remote("pineapple", "banana"),
-
    /// ]);
-
    ///
-
    /// // We can also switch namespaces and list the branches in that namespace.
-
    /// let golden = browser.switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?;
-
    ///
-
    /// let mut branches = golden.list_branches(RefScope::Local)?;
-
    /// branches.sort();
-
    ///
-
    /// assert_eq!(branches, vec![
-
    ///     Branch::local("banana"),
-
    ///     Branch::local("master"),
-
    /// ]);
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn list_branches(&self, filter: RefScope) -> Result<Vec<Branch>, Error> {
-
        self.repository.list_branches(filter)
-
    }
-

-
    /// Given a project id to a repo returns the list of branches.
-
    ///
-
    /// # Errors
-
    ///
-
    /// Will return [`Error`] if the project doesn't exist or the surf
-
    /// interaction fails.
-
    pub fn branch_names(&self, filter: RefScope) -> Result<Vec<BranchName>, Error> {
-
        let mut branches = self
-
            .list_branches(filter)?
-
            .into_iter()
-
            .map(|b| b.name)
-
            .collect::<Vec<BranchName>>();
-

-
        branches.sort();
-

-
        Ok(branches)
-
    }
-

-
    /// List the names of the _tags_ that are contained in the underlying
-
    /// [`Repository`].
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, RefScope, Browser, Namespace, Oid, Repository, Tag, TagName, Author, Time};
-
    /// use std::convert::TryFrom;
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// let tags = browser.list_tags(RefScope::Local)?;
-
    ///
-
    /// assert_eq!(
-
    ///     tags,
-
    ///     vec![
-
    ///         Tag::Light {
-
    ///             id: Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?,
-
    ///             name: TagName::new("v0.1.0"),
-
    ///             remote: None,
-
    ///         },
-
    ///         Tag::Light {
-
    ///             id: Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?,
-
    ///             name: TagName::new("v0.2.0"),
-
    ///             remote: None,
-
    ///         },
-
    ///         Tag::Light {
-
    ///             id: Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97")?,
-
    ///             name: TagName::new("v0.3.0"),
-
    ///             remote: None,
-
    ///         },
-
    ///         Tag::Light {
-
    ///             id: Oid::from_str("91b69e00cd8e5a07e20942e9e4457d83ce7a3ff1")?,
-
    ///             name: TagName::new("v0.4.0"),
-
    ///             remote: None,
-
    ///         },
-
    ///         Tag::Light {
-
    ///             id: Oid::from_str("80ded66281a4de2889cc07293a8f10947c6d57fe")?,
-
    ///             name: TagName::new("v0.5.0"),
-
    ///             remote: None,
-
    ///         },
-
    ///         Tag::Annotated {
-
    ///             id: Oid::from_str("4d1f4af2703074d37cb877f4fdbe36322c8e541d")?,
-
    ///             target_id: Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?,
-
    ///             name: TagName::new("v0.6.0"),
-
    ///             remote: None,
-
    ///             tagger: Some(Author {
-
    ///               name: "Thomas Scholtes".to_string(),
-
    ///               email: "thomas@monadic.xyz".to_string(),
-
    ///               time: Time::new(1620740737, 120),
-
    ///             }),
-
    ///             message: Some("An annotated tag message for v0.6.0\n".to_string())
-
    ///         },
-
    ///     ]
-
    /// );
-
    ///
-
    /// // We can also switch namespaces and list the branches in that namespace.
-
    /// let golden = browser.switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?;
-
    ///
-
    /// let branches = golden.list_tags(RefScope::Local)?;
-
    /// assert_eq!(branches, vec![
-
    ///     Tag::Light {
-
    ///         id: Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?,
-
    ///         name: TagName::new("v0.1.0"),
-
    ///         remote: None,
-
    ///     },
-
    ///     Tag::Light {
-
    ///         id: Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?,
-
    ///         name: TagName::new("v0.2.0"),
-
    ///         remote: None,
-
    ///     },
-
    /// ]);
-
    /// let golden = golden.switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?;
-
    ///
-
    /// let branches = golden.list_tags(RefScope::Remote { name: Some("kickflip".to_string()) })?;
-
    /// assert_eq!(branches, vec![
-
    ///     Tag::Light {
-
    ///         id: Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?,
-
    ///         name: TagName::new("v0.1.0"),
-
    ///         remote: Some("kickflip".to_string()),
-
    ///     },
-
    /// ]);
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn list_tags(&self, scope: RefScope) -> Result<Vec<Tag>, Error> {
-
        self.repository.list_tags(scope)
-
    }
-

-
    /// Returns a sorted list of [`TagName`] from the browser.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    pub fn tag_names(&self) -> Result<Vec<TagName>, Error> {
-
        let tag_names = self.list_tags(RefScope::Local)?;
-
        let mut tags: Vec<TagName> = tag_names
-
            .into_iter()
-
            .map(|tag_name| tag_name.name())
-
            .collect();
-

-
        tags.sort();
-

-
        Ok(tags)
-
    }
-

-
    /// List the namespaces within a `Browser`, filtering out ones that do not
-
    /// parse correctly.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, BranchType, BranchName, Browser, Namespace, Repository};
-
    /// use std::convert::TryFrom;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// let mut namespaces = browser.list_namespaces()?;
-
    /// namespaces.sort();
-
    ///
-
    /// assert_eq!(namespaces, vec![
-
    ///     Namespace::try_from("golden")?,
-
    ///     Namespace::try_from("golden/silver")?,
-
    ///     Namespace::try_from("me")?,
-
    /// ]);
-
    ///
-
    ///
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn list_namespaces(&self) -> Result<Vec<Namespace>, Error> {
-
        self.repository.list_namespaces()
-
    }
-

-
    /// Given a [`crate::file_system::Path`] to a file, return the last
-
    /// [`Commit`] that touched that file or directory.
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    /// * [`error::Error::LastCommitException`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Oid, Repository};
-
    /// use radicle_surf::file_system::{Label, Path, SystemType};
-
    /// use radicle_surf::file_system::unsound;
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// // Clamp the Browser to a particular commit
-
    /// let commit = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
-
    /// browser.commit(commit)?;
-
    ///
-
    /// let head_commit = browser.get().first().clone();
-
    /// let expected_commit = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
-
    ///
-
    /// let readme_last_commit = browser
-
    ///     .last_commit(Path::with_root(&[unsound::label::new("README.md")]))?
-
    ///     .map(|commit| commit.id);
-
    ///
-
    /// assert_eq!(readme_last_commit, Some(expected_commit));
-
    ///
-
    /// let expected_commit = Oid::from_str("e24124b7538658220b5aaf3b6ef53758f0a106dc")?;
-
    ///
-
    /// let memory_last_commit = browser
-
    ///     .last_commit(Path::with_root(&[unsound::label::new("src"), unsound::label::new("memory.rs")]))?
-
    ///     .map(|commit| commit.id);
-
    ///
-
    /// assert_eq!(memory_last_commit, Some(expected_commit));
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn last_commit(&self, path: file_system::Path) -> Result<Option<Commit>, Error> {
-
        let file_history = self.repository.file_history(
-
            &path,
-
            repo::CommitHistory::Last,
-
            self.get().first().clone(),
-
        )?;
-
        Ok(file_history.first().cloned())
-
    }
-

-
    /// Get the commit history for a file _or_ directory.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use nonempty::NonEmpty;
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Oid, Repository};
-
    /// use radicle_surf::file_system::{Label, Path, SystemType};
-
    /// use radicle_surf::file_system::unsound;
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// // Clamp the Browser to a particular commit
-
    /// let commit = Oid::from_str("223aaf87d6ea62eef0014857640fd7c8dd0f80b5")?;
-
    /// browser.commit(commit)?;
-
    ///
-
    /// let root_commits: Vec<Oid> = browser
-
    ///     .file_history(unsound::path::new("~"))?
-
    ///     .into_iter()
-
    ///     .map(|commit| commit.id)
-
    ///     .collect();
-
    ///
-
    /// assert_eq!(root_commits,
-
    ///     vec![
-
    ///         Oid::from_str("223aaf87d6ea62eef0014857640fd7c8dd0f80b5")?,
-
    ///         Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?,
-
    ///         Oid::from_str("a57846bbc8ced6587bf8329fc4bce970eb7b757e")?,
-
    ///         Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?,
-
    ///         Oid::from_str("80ded66281a4de2889cc07293a8f10947c6d57fe")?,
-
    ///         Oid::from_str("91b69e00cd8e5a07e20942e9e4457d83ce7a3ff1")?,
-
    ///         Oid::from_str("1820cb07c1a890016ca5578aa652fd4d4c38967e")?,
-
    ///         Oid::from_str("1e0206da8571ca71c51c91154e2fee376e09b4e7")?,
-
    ///         Oid::from_str("e24124b7538658220b5aaf3b6ef53758f0a106dc")?,
-
    ///         Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97")?,
-
    ///         Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2")?,
-
    ///         Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?,
-
    ///         Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?,
-
    ///     ]
-
    /// );
-
    ///
-
    /// let eval_commits: Vec<Oid> = browser
-
    ///     .file_history(unsound::path::new("~/src/Eval.hs"))?
-
    ///     .into_iter()
-
    ///     .map(|commit| commit.id)
-
    ///     .collect();
-
    ///
-
    /// assert_eq!(eval_commits,
-
    ///     vec![
-
    ///         Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?,
-
    ///         Oid::from_str("e24124b7538658220b5aaf3b6ef53758f0a106dc")?,
-
    ///     ]
-
    /// );
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn file_history(&self, path: file_system::Path) -> Result<Vec<Commit>, Error> {
-
        self.repository
-
            .file_history(&path, repo::CommitHistory::Full, self.get().first().clone())
-
    }
-

-
    /// Extract the signature for a commit
-
    ///
-
    /// # Arguments
-
    ///
-
    /// * `commit` - The commit to extract the signature for
-
    /// * `field` - the name of the header field containing the signature block;
-
    ///   pass `None` to extract the default 'gpgsig'
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Repository, Oid, error};
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// let commit_with_signature_oid = Oid::from_str(
-
    ///     "e24124b7538658220b5aaf3b6ef53758f0a106dc"
-
    /// )?;
-
    ///
-
    /// browser.commit(commit_with_signature_oid)?;
-
    /// let history = browser.get();
-
    /// let commit_with_signature = history.first();
-
    /// let signature = browser.extract_signature(commit_with_signature, None)?;
-
    ///
-
    /// // We have a signature
-
    /// assert!(signature.is_some());
-
    ///
-
    /// let commit_without_signature_oid = Oid::from_str(
-
    ///     "80bacafba303bf0cdf6142921f430ff265f25095"
-
    /// )?;
-
    ///
-
    /// browser.commit(commit_without_signature_oid)?;
-
    /// let history = browser.get();
-
    /// let commit_without_signature = history.first();
-
    /// let signature = browser.extract_signature(commit_without_signature, None)?;
-
    ///
-
    /// // There is no signature
-
    /// assert!(signature.is_none());
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn extract_signature(
-
        &self,
-
        commit: &Commit,
-
        field: Option<&str>,
-
    ) -> Result<Option<Signature>, Error> {
-
        self.repository.extract_signature(&commit.id, field)
-
    }
-

-
    /// List the [`Branch`]es, which contain the provided [`Commit`].
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Browser, Repository, Branch, BranchName, Namespace, Oid};
-
    /// use std::convert::TryFrom;
-
    /// use std::str::FromStr;
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    ///
-
    /// let branches = browser.revision_branches(Oid::from_str("27acd68c7504755aa11023300890bb85bbd69d45")?)?;
-
    /// assert_eq!(
-
    ///     branches,
-
    ///     vec![
-
    ///         Branch::local("dev"),
-
    ///         Branch::remote("dev", "origin"),
-
    ///     ]
-
    /// );
-
    ///
-
    /// // TODO(finto): I worry that this test will fail as other branches get added
-
    /// let mut branches = browser.revision_branches(Oid::from_str("1820cb07c1a890016ca5578aa652fd4d4c38967e")?)?;
-
    /// branches.sort();
-
    /// assert_eq!(
-
    ///     branches,
-
    ///     vec![
-
    ///         Branch::remote("HEAD", "origin"),
-
    ///         Branch::local("dev"),
-
    ///         Branch::remote("dev", "origin"),
-
    ///         Branch::local("master"),
-
    ///         Branch::remote("master", "origin"),
-
    ///         Branch::remote("orange/pineapple", "banana"),
-
    ///         Branch::remote("pineapple", "banana"),
-
    ///     ]
-
    /// );
-
    ///
-
    /// let golden_browser = browser.switch_namespace(&Namespace::try_from("golden")?,
-
    /// Branch::local("master"))?;
-
    ///
-
    /// let branches = golden_browser.revision_branches(Oid::from_str("27acd68c7504755aa11023300890bb85bbd69d45")?)?;
-
    /// assert_eq!(
-
    ///     branches,
-
    ///     vec![
-
    ///         Branch::local("banana"),
-
    ///         Branch::remote("fakie/bigspin", "kickflip"),
-
    ///         Branch::remote("heelflip", "kickflip"),
-
    ///     ]
-
    /// );
-
    /// #
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn revision_branches(&self, rev: impl Into<Rev>) -> Result<Vec<Branch>, Error> {
-
        let commit = self.repository.rev_to_commit(&rev.into())?;
-
        self.repository.revision_branches(&commit.id().into())
-
    }
-

-
    /// Get the [`Stats`] of the underlying [`Repository`].
-
    ///
-
    /// # Errors
-
    ///
-
    /// * [`error::Error::Git`]
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::vcs::git::{Branch, Browser, Repository};
-
    /// # use std::error::Error;
-
    ///
-
    /// # fn main() -> Result<(), Box<dyn Error>> {
-
    /// let repo = Repository::new("./data/git-platinum")?;
-
    /// let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
    ///
-
    /// let stats = browser.get_stats()?;
-
    ///
-
    /// assert_eq!(stats.branches, 2);
-
    ///
-
    /// assert_eq!(stats.commits, 15);
-
    ///
-
    /// assert_eq!(stats.contributors, 4);
-
    ///
-
    /// # Ok(())
-
    /// # }
-
    /// ```
-
    pub fn get_stats(&self) -> Result<Stats, Error> {
-
        let branches = self.list_branches(RefScope::Local)?.len();
-
        let commits = self.history.len();
-

-
        let contributors = self
-
            .history
-
            .iter()
-
            .cloned()
-
            .map(|commit| (commit.author.name, commit.author.email))
-
            .collect::<BTreeSet<_>>();
-

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

-
    /// Do a pre-order TreeWalk of the given commit. This turns a Tree
-
    /// into a HashMap of Paths and a list of Files. We can then turn that
-
    /// into a Directory.
-
    fn get_tree(
-
        repo: &git2::Repository,
-
        commit: &Commit,
-
    ) -> Result<HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>, Error>
-
    {
-
        let mut file_paths_or_error: Result<
-
            HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
-
            Error,
-
        > = Ok(HashMap::new());
-

-
        let commit = repo.find_commit(commit.id.into())?;
-
        let tree = commit.as_object().peel_to_tree()?;
-

-
        tree.walk(
-
            git2::TreeWalkMode::PreOrder,
-
            |s, entry| match Self::tree_entry_to_file_and_path(repo, s, entry) {
-
                Ok((path, name, file)) => {
-
                    match file_paths_or_error.as_mut() {
-
                        Ok(files) => Self::update_file_map(path, name, file, files),
-

-
                        // We don't need to update, we want to keep the error.
-
                        Err(_err) => {},
-
                    }
-
                    git2::TreeWalkResult::Ok
-
                },
-
                Err(err) => match err {
-
                    // We want to continue if the entry was not a Blob.
-
                    TreeWalkError::NotBlob => git2::TreeWalkResult::Ok,
-

-
                    // We found a ObjectType::Commit (likely a submodule) and
-
                    // so we can skip it.
-
                    TreeWalkError::Commit => git2::TreeWalkResult::Ok,
-

-
                    // But we want to keep the error and abort otherwise.
-
                    TreeWalkError::Git(err) => {
-
                        file_paths_or_error = Err(err);
-
                        git2::TreeWalkResult::Abort
-
                    },
-
                },
-
            },
-
        )?;
-

-
        file_paths_or_error
-
    }
-

-
    /// Find the best common ancestor between two commits if it exists.
-
    ///
-
    /// See [`git2::Repository::merge_base`] for details.
-
    pub fn merge_base(&self, one: Oid, two: Oid) -> Result<Option<Oid>, Error> {
-
        match self.repository.repo_ref.merge_base(one.into(), two.into()) {
-
            Ok(merge_base) => Ok(Some(merge_base.into())),
-
            Err(err) => {
-
                if err.code() == git2::ErrorCode::NotFound {
-
                    Ok(None)
-
                } else {
-
                    Err(Error::Git(err))
-
                }
-
            },
-
        }
-
    }
-

-
    fn update_file_map(
-
        path: file_system::Path,
-
        name: file_system::Label,
-
        file: directory::File,
-
        files: &mut HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
-
    ) {
-
        files
-
            .entry(path)
-
            .and_modify(|entries| entries.push((name.clone(), file.clone())))
-
            .or_insert_with(|| NonEmpty::new((name, file)));
-
    }
-

-
    fn tree_entry_to_file_and_path(
-
        repo: &git2::Repository,
-
        tree_path: &str,
-
        entry: &git2::TreeEntry,
-
    ) -> Result<(file_system::Path, file_system::Label, directory::File), TreeWalkError> {
-
        // Account for the "root" of git being the empty string
-
        let path = if tree_path.is_empty() {
-
            Ok(file_system::Path::root())
-
        } else {
-
            file_system::Path::try_from(tree_path)
-
        }?;
-

-
        // We found a Commit object in the Tree, likely a submodule.
-
        // We will skip this entry.
-
        if let Some(git2::ObjectType::Commit) = entry.kind() {
-
            return Err(TreeWalkError::Commit);
-
        }
-

-
        let object = entry.to_object(repo)?;
-
        let blob = object.as_blob().ok_or(TreeWalkError::NotBlob)?;
-
        let name = str::from_utf8(entry.name_bytes())?;
-

-
        let name = file_system::Label::try_from(name).map_err(Error::FileSystem)?;
-

-
        Ok((
-
            path,
-
            name,
-
            directory::File {
-
                contents: blob.content().to_owned(),
-
                size: blob.size(),
-
            },
-
        ))
-
    }
-
}
modified radicle-surf/src/vcs/git/namespace.rs
@@ -21,7 +21,7 @@ pub use radicle_git_ext::Oid;
use std::{convert::TryFrom, fmt, str};

/// A `Namespace` value allows us to switch the git namespace of
-
/// [`super::Browser`].
+
/// a repo.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Namespace {
    /// Since namespaces can be nested we have a vector of strings.
modified radicle-surf/src/vcs/git/repo.rs
@@ -18,29 +18,37 @@
use crate::{
    diff::*,
    file_system,
+
    file_system::directory,
    vcs,
    vcs::{
        git::{
            error::*,
            reference::{glob::RefGlob, Ref, Rev},
            Branch,
+
            BranchName,
            Commit,
            Namespace,
            RefScope,
            Signature,
+
            Stats,
            Tag,
+
            TagName,
        },
        Vcs,
    },
};
use nonempty::NonEmpty;
use radicle_git_ext::Oid;
-
use std::{collections::HashSet, convert::TryFrom, str};
+
use std::{
+
    collections::{BTreeSet, HashMap, HashSet},
+
    convert::TryFrom,
+
    str,
+
};

/// This is for flagging to the `file_history` function that it should
/// stop at the first (i.e. Last) commit it finds for a file.
pub(super) enum CommitHistory {
-
    Full,
+
    _Full,
    Last,
}

@@ -171,6 +179,81 @@ impl<'a> RepositoryRef<'a> {
        Ok(self.repo_ref.revparse_single(oid)?.id().into())
    }

+
    /// Gets a snapshot of the repo as a Directory.
+
    pub fn snapshot(&self, rev: &Rev) -> Result<directory::Directory, Error> {
+
        let commit = self.rev_to_commit(rev)?;
+
        let oid: Oid = commit.id().into();
+
        let tree = self.get_tree(&oid)?;
+
        Ok(directory::Directory::from_hash_map(tree))
+
    }
+

+
    /// Returns the last commit, if exists, for a `path` in the history of
+
    /// `rev`.
+
    pub fn last_commit(&self, path: file_system::Path, rev: &Rev) -> Result<Option<Commit>, Error> {
+
        let git2_commit = self.rev_to_commit(rev)?;
+
        let commit = Commit::try_from(git2_commit)?;
+
        let file_history = self.file_history(&path, CommitHistory::Last, commit)?;
+
        Ok(file_history.first().cloned())
+
    }
+

+
    /// Retrieves `Commit` identified by `oid`.
+
    pub fn get_commit(&self, oid: Oid) -> Result<Commit, Error> {
+
        let git2_commit = self.get_git2_commit(oid)?;
+
        Commit::try_from(git2_commit)
+
    }
+

+
    /// Gets stats of `rev`.
+
    pub fn get_stats(&self, rev: &Rev) -> Result<Stats, Error> {
+
        let branches = self.list_branches(RefScope::Local)?.len();
+
        let history = self.get_history(rev.clone())?;
+
        let commits = history.len();
+

+
        let contributors = history
+
            .iter()
+
            .cloned()
+
            .map(|commit| (commit.author.name, commit.author.email))
+
            .collect::<BTreeSet<_>>();
+

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

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

+
        branches.sort();
+

+
        Ok(branches)
+
    }
+

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

+
        tags.sort();
+

+
        Ok(tags)
+
    }
+

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

    pub(super) fn rev_to_commit(&self, rev: &Rev) -> Result<git2::Commit, Error> {
        match rev {
            Rev::Oid(oid) => Ok(self.repo_ref.find_commit((*oid).into())?),
@@ -178,7 +261,8 @@ impl<'a> RepositoryRef<'a> {
        }
    }

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

@@ -189,7 +273,7 @@ impl<'a> RepositoryRef<'a> {
    }

    /// Build a [`History`] using the `head` reference.
-
    pub(super) fn head(&self) -> Result<History, Error> {
+
    pub fn head_history(&self) -> Result<History, Error> {
        let head = self.repo_ref.head()?;
        self.to_history(&head)
    }
@@ -235,7 +319,7 @@ impl<'a> RepositoryRef<'a> {
    /// `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(super) fn extract_signature(
+
    pub fn extract_signature(
        &self,
        commit_oid: &Oid,
        field: Option<&str>,
@@ -256,7 +340,8 @@ impl<'a> RepositoryRef<'a> {
        }
    }

-
    pub(crate) fn revision_branches(&self, oid: &Oid) -> Result<Vec<Branch>, Error> {
+
    /// Lists branches that are reachable from `oid`.
+
    pub fn revision_branches(&self, oid: &Oid) -> Result<Vec<Branch>, Error> {
        let local = RefGlob::LocalBranch.references(self)?;
        let remote = RefGlob::RemoteBranch { remote: None }.references(self)?;
        let mut references = local.iter().chain(remote.iter());
@@ -307,7 +392,7 @@ impl<'a> RepositoryRef<'a> {
                commits.push(Commit::try_from(parent)?);
                match &commit_history {
                    CommitHistory::Last => break,
-
                    CommitHistory::Full => {},
+
                    CommitHistory::_Full => {},
                }
            }
        }
@@ -355,6 +440,99 @@ impl<'a> RepositoryRef<'a> {

        Ok(diff)
    }
+

+
    fn update_file_map(
+
        path: file_system::Path,
+
        name: file_system::Label,
+
        file: directory::File,
+
        files: &mut HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
+
    ) {
+
        files
+
            .entry(path)
+
            .and_modify(|entries| entries.push((name.clone(), file.clone())))
+
            .or_insert_with(|| NonEmpty::new((name, file)));
+
    }
+

+
    /// Do a pre-order TreeWalk of the given commit. This turns a Tree
+
    /// into a HashMap of Paths and a list of Files. We can then turn that
+
    /// into a Directory.
+
    fn get_tree(
+
        &self,
+
        oid: &Oid,
+
    ) -> Result<HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>, Error>
+
    {
+
        let mut file_paths_or_error: Result<
+
            HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
+
            Error,
+
        > = Ok(HashMap::new());
+

+
        let commit = self.repo_ref.find_commit((*oid).into())?;
+
        let tree = commit.as_object().peel_to_tree()?;
+

+
        tree.walk(git2::TreeWalkMode::PreOrder, |s, entry| {
+
            match self.tree_entry_to_file_and_path(s, entry) {
+
                Ok((path, name, file)) => {
+
                    match file_paths_or_error.as_mut() {
+
                        Ok(files) => Self::update_file_map(path, name, file, files),
+

+
                        // We don't need to update, we want to keep the error.
+
                        Err(_err) => {},
+
                    }
+
                    git2::TreeWalkResult::Ok
+
                },
+
                Err(err) => match err {
+
                    // We want to continue if the entry was not a Blob.
+
                    TreeWalkError::NotBlob => git2::TreeWalkResult::Ok,
+

+
                    // We found a ObjectType::Commit (likely a submodule) and
+
                    // so we can skip it.
+
                    TreeWalkError::Commit => git2::TreeWalkResult::Ok,
+

+
                    // But we want to keep the error and abort otherwise.
+
                    TreeWalkError::Git(err) => {
+
                        file_paths_or_error = Err(err);
+
                        git2::TreeWalkResult::Abort
+
                    },
+
                },
+
            }
+
        })?;
+

+
        file_paths_or_error
+
    }
+

+
    fn tree_entry_to_file_and_path(
+
        &self,
+
        tree_path: &str,
+
        entry: &git2::TreeEntry,
+
    ) -> Result<(file_system::Path, file_system::Label, directory::File), TreeWalkError> {
+
        // Account for the "root" of git being the empty string
+
        let path = if tree_path.is_empty() {
+
            Ok(file_system::Path::root())
+
        } else {
+
            file_system::Path::try_from(tree_path)
+
        }?;
+

+
        // We found a Commit object in the Tree, likely a submodule.
+
        // We will skip this entry.
+
        if let Some(git2::ObjectType::Commit) = entry.kind() {
+
            return Err(TreeWalkError::Commit);
+
        }
+

+
        let object = entry.to_object(self.repo_ref)?;
+
        let blob = object.as_blob().ok_or(TreeWalkError::NotBlob)?;
+
        let name = str::from_utf8(entry.name_bytes())?;
+

+
        let name = file_system::Label::try_from(name).map_err(Error::FileSystem)?;
+

+
        Ok((
+
            path,
+
            name,
+
            directory::File {
+
                contents: blob.content().to_owned(),
+
                size: blob.size(),
+
            },
+
        ))
+
    }
}

impl<'a> Vcs<Commit, Error> for RepositoryRef<'a> {
modified radicle-surf/t/src/git.rs
@@ -8,17 +8,7 @@ use radicle_surf::git::{Author, BranchType, Commit};
use radicle_surf::{
    diff::*,
    file_system::{unsound, Path},
-
    git::{
-
        error::Error,
-
        Branch,
-
        BranchName,
-
        Browser,
-
        Namespace,
-
        Oid,
-
        RefScope,
-
        Repository,
-
        TagName,
-
    },
+
    git::{error::Error, Branch, BranchName, Namespace, Oid, RefScope, Repository, Rev, TagName},
};

const GIT_PLATINUM: &str = "../data/git-platinum";
@@ -28,29 +18,26 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
fn test_submodule_failure() {
    let repo = Repository::new("../..").unwrap();
-
    let browser = Browser::new(&repo, Branch::local("main")).unwrap();
-

-
    browser.get_directory().unwrap();
+
    repo.as_ref()
+
        .snapshot(&Branch::local("main").into())
+
        .unwrap();
}

#[cfg(test)]
mod namespace {
    use super::*;
    use pretty_assertions::{assert_eq, assert_ne};
+
    use radicle_surf::vcs::Vcs;

    #[test]
    fn switch_to_banana() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let mut browser = Browser::new_with_namespace(
-
            &repo,
-
            &Namespace::try_from("golden")?,
-
            Branch::local("master"),
-
        )?;
-
        let history = browser.get();
-

-
        browser.branch(Branch::local("banana"))?;
+
        let repo = repo.as_ref();
+
        let history_master = repo.get_history(Branch::local("master").into())?;
+
        repo.switch_namespace("golden")?;
+
        let history_banana = repo.get_history(Branch::local("banana").into())?;

-
        assert_ne!(history, browser.get());
+
        assert_ne!(history_master, history_banana);

        Ok(())
    }
@@ -58,28 +45,25 @@ mod namespace {
    #[test]
    fn me_namespace() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let browser = Browser::new(&repo, Branch::local("master"))?;
-
        let history = browser.get();
+
        let repo = repo.as_ref();
+
        let history = repo.get_history(Branch::local("master").into())?;

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

-
        let browser = browser
-
            .switch_namespace(&Namespace::try_from("me")?, Branch::local("feature/#1194"))?;
+
        repo.switch_namespace("me")?;
+
        assert_eq!(repo.which_namespace(), Ok(Some(Namespace::try_from("me")?)));

-
        assert_eq!(
-
            browser.which_namespace(),
-
            Ok(Some(Namespace::try_from("me")?))
-
        );
-
        assert_eq!(history, browser.get());
+
        let history_feature = repo.get_history(Branch::local("feature/#1194").into())?;
+
        assert_eq!(history, history_feature);

        let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
-
        let mut branches = browser.list_branches(RefScope::Local)?;
+
        let mut branches = repo.list_branches(RefScope::Local)?;
        branches.sort();

        assert_eq!(expected_branches, branches);

        let expected_branches: Vec<Branch> = vec![Branch::remote("feature/#1194", "fein")];
-
        let mut branches = browser.list_branches(RefScope::Remote {
+
        let mut branches = repo.list_branches(RefScope::Remote {
            name: Some("fein".to_string()),
        })?;
        branches.sort();
@@ -92,22 +76,23 @@ mod namespace {
    #[test]
    fn golden_namespace() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let browser = Browser::new(&repo, Branch::local("master"))?;
-
        let history = browser.get();
+
        let repo = repo.as_ref();
+
        let history = repo.get_history(Branch::local("master").into())?;

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

-
        let golden_browser =
-
            browser.switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?;
+
        repo.switch_namespace("golden")?;

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

+
        let golden_history = repo.get_history(Branch::local("master").into())?;
+
        assert_eq!(history, golden_history);

        let expected_branches: Vec<Branch> = vec![Branch::local("banana"), Branch::local("master")];
-
        let mut branches = golden_browser.list_branches(RefScope::Local)?;
+
        let mut branches = repo.list_branches(RefScope::Local)?;
        branches.sort();

        assert_eq!(expected_branches, branches);
@@ -117,7 +102,7 @@ mod namespace {
            Branch::remote("heelflip", "kickflip"),
            Branch::remote("v0.1.0", "kickflip"),
        ];
-
        let mut branches = golden_browser.list_branches(RefScope::Remote {
+
        let mut branches = repo.list_branches(RefScope::Remote {
            name: Some("kickflip".to_string()),
        })?;
        branches.sort();
@@ -130,24 +115,21 @@ mod namespace {
    #[test]
    fn silver_namespace() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let browser = Browser::new(&repo, Branch::local("master"))?;
-
        let history = browser.get();
-

-
        assert_eq!(browser.which_namespace(), Ok(None));
+
        let repo = repo.as_ref();
+
        let history = repo.get_history(Branch::local("master").into())?;

-
        let silver_browser = browser.switch_namespace(
-
            &Namespace::try_from("golden/silver")?,
-
            Branch::local("master"),
-
        )?;
+
        assert_eq!(repo.which_namespace(), Ok(None));

+
        repo.switch_namespace("golden/silver")?;
        assert_eq!(
-
            silver_browser.which_namespace(),
+
            repo.which_namespace(),
            Ok(Some(Namespace::try_from("golden/silver")?))
        );
-
        assert_ne!(history, silver_browser.get());
+
        let silver_history = repo.get_history(Branch::local("master").into())?;
+
        assert_ne!(history, silver_history);

        let expected_branches: Vec<Branch> = vec![Branch::local("master")];
-
        let mut branches = silver_browser.list_branches(RefScope::All)?;
+
        let mut branches = repo.list_branches(RefScope::All)?;
        branches.sort();

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

#[cfg(test)]
mod rev {
+
    use radicle_surf::vcs::Vcs;
+

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

@@ -175,13 +159,12 @@ mod rev {
    #[test]
    fn _master() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
        browser.rev(Branch::remote("master", "origin"))?;
+
        let repo = repo.as_ref();
+
        let history = repo.get_history(Branch::remote("master", "origin").into())?;

        let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
        assert!(
-
            browser
-
                .as_history()
+
            history
                .find(|commit| if commit.id == commit1 {
                    Some(commit.clone())
                } else {
@@ -190,13 +173,12 @@ mod rev {
                .is_some(),
            "commit_id={}, history =\n{:#?}",
            commit1,
-
            browser.as_history()
+
            &history
        );

        let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
        assert!(
-
            browser
-
                .as_history()
+
            history
                .find(|commit| if commit.id == commit2 {
                    Some(commit.clone())
                } else {
@@ -205,7 +187,7 @@ mod rev {
                .is_some(),
            "commit_id={}, history =\n{:#?}",
            commit2,
-
            browser.as_history()
+
            &history
        );

        Ok(())
@@ -214,12 +196,11 @@ mod rev {
    #[test]
    fn commit() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
        browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?;
+
        let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
+
        let history = repo.as_ref().get_history(rev)?;

        let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
        assert!(browser
-
            .as_history()
+
        assert!(history
            .find(|commit| if commit.id == commit1 {
                Some(commit.clone())
            } else {
@@ -233,9 +214,9 @@ mod rev {
    #[test]
    fn commit_parents() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
        browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?;
-
        let commit = browser.as_history().first();
+
        let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
+
        let history = repo.as_ref().get_history(rev)?;
+
        let commit = history.first();

        assert_eq!(
            commit.parents,
@@ -248,12 +229,11 @@ mod rev {
    #[test]
    fn commit_short() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
        browser.rev(browser.oid("3873745c8")?)?;
+
        let rev: Rev = repo.as_ref().oid("3873745c8")?.into();
+
        let history = repo.as_ref().get_history(rev)?;

        let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
        assert!(browser
-
            .as_history()
+
        assert!(history
            .find(|commit| if commit.id == commit1 {
                Some(commit.clone())
            } else {
@@ -267,11 +247,11 @@ mod rev {
    #[test]
    fn tag() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
-
        let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
        browser.rev(TagName::new("v0.2.0"))?;
+
        let rev: Rev = TagName::new("v0.2.0").into();
+
        let history = repo.as_ref().get_history(rev)?;

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

        Ok(())
    }
@@ -279,6 +259,8 @@ mod rev {

#[cfg(test)]
mod last_commit {
+
    use radicle_surf::vcs::Vcs;
+

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

@@ -286,52 +268,46 @@ mod last_commit {
    fn readme_missing_and_memory() {
        let repo = Repository::new(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
-
        let mut browser =
-
            Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
-

-
        // Set the browser history to the initial commit
-
        let commit =
+
        let oid =
            Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");
-
        browser.commit(commit).unwrap();
-

-
        let head_commit = browser.get().0.first().clone();

        // memory.rs is commited later so it should not exist here.
-
        let memory_last_commit = browser
-
            .last_commit(Path::with_root(&[
-
                unsound::label::new("src"),
-
                unsound::label::new("memory.rs"),
-
            ]))
+
        let rev: Rev = oid.into();
+
        let memory_last_commit_oid = repo
+
            .as_ref()
+
            .last_commit(
+
                Path::with_root(&[unsound::label::new("src"), unsound::label::new("memory.rs")]),
+
                &rev,
+
            )
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

-
        assert_eq!(memory_last_commit, None);
+
        assert_eq!(memory_last_commit_oid, None);

        // README.md exists in this commit.
-
        let readme_last_commit = browser
-
            .last_commit(Path::with_root(&[unsound::label::new("README.md")]))
+
        let readme_last_commit = repo
+
            .as_ref()
+
            .last_commit(Path::with_root(&[unsound::label::new("README.md")]), &rev)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

-
        assert_eq!(readme_last_commit, Some(head_commit.id));
+
        assert_eq!(readme_last_commit, Some(oid));
    }

    #[test]
    fn folder_svelte() {
        let repo = Repository::new(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
-
        let mut browser =
-
            Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
-

        // Check that last commit is the actual last commit even if head commit differs.
-
        let commit =
+
        let oid =
            Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
-
        browser.commit(commit).unwrap();
+
        let rev: Rev = oid.into();

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

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

@@ -342,20 +318,19 @@ mod last_commit {
    fn nest_directory() {
        let repo = Repository::new(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
-
        let mut browser =
-
            Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
-

        // Check that last commit is the actual last commit even if head commit differs.
-
        let commit =
+
        let oid =
            Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
-
        browser.commit(commit).unwrap();
+
        let rev: Rev = oid.into();

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

-
        let nested_directory_tree_commit_id = browser
-
            .last_commit(unsound::path::new(
-
                "~/this/is/a/really/deeply/nested/directory/tree",
-
            ))
+
        let nested_directory_tree_commit_id = repo
+
            .as_ref()
+
            .last_commit(
+
                unsound::path::new("~/this/is/a/really/deeply/nested/directory/tree"),
+
                &rev,
+
            )
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

@@ -367,24 +342,24 @@ mod last_commit {
    fn can_get_last_commit_for_special_filenames() {
        let repo = Repository::new(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
-
        let mut browser =
-
            Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");

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

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

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

-
        let ogre_commit_id = browser
-
            .last_commit(unsound::path::new("~/special/👹👹👹"))
+
        let ogre_commit_id = repo
+
            .as_ref()
+
            .last_commit(unsound::path::new("~/special/👹👹👹"), &rev)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);
        assert_eq!(ogre_commit_id, Some(expected_commit_id));
@@ -394,15 +369,20 @@ mod last_commit {
    fn root() {
        let repo = Repository::new(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
-
        let browser =
-
            Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
-

-
        let root_last_commit_id = browser
-
            .last_commit(Path::root())
+
        let rev: Rev = Branch::local("master").into();
+
        let root_last_commit_id = repo
+
            .as_ref()
+
            .last_commit(Path::root(), &rev)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

-
        assert_eq!(root_last_commit_id, Some(browser.get().first().id));
+
        let expected_oid = repo
+
            .as_ref()
+
            .get_history(Branch::local("master").into())
+
            .unwrap()
+
            .first()
+
            .id;
+
        assert_eq!(root_last_commit_id, Some(expected_oid));
    }
}

@@ -416,14 +396,13 @@ mod diff {
    fn test_initial_diff() -> Result<(), Error> {
        let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
        let repo = Repository::new(GIT_PLATINUM)?;
-
        // let commit = repo.0.find_commit(oid).unwrap();
-
        let commit = repo.as_ref().get_git2_commit(oid).unwrap();
+
        let repo = repo.as_ref();
+
        let commit = repo.get_git2_commit(oid).unwrap();

        assert!(commit.parents().count() == 0);
        assert!(commit.parent(0).is_err());

-
        let bro = Browser::new(&repo, Branch::local("master"))?;
-
        let diff = bro.initial_diff(oid)?;
+
        let diff = repo.initial_diff(oid)?;

        let expected_diff = Diff {
            created: vec![CreateFile {
@@ -453,15 +432,12 @@ mod diff {
    #[test]
    fn test_diff() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
+
        let repo = repo.as_ref();
        let commit = repo
-
            .as_ref()
            .get_git2_commit(Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?)
            .unwrap();
        let parent = commit.parent(0)?;
-

-
        let bro = Browser::new(&repo, Branch::local("master"))?;
-

-
        let diff = bro.diff(parent.id().into(), commit.id().into())?;
+
        let diff = repo.diff(parent.id().into(), commit.id().into())?;

        let expected_diff = Diff {
                created: vec![],
@@ -572,8 +548,7 @@ mod threading {
    fn basic_test() -> Result<(), Error> {
        let shared_repo = Mutex::new(Repository::new(GIT_PLATINUM)?);
        let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
-
        let bro = Browser::new(&*locked_repo, Branch::local("master"))?;
-
        let mut branches = bro.list_branches(RefScope::All)?;
+
        let mut branches = locked_repo.as_ref().list_branches(RefScope::All)?;
        branches.sort();

        assert_eq!(