Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'han/new-design-2'
Fintan Halpenny committed 3 years ago
commit 207b55494e39f64d34a0ec3bdba70ceb7f99737a
parent 6a3fac7
15 files changed +417 -1730
modified radicle-surf/docs/refactor-design.md
@@ -1,47 +1,153 @@
# 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:
+
## Introduction

-
- 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.
+
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:

-
The main goals of the changes are:
+
1. Code browsing: given a specific commit/ref, browse files and directories.
+
2. Diff between two revisions that resolve into two commits.
+
3. Retrieve the history of the commits.
+
4. Retrieve a specific object: all its metadata.
+
5. Retrieve the refs: Branches, Tags, Remotes, Notes and user-defined
+
"categories", where a category is: refs/<category>/<...>.

-
- 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)
+
## Motivation

-
## Make API simpler
+
The `radicle-surf` crate aims to provide a safe and easy-to-use API that
+
supports the features listed in [Introduction]. Based on the existing API,
+
the main goals of the refactoring are:

-
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.
+
- API review: 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)
+
- Hide away `git2` from the API. The use of `git2` should be an implementation
+
detail.
+

+
## API review
+

+
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 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?
+
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`
+
## Remove the `Snapshot` type
+

+
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 snapshot function can be easily implement as a method of `RepositoryRef`.
+

+
## Simplify `Directory` and remove the `Tree` and `Forest` types
+

+
The `Directory` type represents the file system view of a snapshot. Its field
+
`sub_directories` is defined a `Forest` based on `Tree`. The types are
+
over-engineered from such a simple concept. We could refactor `Directory` to
+
use `DirectoryContents` for its items and not to use `Tree` or `Forest` at all.
+

+
We also found the `list_directory()` method duplicates with `iter()` method.
+
Hence `list_directory()` is removed, together with `SystemType` type.
+

+
## The new API
+

+
With the changes proposed in the previous section, we describe what the new API
+
would look like and how they meet the requirements.
+

+
### Common principles
+

+
#### How to identify things that resolve into a commit
+

+
In our API, it will be good to have a single way to identify all refs and
+
objects that resolve into commits. In other words, we try to avoid using
+
different ways at different places. Currently there are multiple types in the
+
API for this purpose:
+

+
- Commit
+
- Oid
+
- Rev
+

+
Because `Rev` is the most high level among those and supports refs already,
+
I think we should use `Rev` in our API as much as possible.
+

+
#### How to identify History
+

+
TBD

-
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.
+
### Code browsing

-
The only `snapshot` function defined currently:
+
The user should be able to browse the files and directories for any given
+
commits or references. The core API is:

+
- Create a root Directory:
```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))
-
        });
+
imp RepositoryRef {
+
    pub fn snapshot(&self, rev: &Rev) -> Result<Directory, Error>;
+
}
```

-
The above function can be easily implement as a method of `Repository`.
+
- Browse a Directory:
+
```Rust
+
impl Directory {
+
    pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents>;
+
}
+
```
+
where `DirectoryContents` supports both files and sub-directories:
+
```Rust
+
pub enum DirectoryContents {
+
    /// The `File` variant contains the file's name and the [`File`] itself.
+
    File {
+
        /// The name of the file.
+
        name: Label,
+
        /// The file data.
+
        file: File,
+
    },
+
    /// The `Directory` variant contains a sub-directory to the current one.
+
    Directory(Directory),
+
}
+
```
+

+
### Diffs
+

+
The user would be able to create a diff between any two revisions that resolve
+
into two commits.
+

+
The main change is to use `Rev` instead of `Oid` to identify `from` and `to`.
+
The core API is:
+

+
```Rust
+
imp RepositoryRef {
+
    pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error>;
+
}
+
```
+

+
To help convert from `Oid` to `Rev`, we provide a helper method:
+
```Rust
+
imp RepositoryRef {
+
    /// Returns the Oid of `rev`.
+
    pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error>;
+
}
+
```
+

+
## Error handling
+

+
TBD
modified radicle-surf/examples/diff.rs
@@ -32,7 +32,10 @@ fn main() {
    let base_oid = Oid::from_str(&options.base_revision).unwrap();
    let now = Instant::now();
    let elapsed_nanos = now.elapsed().as_nanos();
-
    let diff = repo.as_ref().diff(base_oid, head_oid).unwrap();
+
    let diff = repo
+
        .as_ref()
+
        .diff(&base_oid.into(), &head_oid.into())
+
        .unwrap();
    print_diff_summary(&diff, elapsed_nanos);
}

modified radicle-surf/src/commit.rs
@@ -144,12 +144,14 @@ pub struct Commits {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
-
pub fn commit(repo: &RepositoryRef, sha1: Oid) -> Result<Commit, Error> {
+
pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
+
    let sha1 = repo.rev_oid(rev)?;
    let commit = repo.get_commit(sha1)?;
    let diff = if let Some(parent) = commit.parents.first() {
-
        repo.diff(*parent, sha1)?
+
        let parent_rev = (*parent).into();
+
        repo.diff(&parent_rev, rev)?
    } else {
-
        repo.initial_diff(sha1)?
+
        repo.initial_diff(rev)?
    };

    let mut deletions = 0;
modified radicle-surf/src/diff.rs
@@ -295,7 +295,10 @@ impl Diff {
    #[allow(clippy::self_named_constructors)]
    pub fn diff(left: Directory, right: Directory) -> Self {
        let mut diff = Diff::new();
-
        let path = Rc::new(RefCell::new(Path::from_labels(right.current(), &[])));
+
        let path = Rc::new(RefCell::new(Path::from_labels(
+
            right.current().clone(),
+
            &[],
+
        )));
        Diff::collect_diff(&left, &right, &path, &mut diff);

        // TODO: Some of the deleted files may actually be moved (renamed) to one of the
@@ -315,15 +318,15 @@ impl Diff {
        parent_path: &Rc<RefCell<Path>>,
        diff: &mut Diff,
    ) {
-
        let mut old_iter = old.iter();
-
        let mut new_iter = new.iter();
+
        let mut old_iter = old.contents();
+
        let mut new_iter = new.contents();
        let mut old_entry_opt = old_iter.next();
        let mut new_entry_opt = new_iter.next();

        while old_entry_opt.is_some() || new_entry_opt.is_some() {
            match (&old_entry_opt, &new_entry_opt) {
                (Some(ref old_entry), Some(ref new_entry)) => {
-
                    match new_entry.label().cmp(&old_entry.label()) {
+
                    match new_entry.label().cmp(old_entry.label()) {
                        Ordering::Greater => {
                            diff.add_deleted_files(old_entry, parent_path);
                            old_entry_opt = old_iter.next();
@@ -414,11 +417,11 @@ impl Diff {
                        },
                    }
                },
-
                (Some(ref old_entry), None) => {
+
                (Some(old_entry), None) => {
                    diff.add_deleted_files(old_entry, parent_path);
                    old_entry_opt = old_iter.next();
                },
-
                (None, Some(ref new_entry)) => {
+
                (None, Some(new_entry)) => {
                    diff.add_created_files(new_entry, parent_path);
                    new_entry_opt = new_iter.next();
                },
@@ -465,15 +468,15 @@ impl Diff {
    ) where
        F: Fn(Path) -> T + Copy,
    {
-
        parent_path.borrow_mut().push(dir.current());
-
        for entry in dir.iter() {
+
        parent_path.borrow_mut().push(dir.current().clone());
+
        for entry in dir.contents() {
            match entry {
                DirectoryContents::Directory(subdir) => {
-
                    Diff::collect_files_inner(&subdir, parent_path, mapper, files);
+
                    Diff::collect_files_inner(subdir, parent_path, mapper, files);
                },
                DirectoryContents::File { name, .. } => {
                    let mut path = parent_path.borrow().clone();
-
                    path.push(name);
+
                    path.push(name.clone());
                    files.push(mapper(path));
                },
            }
modified radicle-surf/src/file_system/directory.rs
@@ -21,42 +21,15 @@
//! See [`Directory`] for more information.
//!
//! As well as this, this module contains [`DirectoryContents`] which is the
-
//! output of iterating over a [`Directory`], and also [`SystemType`] which is
-
//! an identifier of what type of [`DirectoryContents`] one is viewing when
-
//! [listing](#method.list_directory) a directory.
+
//! output of iterating over a [`Directory`].

-
use crate::{file_system::path::*, tree::*};
+
use crate::file_system::path::*;
use nonempty::NonEmpty;
use std::{
-
    collections::{hash_map::DefaultHasher, HashMap},
+
    collections::{hash_map::DefaultHasher, BTreeMap},
    hash::{Hash, Hasher},
};

-
/// `SystemType` is an enumeration over what can be found in a [`Directory`] so
-
/// we can report back to the caller a [`Label`] and its type.
-
///
-
/// See [`SystemType::file`](#method.file) and
-
/// [`SystemType::directory`](#method.directory).
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub enum SystemType {
-
    /// The `File` type in a directory system.
-
    File,
-
    /// The `Directory` type in a directory system.
-
    Directory,
-
}
-

-
impl SystemType {
-
    /// A file name and [`SystemType::File`].
-
    pub fn file(label: Label) -> (Label, Self) {
-
        (label, SystemType::File)
-
    }
-

-
    /// A directory name and [`SystemType::Directory`].
-
    pub fn directory(label: Label) -> (Label, Self) {
-
        (label, SystemType::Directory)
-
    }
-
}
-

/// A `File` consists of its file contents (a [`Vec`] of bytes).
///
/// The `Debug` instance of `File` will show the first few bytes of the file and
@@ -126,29 +99,16 @@ impl File {
    }
}

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
enum Location {
-
    Root,
-
    SubDirectory(Label),
-
}
-

-
/// A `Directory` can be thought of as a non-empty set of entries of
-
/// sub-directories and files. The reason for the non-empty property is that a
-
/// VCS directory would have at least one artifact as a sub-directory which
-
/// tracks the VCS work, e.g. git using the `.git` folder.
-
///
-
/// On top of that, some VCSes, such as git, will not track an empty directory,
-
/// and so when creating a new directory to track it will have to contain at
-
/// least one file.
+
/// A `Directory` is a set of entries of sub-directories and files, ordered
+
/// by their unique names in the alphabetical order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Directory {
-
    current: Location,
-
    sub_directories: Forest<Label, File>,
+
    name: Label,
+
    contents: BTreeMap<Label, DirectoryContents>,
}

/// `DirectoryContents` is an enumeration of what a [`Directory`] can contain
-
/// and is used for when we are [`iter`](struct.Directory.html#method.iter)ating
-
/// through a [`Directory`].
+
/// and is used for when we are iterating through a [`Directory`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DirectoryContents {
    /// The `File` variant contains the file's name and the [`File`] itself.
@@ -165,168 +125,53 @@ pub enum DirectoryContents {
impl DirectoryContents {
    /// Get a label for the `DirectoryContents`, either the name of the [`File`]
    /// or the name of the [`Directory`].
-
    pub fn label(&self) -> Label {
+
    pub fn label(&self) -> &Label {
        match self {
-
            DirectoryContents::File { name, .. } => name.clone(),
+
            DirectoryContents::File { name, .. } => name,
            DirectoryContents::Directory(directory) => directory.current(),
        }
    }
}

-
impl From<SubTree<Label, File>> for DirectoryContents {
-
    fn from(sub_tree: SubTree<Label, File>) -> Self {
-
        match sub_tree {
-
            SubTree::Node { key, value } => DirectoryContents::File {
-
                name: key,
-
                file: value,
-
            },
-
            SubTree::Branch { key, forest } => DirectoryContents::Directory(Directory {
-
                current: Location::SubDirectory(key),
-
                sub_directories: (*forest).into(),
-
            }),
-
        }
-
    }
-
}
-

impl Directory {
    /// Create a root directory.
    ///
    /// This function is usually used for testing and demonstation purposes.
    pub fn root() -> Self {
        Directory {
-
            current: Location::Root,
-
            sub_directories: Forest::root(),
+
            name: Label::root(),
+
            contents: BTreeMap::new(),
        }
    }

    /// Create a directory, similar to `root`, except with a given name.
    ///
    /// This function is usually used for testing and demonstation purposes.
-
    pub fn new(label: Label) -> Self {
+
    pub fn new(name: Label) -> Self {
        Directory {
-
            current: Location::SubDirectory(label),
-
            sub_directories: Forest::root(),
+
            name,
+
            contents: BTreeMap::new(),
        }
    }

-
    /// List the current `Directory`'s files and sub-directories.
-
    ///
-
    /// The listings are a pair of [`Label`] and [`SystemType`], where the
-
    /// [`Label`] represents the name of the file or directory.
-
    ///
-
    /// ```
-
    /// use nonempty::NonEmpty;
-
    /// use radicle_surf::file_system::{Directory, File, SystemType};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let mut directory = Directory::root();
-
    ///
-
    /// // Root files set up
-
    /// let root_files = NonEmpty::from((
-
    ///     (unsound::label::new("foo.rs"), File::new(b"use crate::bar")),
-
    ///     vec![(
-
    ///         unsound::label::new("bar.rs"),
-
    ///         File::new(b"fn hello_world()"),
-
    ///     )],
-
    /// ));
-
    /// directory.insert_files(&[], root_files);
-
    ///
-
    /// // Haskell files set up
-
    /// let haskell_files = NonEmpty::from((
-
    ///     (
-
    ///         unsound::label::new("foo.hs"),
-
    ///         File::new(b"module Foo where"),
-
    ///     ),
-
    ///     vec![(
-
    ///         unsound::label::new("bar.hs"),
-
    ///         File::new(b"module Bar where"),
-
    ///     )],
-
    /// ));
-
    ///
-
    /// directory.insert_files(&[unsound::label::new("haskell")], haskell_files);
-
    ///
-
    /// let mut directory_contents = directory.list_directory();
-
    /// directory_contents.sort();
-
    ///
-
    /// assert_eq!(
-
    ///     directory_contents,
-
    ///     vec![
-
    ///         SystemType::file(unsound::label::new("bar.rs")),
-
    ///         SystemType::file(unsound::label::new("foo.rs")),
-
    ///         SystemType::directory(unsound::label::new("haskell")),
-
    ///     ]
-
    /// );
-
    /// ```
-
    pub fn list_directory(&self) -> Vec<(Label, SystemType)> {
-
        let forest = &self.sub_directories;
-
        match &forest.0 {
-
            None => vec![],
-
            Some(trees) => trees
-
                .0
-
                .iter()
-
                .map(|tree| match tree {
-
                    SubTree::Node { key: name, .. } => SystemType::file(name.clone()),
-
                    SubTree::Branch { key: name, .. } => SystemType::directory(name.clone()),
-
                })
-
                .collect(),
-
        }
+
    /// Get the name of the current `Directory`.
+
    pub fn name(&self) -> &Label {
+
        &self.name
    }

-
    /// Get the [`Label`] of the current directory.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Directory, DirectoryContents, File, Label};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let mut root = Directory::root();
-
    ///
-
    /// let main = File::new(b"println!(\"Hello, world!\")");
-
    /// root.insert_file(unsound::path::new("main.rs"), main.clone());
-
    ///
-
    /// let lib = File::new(b"struct Hello(String)");
-
    /// root.insert_file(unsound::path::new("lib.rs"), lib.clone());
-
    ///
-
    /// let test_mod = File::new(b"assert_eq!(1 + 1, 2);");
-
    /// root.insert_file(unsound::path::new("test/mod.rs"), test_mod.clone());
-
    ///
-
    /// let mut root_iter = root.iter();
-
    ///
-
    /// assert_eq!(root_iter.next(), Some(DirectoryContents::File {
-
    ///     name: unsound::label::new("lib.rs"),
-
    ///     file: lib
-
    /// }));
-
    ///
-
    /// assert_eq!(root_iter.next(), Some(DirectoryContents::File {
-
    ///     name: unsound::label::new("main.rs"),
-
    ///     file: main
-
    /// }));
-
    ///
-
    /// let mut test_dir = Directory::new(unsound::label::new("test"));
-
    /// test_dir.insert_file(unsound::path::new("mod.rs"), test_mod);
-
    ///
-
    /// assert_eq!(root_iter.next(), Some(DirectoryContents::Directory(test_dir)));
-
    /// ```
-
    pub fn iter(&self) -> impl Iterator<Item = DirectoryContents> + '_ {
-
        let mut empty_iter = None;
-
        let mut trees_iter = None;
-
        match &self.sub_directories.0 {
-
            None => empty_iter = Some(std::iter::empty()),
-
            Some(trees) => {
-
                trees_iter = Some(
-
                    trees
-
                        .iter_subtrees()
-
                        .cloned()
-
                        .map(|sub_tree| sub_tree.into()),
-
                )
-
            },
-
        }
+
    /// Add the `content` under `name` to the current `Directory`.
+
    /// If `name` already exists in this directory, then the previous contents
+
    /// are replaced.
+
    pub fn insert(&mut self, name: Label, content: DirectoryContents) {
+
        self.contents.insert(name, content);
+
    }

-
        empty_iter
-
            .into_iter()
-
            .flatten()
-
            .chain(trees_iter.into_iter().flatten())
+
    /// Returns an iterator for the contents of the current directory.
+
    ///
+
    /// Note that the returned iterator only iterates the current level,
+
    /// without going recursively into sub-directories.
+
    pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents> {
+
        self.contents.values()
    }

    /// Find a [`File`] in the directory given the [`Path`] to the [`File`].
@@ -361,8 +206,23 @@ impl Directory {
    /// // We shouldn't be able to find a file that doesn't exist
    /// assert_eq!(directory.find_file(unsound::path::new("foo/bar/qux.rs")), None);
    /// ```
-
    pub fn find_file(&self, path: Path) -> Option<File> {
-
        self.sub_directories.find_node(path.0).cloned()
+
    pub fn find_file(&self, path: Path) -> Option<&File> {
+
        let mut contents = &self.contents;
+
        let path_depth = path.0.len();
+
        for (idx, label) in path.iter().enumerate() {
+
            match contents.get(label) {
+
                Some(DirectoryContents::Directory(d)) => contents = &d.contents,
+
                Some(DirectoryContents::File { name: _, file }) => {
+
                    if idx + 1 == path_depth {
+
                        return Some(file);
+
                    } else {
+
                        break; // Abort: finding a file before the last label.
+
                    }
+
                },
+
                None => break, // Abort: a label not found.
+
            }
+
        }
+
        None
    }

    /// Find a `Directory` in the directory given the [`Path`] to the
@@ -400,17 +260,22 @@ impl Directory {
    /// // 'baz.rs' is a file and not a directory
    /// assert!(directory.find_directory(unsound::path::new("foo/bar/baz.rs")).is_none());
    /// ```
-
    pub fn find_directory(&self, path: Path) -> Option<Self> {
-
        self.sub_directories
-
            .find_branch(path.0.clone())
-
            .cloned()
-
            .map(|tree| {
-
                let (_, current) = path.split_last();
-
                Directory {
-
                    current: Location::SubDirectory(current),
-
                    sub_directories: tree.into(),
-
                }
-
            })
+
    pub fn find_directory(&self, path: Path) -> Option<&Self> {
+
        let mut found = None;
+
        let mut contents = &self.contents;
+

+
        for label in path.iter() {
+
            match contents.get(label) {
+
                Some(DirectoryContents::Directory(d)) => {
+
                    found = Some(d);
+
                    contents = &d.contents;
+
                },
+
                Some(DirectoryContents::File { .. }) => break, // Abort: should not be a file.
+
                None => break,                                 // Abort: a label not found.
+
            }
+
        }
+

+
        found
    }

    /// Get the [`Label`] of the current directory.
@@ -433,11 +298,8 @@ impl Directory {
    /// ).expect("Missing test directory");
    /// assert_eq!(test.current(), unsound::label::new("test"));
    /// ```
-
    pub fn current(&self) -> Label {
-
        match &self.current {
-
            Location::Root => Label::root(),
-
            Location::SubDirectory(label) => label.clone(),
-
        }
+
    pub fn current(&self) -> &Label {
+
        &self.name
    }

    // TODO(fintan): This is going to be a bit trickier so going to leave it out for
@@ -464,9 +326,13 @@ impl Directory {
    /// assert_eq!(root.size(), 66);
    /// ```
    pub fn size(&self) -> usize {
-
        self.sub_directories
-
            .iter()
-
            .fold(0, |size, file| size + file.size())
+
        self.contents().fold(0, |size, item| {
+
            if let DirectoryContents::File { name: _, file } = item {
+
                size + file.size()
+
            } else {
+
                size
+
            }
+
        })
    }

    /// Insert a file into a directory, given the full path to file (file name
@@ -474,7 +340,39 @@ impl Directory {
    ///
    /// This function is usually used for testing and demonstation purposes.
    pub fn insert_file(&mut self, path: Path, file: File) {
-
        self.sub_directories.insert(path.0, file)
+
        let name = path.0.last().clone();
+
        let f = DirectoryContents::File {
+
            name: name.clone(),
+
            file,
+
        };
+

+
        let mut contents = &mut self.contents;
+
        let path_depth = path.0.len() - 1; // exclude the last label: file name
+

+
        if path_depth == 0 {
+
            contents.insert(name, f);
+
        } else {
+
            for (idx, label) in path.iter().enumerate() {
+
                // if label does not exist, create a sub directory.
+
                if contents.get(label).is_none() {
+
                    let new_dir = Directory::new(label.clone());
+
                    contents.insert(label.clone(), DirectoryContents::Directory(new_dir));
+
                }
+

+
                match contents.get_mut(label) {
+
                    Some(DirectoryContents::Directory(d)) => {
+
                        contents = &mut d.contents;
+
                        if idx + 1 == path_depth {
+
                            // We are in the last directory level, insert the file.
+
                            contents.insert(name, f);
+
                            return;
+
                        }
+
                    },
+
                    Some(DirectoryContents::File { .. }) => return, // Abort: should not be a file.
+
                    None => return,
+
                }
+
            }
+
        }
    }

    /// Insert files into a shared directory path.
@@ -503,25 +401,4 @@ impl Directory {
            },
        }
    }
-

-
    /// Creates a `Directory` from a HashMap `files`.
-
    pub fn from_hash_map(files: HashMap<Path, NonEmpty<(Label, File)>>) -> Self {
-
        let mut directory: Self = Directory::root();
-

-
        for (path, files) in files.into_iter() {
-
            for (file_name, file) in files.into_iter() {
-
                let file_path = if path.is_root() {
-
                    Path::new(file_name)
-
                } else {
-
                    let mut new_path = path.clone();
-
                    new_path.push(file_name);
-
                    new_path
-
                };
-

-
                directory.insert_file(file_path, file)
-
            }
-
        }
-

-
        directory
-
    }
}
modified radicle-surf/src/file_system/path.rs
@@ -48,6 +48,9 @@ impl Deref for Label {
    }
}

+
/// The label for the root directory.
+
pub const ROOT_LABEL: &str = "~";
+

impl Label {
    /// The root label for the root directory, i.e. `"~"`.
    ///
@@ -64,7 +67,7 @@ impl Label {
    /// ```
    pub fn root() -> Self {
        Label {
-
            label: "~".into(),
+
            label: ROOT_LABEL.into(),
            hidden: false,
        }
    }
modified radicle-surf/src/lib.rs
@@ -103,8 +103,6 @@ pub mod syntax;
#[cfg(feature = "syntax")]
pub use syntax::SYNTAX_SET;

-
pub mod tree;
-

// Private modules
mod nonempty;

modified radicle-surf/src/object/tree.rs
@@ -28,7 +28,7 @@ use serde::{

use crate::{
    commit,
-
    file_system,
+
    file_system::{self, DirectoryContents},
    git::RepositoryRef,
    object::{Error, Info, ObjectType},
    revision::Revision,
@@ -113,33 +113,31 @@ where

    let root_dir = repo.snapshot(&rev)?;
    let prefix_dir = if path.is_root() {
-
        root_dir
+
        &root_dir
    } else {
        root_dir
            .find_directory(path.clone())
            .ok_or_else(|| Error::PathNotFound(path.clone()))?
    };
-
    let mut prefix_contents = prefix_dir.list_directory();
-
    prefix_contents.sort();

-
    let entries_results: Result<Vec<TreeEntry>, Error> = prefix_contents
-
        .iter()
-
        .map(|(label, system_type)| {
+
    let entries_results: Result<Vec<TreeEntry>, Error> = prefix_dir
+
        .contents()
+
        .map(|entry| {
            let entry_path = if path.is_root() {
-
                file_system::Path::new(label.clone())
+
                file_system::Path::new(entry.label().clone())
            } else {
                let mut p = path.clone();
-
                p.push(label.clone());
+
                p.push(entry.label().clone());
                p
            };
            let mut commit_path = file_system::Path::root();
            commit_path.append(entry_path.clone());

            let info = Info {
-
                name: label.to_string(),
-
                object_type: match system_type {
-
                    file_system::SystemType::Directory => ObjectType::Tree,
-
                    file_system::SystemType::File => ObjectType::Blob,
+
                name: entry.label().to_string(),
+
                object_type: match entry {
+
                    DirectoryContents::Directory(_) => ObjectType::Tree,
+
                    DirectoryContents::File { .. } => ObjectType::Blob,
                },
                last_commit: None,
            };
deleted radicle-surf/src/tree.rs
@@ -1,503 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
#![allow(missing_docs)]
-

-
use crate::nonempty::split_last;
-
use nonempty::NonEmpty;
-
use std::cmp::Ordering;
-

-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub enum SubTree<K, A> {
-
    Node { key: K, value: A },
-
    Branch { key: K, forest: Box<Tree<K, A>> },
-
}
-

-
impl<K, A> SubTree<K, A> {
-
    /// Create a new `Branch` from a key and sub-tree.
-
    ///
-
    /// This function is a convenience for now having to
-
    /// remember to use `Box::new`.
-
    fn branch(key: K, tree: Tree<K, A>) -> Self {
-
        SubTree::Branch {
-
            key,
-
            forest: Box::new(tree),
-
        }
-
    }
-

-
    fn key(&self) -> &K {
-
        match self {
-
            SubTree::Node { key, .. } => key,
-
            SubTree::Branch { key, .. } => key,
-
        }
-
    }
-

-
    pub fn find(&self, keys: NonEmpty<K>) -> Option<&Self>
-
    where
-
        K: Ord,
-
    {
-
        let (head, tail) = keys.into();
-
        let tail = NonEmpty::from_vec(tail);
-
        match self {
-
            SubTree::Node { key, .. } => match tail {
-
                None if *key == head => Some(self),
-
                _ => None,
-
            },
-
            SubTree::Branch { key, ref forest } => match tail {
-
                None if *key == head => Some(self),
-
                None => None,
-
                Some(keys) => forest.find(keys),
-
            },
-
        }
-
    }
-

-
    pub fn to_nonempty(&self) -> NonEmpty<A>
-
    where
-
        A: Clone,
-
        K: Clone,
-
    {
-
        match self {
-
            Self::Node { value, .. } => NonEmpty::new(value.clone()),
-
            Self::Branch { forest, .. } => forest.to_nonempty(),
-
        }
-
    }
-

-
    pub(crate) fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &A> + 'a> {
-
        match self {
-
            SubTree::Node { value, .. } => Box::new(std::iter::once(value)),
-
            SubTree::Branch { ref forest, .. } => Box::new(forest.iter()),
-
        }
-
    }
-

-
    fn iter_keys<'a>(&'a self) -> Box<dyn Iterator<Item = &K> + 'a> {
-
        match self {
-
            SubTree::Node { key, .. } => Box::new(std::iter::once(key)),
-
            SubTree::Branch {
-
                ref key,
-
                ref forest,
-
            } => Box::new(std::iter::once(key).chain(forest.iter_keys())),
-
        }
-
    }
-

-
    fn compare_by<F>(&self, other: &Self, f: &F) -> Ordering
-
    where
-
        F: Fn(&A, &A) -> Ordering,
-
    {
-
        match (self, other) {
-
            (
-
                SubTree::Node { value, .. },
-
                SubTree::Node {
-
                    value: other_value, ..
-
                },
-
            ) => f(value, other_value),
-
            (SubTree::Branch { forest, .. }, SubTree::Node { value, .. }) => {
-
                let max_forest = forest.maximum_by(f);
-
                f(max_forest, value)
-
            },
-
            (SubTree::Node { value, .. }, SubTree::Branch { forest, .. }) => {
-
                let max_forest = &forest.maximum_by(f);
-
                f(value, max_forest)
-
            },
-
            (
-
                SubTree::Branch { forest, .. },
-
                SubTree::Branch {
-
                    forest: other_forest,
-
                    ..
-
                },
-
            ) => {
-
                let max_forest = forest.maximum_by(f);
-
                let max_other_forest = other_forest.maximum_by(f);
-
                f(max_forest, max_other_forest)
-
            },
-
        }
-
    }
-

-
    pub fn maximum_by<F>(&self, f: &F) -> &A
-
    where
-
        F: Fn(&A, &A) -> Ordering,
-
    {
-
        match self {
-
            SubTree::Node { value, .. } => value,
-
            SubTree::Branch { forest, .. } => forest.maximum_by(f),
-
        }
-
    }
-

-
    pub fn map<F, B>(self, f: &mut F) -> SubTree<K, B>
-
    where
-
        F: FnMut(A) -> B,
-
    {
-
        match self {
-
            SubTree::Node { key, value } => SubTree::Node {
-
                key,
-
                value: f(value),
-
            },
-
            SubTree::Branch { key, forest } => SubTree::Branch {
-
                key,
-
                forest: Box::new(forest.map(f)),
-
            },
-
        }
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct Tree<K, A>(pub NonEmpty<SubTree<K, A>>);
-

-
impl<K, A> From<Tree<K, A>> for Forest<K, A> {
-
    fn from(tree: Tree<K, A>) -> Self {
-
        Forest(Some(tree))
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct Forest<K, A>(pub Option<Tree<K, A>>);
-

-
impl<K, A> Tree<K, A> {
-
    /// Create a new `Tree` containing a single `Branch` given
-
    /// the key and sub-tree.
-
    pub fn branch(key: K, forest: Self) -> Self {
-
        Tree(NonEmpty::new(SubTree::branch(key, forest)))
-
    }
-

-
    /// Create a new `Tree` containing a single `Node`.
-
    pub fn node(key: K, value: A) -> Self {
-
        Tree(NonEmpty::new(SubTree::Node { key, value }))
-
    }
-

-
    /// Create a new `Tree` that creates a series of
-
    /// `Branch`es built using the `keys`. The final `Branch`
-
    /// will contain the `node`.
-
    fn new(keys: NonEmpty<K>, node: A) -> Self
-
    where
-
        K: Ord,
-
    {
-
        let (start, mut middle) = keys.into();
-
        let last = middle.pop();
-

-
        match last {
-
            None => Tree::node(start, node),
-
            Some(last) => {
-
                let mut branch = Tree::node(last, node);
-

-
                for key in middle.into_iter().rev() {
-
                    branch = Tree(NonEmpty::new(SubTree::branch(key, branch)))
-
                }
-

-
                Tree::branch(start, branch)
-
            },
-
        }
-
    }
-

-
    /// Perform a binary search in the sub-trees, based on comparing
-
    /// each of the sub-trees' key to the provided `key`.
-
    fn search(&self, key: &K) -> Result<usize, usize>
-
    where
-
        K: Ord,
-
    {
-
        self.0.binary_search_by(|tree| tree.key().cmp(key))
-
    }
-

-
    pub fn map<F, B>(self, mut f: F) -> Tree<K, B>
-
    where
-
        F: FnMut(A) -> B,
-
    {
-
        Tree(self.0.map(|tree| tree.map(&mut f)))
-
    }
-

-
    /// Insert a `node` into the list of sub-trees.
-
    ///
-
    /// The node's position will be based on the `Ord` instance
-
    /// of `K`.
-
    fn insert_node_with<F>(&mut self, key: K, value: A, f: F)
-
    where
-
        F: FnOnce(&mut A),
-
        K: Ord,
-
    {
-
        let result = self.search(&key);
-

-
        match result {
-
            Ok(index) => {
-
                let old_node = self.0.get_mut(index).unwrap();
-
                match old_node {
-
                    SubTree::Node { value: old, .. } => f(old),
-
                    SubTree::Branch { .. } => *old_node = SubTree::Node { key, value },
-
                }
-
            },
-
            Err(index) => self.0.insert(index, SubTree::Node { key, value }),
-
        }
-
    }
-

-
    /// Insert the `node` in the position given by `keys`.
-
    ///
-
    /// If the same path to a node is provided the `node` will replace the old
-
    /// one, i.e. if `a/b/c` exists in the tree and `a/b/c` is the full path
-
    /// to the node, then `c` will be replaced.
-
    ///
-
    /// If the path points to a branch, then the `node` will be inserted in this
-
    /// branch.
-
    ///
-
    /// If a portion of the path points to a node then a branch will be created
-
    /// in its place, i.e. if `a/b/c` exists in the tree and the provided
-
    /// path is `a/b/c/d`, then the node `c` will be replaced by a branch
-
    /// `c/d`.
-
    ///
-
    /// If the path does not exist it will be inserted into the set of
-
    /// sub-trees.
-
    fn insert_with<F>(&mut self, keys: NonEmpty<K>, value: A, f: F)
-
    where
-
        F: FnOnce(&mut A),
-
        K: Ord,
-
    {
-
        let (head, tail) = keys.into();
-
        let maybe_keys = NonEmpty::from_vec(tail);
-
        match self.search(&head) {
-
            // Found the label in our set of sub-trees
-
            Ok(index) => match maybe_keys {
-
                // The keys have been exhausted and so its time to insert the node
-
                None => {
-
                    let sub_tree = self.0.get_mut(index).unwrap();
-
                    match sub_tree {
-
                        // Our sub-tree was a node.
-
                        SubTree::Node { key, value } => {
-
                            let _ = std::mem::replace(key, head);
-
                            f(value);
-
                        },
-
                        SubTree::Branch { .. } => *sub_tree = SubTree::Node { key: head, value },
-
                    }
-
                },
-
                Some(keys) => {
-
                    let sub_tree = self.0.get_mut(index).unwrap();
-
                    match sub_tree {
-
                        // We have reached a node, but still have keys left to get through.
-
                        SubTree::Node { .. } => {
-
                            let new_tree = SubTree::branch(head, Tree::new(keys, value));
-
                            *sub_tree = new_tree
-
                        },
-
                        // We keep moving down the set of keys to find where to insert this node.
-
                        SubTree::Branch { forest, .. } => forest.insert_with(keys, value, f),
-
                    }
-
                },
-
            },
-
            // The label was not found and we have an index for insertion.
-
            Err(index) => match maybe_keys {
-
                // We create the branch with the head label and node, since there are
-
                // no more labels left.
-
                None => self.0.insert(index, SubTree::Node { key: head, value }),
-
                // We insert an entirely new branch with the full list of keys.
-
                Some(tail) => self
-
                    .0
-
                    .insert(index, SubTree::branch(head, Tree::new(tail, value))),
-
            },
-
        }
-
    }
-

-
    pub fn insert(&mut self, keys: NonEmpty<K>, value: A)
-
    where
-
        A: Clone,
-
        K: Ord,
-
    {
-
        self.insert_with(keys, value.clone(), |old| *old = value)
-
    }
-

-
    pub fn to_nonempty(&self) -> NonEmpty<A>
-
    where
-
        A: Clone,
-
        K: Clone,
-
    {
-
        self.0.clone().flat_map(|sub_tree| sub_tree.to_nonempty())
-
    }
-

-
    pub fn iter(&self) -> impl Iterator<Item = &A> {
-
        self.0.iter().flat_map(|tree| tree.iter())
-
    }
-

-
    pub fn iter_keys(&self) -> impl Iterator<Item = &K> {
-
        self.0.iter().flat_map(|tree| tree.iter_keys())
-
    }
-

-
    pub fn iter_subtrees(&self) -> impl Iterator<Item = &SubTree<K, A>> {
-
        self.0.iter()
-
    }
-

-
    pub fn find_node(&self, keys: NonEmpty<K>) -> Option<&A>
-
    where
-
        K: Ord,
-
    {
-
        self.find(keys).and_then(|tree| match tree {
-
            SubTree::Node { value, .. } => Some(value),
-
            SubTree::Branch { .. } => None,
-
        })
-
    }
-

-
    pub fn find_branch(&self, keys: NonEmpty<K>) -> Option<&Self>
-
    where
-
        K: Ord,
-
    {
-
        self.find(keys).and_then(|tree| match tree {
-
            SubTree::Node { .. } => None,
-
            SubTree::Branch { ref forest, .. } => Some(&**forest),
-
        })
-
    }
-

-
    /// Find a `SubTree` given a search path. If the path does not match
-
    /// it will return `None`.
-
    pub fn find(&self, keys: NonEmpty<K>) -> Option<&SubTree<K, A>>
-
    where
-
        K: Ord,
-
    {
-
        let (head, tail) = keys.into();
-
        let tail = NonEmpty::from_vec(tail);
-
        match self.search(&head) {
-
            Err(_) => None,
-
            Ok(index) => {
-
                let sub_tree = self.0.get(index).unwrap();
-
                match tail {
-
                    None => match sub_tree {
-
                        SubTree::Node { .. } => Some(sub_tree),
-
                        SubTree::Branch { .. } => Some(sub_tree),
-
                    },
-
                    Some(mut tail) => {
-
                        tail.insert(0, head);
-
                        sub_tree.find(tail)
-
                    },
-
                }
-
            },
-
        }
-
    }
-

-
    pub fn maximum_by<F>(&self, f: &F) -> &A
-
    where
-
        F: Fn(&A, &A) -> Ordering,
-
    {
-
        self.0.maximum_by(|s, t| s.compare_by(t, f)).maximum_by(f)
-
    }
-

-
    #[allow(dead_code)]
-
    pub fn maximum(&self) -> &A
-
    where
-
        A: Ord,
-
    {
-
        self.maximum_by(&|a, b| a.cmp(b))
-
    }
-
}
-

-
impl<K, A> Forest<K, A> {
-
    pub fn root() -> Self {
-
        Forest(None)
-
    }
-

-
    #[allow(dead_code)]
-
    pub fn is_empty(&self) -> bool {
-
        self.0.is_none()
-
    }
-

-
    fn insert_forest(&mut self, forest: Tree<K, A>) {
-
        self.0 = Some(forest)
-
    }
-

-
    /// Insert the `node` in the position given by `keys`.
-
    ///
-
    /// If the same path to a node is provided the `node` will replace the old
-
    /// one, i.e. if `a/b/c` exists in the tree and `a/b/c` is the full path
-
    /// to the node, then `c` will be replaced.
-
    ///
-
    /// If the path points to a branch, then the `node` will be inserted in this
-
    /// branch.
-
    ///
-
    /// If a portion of the path points to a node then a branch will be created
-
    /// in its place, i.e. if `a/b/c` exists in the tree and the provided
-
    /// path is `a/b/c/d`, then the node `c` will be replaced by a branch
-
    /// `c/d`.
-
    ///
-
    /// If the path does not exist it will be inserted into the set of
-
    /// sub-trees.
-
    #[allow(dead_code)]
-
    pub fn insert(&mut self, keys: NonEmpty<K>, node: A)
-
    where
-
        A: Clone,
-
        K: Ord,
-
    {
-
        self.insert_with(keys, node.clone(), |old| *old = node)
-
    }
-

-
    pub fn insert_with<F>(&mut self, keys: NonEmpty<K>, node: A, f: F)
-
    where
-
        F: FnOnce(&mut A),
-
        K: Ord,
-
    {
-
        let (prefix, node_key) = split_last(keys);
-
        match self.0.as_mut() {
-
            Some(forest) => match NonEmpty::from_vec(prefix) {
-
                None => {
-
                    // Insert the node at the root
-
                    forest.insert_node_with(node_key, node, f)
-
                },
-
                Some(mut keys) => {
-
                    keys.push(node_key);
-
                    forest.insert_with(keys, node, f)
-
                },
-
            },
-
            None => match NonEmpty::from_vec(prefix) {
-
                None => self.insert_forest(Tree::node(node_key, node)),
-
                Some(mut keys) => {
-
                    keys.push(node_key);
-
                    self.insert_forest(Tree::new(keys, node))
-
                },
-
            },
-
        }
-
    }
-

-
    pub fn find_node(&self, keys: NonEmpty<K>) -> Option<&A>
-
    where
-
        K: Ord,
-
    {
-
        self.0.as_ref().and_then(|trees| trees.find_node(keys))
-
    }
-

-
    pub fn find_branch(&self, keys: NonEmpty<K>) -> Option<&Tree<K, A>>
-
    where
-
        K: Ord,
-
    {
-
        self.0.as_ref().and_then(|trees| trees.find_branch(keys))
-
    }
-

-
    #[allow(dead_code)]
-
    /// Find a `SubTree` given a search path. If the path does not match
-
    /// it will return `None`.
-
    pub fn find(&self, keys: NonEmpty<K>) -> Option<&SubTree<K, A>>
-
    where
-
        K: Ord,
-
    {
-
        self.0.as_ref().and_then(|trees| trees.find(keys))
-
    }
-

-
    #[allow(dead_code)]
-
    pub fn maximum_by<F>(&self, f: F) -> Option<&A>
-
    where
-
        F: Fn(&A, &A) -> Ordering,
-
    {
-
        self.0.as_ref().map(|trees| trees.maximum_by(&f))
-
    }
-

-
    pub fn iter(&self) -> impl Iterator<Item = &A> {
-
        self.0.iter().flat_map(|trees| trees.iter())
-
    }
-

-
    #[allow(dead_code)]
-
    pub fn iter_keys(&self) -> impl Iterator<Item = &K> {
-
        self.0.iter().flat_map(|trees| trees.iter_keys())
-
    }
-
}
modified radicle-surf/src/vcs/git/error.rs
@@ -89,38 +89,3 @@ pub enum Error {
    #[error(transparent)]
    Git(#[from] git2::Error),
}
-

-
/// A private enum that captures a recoverable and
-
/// non-recoverable error when walking the git tree.
-
///
-
/// In the case of `NotBlob` we abort the the computation but do
-
/// a check for it and recover.
-
///
-
/// In the of `Git` we abort both computations.
-
#[derive(Debug, Error)]
-
pub(crate) enum TreeWalkError {
-
    #[error("entry is not a blob")]
-
    NotBlob,
-
    #[error("git object is a commit")]
-
    Commit,
-
    #[error(transparent)]
-
    Git(#[from] Error),
-
}
-

-
impl From<git2::Error> for TreeWalkError {
-
    fn from(err: git2::Error) -> Self {
-
        TreeWalkError::Git(err.into())
-
    }
-
}
-

-
impl From<file_system::Error> for TreeWalkError {
-
    fn from(err: file_system::Error) -> Self {
-
        err.into()
-
    }
-
}
-

-
impl From<str::Utf8Error> for TreeWalkError {
-
    fn from(err: str::Utf8Error) -> Self {
-
        err.into()
-
    }
-
}
modified radicle-surf/src/vcs/git/repo.rs
@@ -18,7 +18,7 @@
use crate::{
    diff::*,
    file_system,
-
    file_system::directory,
+
    file_system::{directory, DirectoryContents, Label},
    vcs,
    vcs::{
        git::{
@@ -37,10 +37,11 @@ use crate::{
        Vcs,
    },
};
+
use directory::Directory;
use nonempty::NonEmpty;
use radicle_git_ext::Oid;
use std::{
-
    collections::{BTreeSet, HashMap, HashSet},
+
    collections::{BTreeSet, HashSet},
    convert::TryFrom,
    str,
};
@@ -163,14 +164,17 @@ impl<'a> RepositoryRef<'a> {
    }

    /// Get the [`Diff`] between two commits.
-
    pub fn diff(&self, from: Oid, to: Oid) -> Result<Diff, Error> {
-
        self.diff_commits(None, Some(from.into()), to.into())
+
    pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error> {
+
        let from_commit = self.rev_to_commit(from)?;
+
        let to_commit = self.rev_to_commit(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 with no parents.
-
    pub fn initial_diff(&self, oid: Oid) -> Result<Diff, Error> {
-
        self.diff_commits(None, None, oid.into())
+
    pub fn initial_diff(&self, rev: &Rev) -> Result<Diff, Error> {
+
        let commit = self.rev_to_commit(rev)?;
+
        self.diff_commits(None, None, &commit)
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
    }

@@ -180,11 +184,9 @@ impl<'a> RepositoryRef<'a> {
    }

    /// Gets a snapshot of the repo as a Directory.
-
    pub fn snapshot(&self, rev: &Rev) -> Result<directory::Directory, Error> {
+
    pub fn snapshot(&self, rev: &Rev) -> Result<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))
+
        self.directory_of_commit(&commit)
    }

    /// Returns the last commit, if exists, for a `path` in the history of
@@ -254,6 +256,12 @@ impl<'a> RepositoryRef<'a> {
        Ok(head_commit.id().into())
    }

+
    /// Returns the Oid of `rev`.
+
    pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error> {
+
        let commit = self.rev_to_commit(rev)?;
+
        Ok(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())?),
@@ -406,9 +414,8 @@ impl<'a> RepositoryRef<'a> {
        commit: &git2::Commit,
    ) -> Result<Option<file_system::Path>, Error> {
        let mut parents = commit.parents();
-
        let parent = parents.next().map(|c| c.id());

-
        let diff = self.diff_commits(Some(path), parent, commit.id())?;
+
        let diff = self.diff_commits(Some(path), parents.next().as_ref(), commit)?;
        if let Some(_delta) = diff.deltas().next() {
            Ok(Some(path.clone()))
        } else {
@@ -419,13 +426,11 @@ impl<'a> RepositoryRef<'a> {
    fn diff_commits(
        &self,
        path: Option<&file_system::Path>,
-
        from: Option<git2::Oid>,
-
        to: git2::Oid,
+
        from: Option<&git2::Commit>,
+
        to: &git2::Commit,
    ) -> Result<git2::Diff, Error> {
-
        let new_tree = self.repo_ref.find_commit(to)?.tree()?;
-
        let old_tree = from.map_or(Ok(None), |oid| {
-
            self.repo_ref.find_commit(oid)?.tree().map(Some)
-
        })?;
+
        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 {
@@ -441,97 +446,98 @@ 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())?;
+
    /// Generates a Directory for the commit.
+
    fn directory_of_commit(&self, commit: &git2::Commit) -> Result<Directory, Error> {
+
        let mut parent_dirs = vec![Directory::root()];
        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),
+
            let tree_level = s.split('/').count();
+
            if tree_level < parent_dirs.len() {
+
                // As it is PreOrder, the last directory A was visited
+
                // completely and we are back to the level. Now insert A
+
                // into its parent directory.
+
                if let Some(last_dir) = parent_dirs.pop() {
+
                    if let Some(parent) = parent_dirs.last_mut() {
+
                        let name = last_dir.name().clone();
+
                        let content = DirectoryContents::Directory(last_dir);
+
                        parent.insert(name, content);
+
                    }
+
                }
+
            }

-
                        // We don't need to update, we want to keep the error.
-
                        Err(_err) => {},
+
            match entry.kind() {
+
                Some(git2::ObjectType::Tree) => {
+
                    if let Some(name) = entry.name() {
+
                        // Add a new level of directory.
+
                        match name.parse() {
+
                            Ok(label) => parent_dirs.push(Directory::new(label)),
+
                            Err(_) => {
+
                                return git2::TreeWalkResult::Abort;
+
                            },
+
                        }
                    }
-
                    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
-
                    },
+
                Some(git2::ObjectType::Blob) => {
+
                    // Construct a File to insert into its parent directory.
+
                    let object = match entry.to_object(self.repo_ref) {
+
                        Ok(obj) => obj,
+
                        Err(_) => {
+
                            return git2::TreeWalkResult::Abort;
+
                        },
+
                    };
+
                    let blob = match object.as_blob() {
+
                        Some(b) => b,
+
                        None => return git2::TreeWalkResult::Abort,
+
                    };
+
                    let f = directory::File::new(blob.content());
+
                    let label = match entry.name() {
+
                        Some(name) => match name.parse::<Label>() {
+
                            Ok(label) => label,
+
                            Err(_) => {
+
                                return git2::TreeWalkResult::Abort;
+
                            },
+
                        },
+
                        None => return git2::TreeWalkResult::Abort,
+
                    };
+
                    let content = DirectoryContents::File {
+
                        name: label.clone(),
+
                        file: f,
+
                    };
+
                    let parent = match parent_dirs.last_mut() {
+
                        Some(parent_dir) => parent_dir,
+
                        None => return git2::TreeWalkResult::Abort,
+
                    };
+
                    parent.insert(label, content);
+
                },
+
                _ => {
+
                    return git2::TreeWalkResult::Skip;
                },
            }
-
        })?;
-

-
        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)
-
        }?;
+
            git2::TreeWalkResult::Ok
+
        })?;

-
        // 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);
+
        // Tree walk is complete but there are some levels of dirs
+
        // that are not popped up from `parent_dirs` yet. Note that
+
        // the root dir is `parent_dirs[0]`.
+
        //
+
        // We need to pop up `parent_dirs` fully and update the directory
+
        // content at each level.
+
        while let Some(curr_dir) = parent_dirs.pop() {
+
            match parent_dirs.last_mut() {
+
                Some(parent) => {
+
                    let name = curr_dir.name().clone();
+
                    let content = DirectoryContents::Directory(curr_dir);
+
                    parent.insert(name, content);
+
                },
+
                None => return Ok(curr_dir), // No more parent, we're at the root.
+
            }
        }

-
        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(),
-
            },
-
        ))
+
        Err(Error::RevParseFailure {
+
            rev: commit.id().to_string(),
+
        })
    }
}

modified radicle-surf/t/src/file_system.rs
@@ -5,7 +5,7 @@

#[cfg(test)]
mod list_directory {
-
    use radicle_surf::file_system::{unsound, Directory, File, SystemType};
+
    use radicle_surf::file_system::{unsound, Directory, File, Label};

    #[test]
    fn root_files() {
@@ -23,12 +23,13 @@ mod list_directory {
            File::new(b"module BananaBaz ..."),
        );

+
        let files: Vec<Label> = directory.contents().map(|c| c.label().clone()).collect();
        assert_eq!(
-
            directory.list_directory(),
+
            files,
            vec![
-
                SystemType::file(unsound::label::new("bar.hs")),
-
                SystemType::file(unsound::label::new("baz.hs")),
-
                SystemType::file(unsound::label::new("foo.hs")),
+
                "bar.hs".parse::<Label>().unwrap(),
+
                "baz.hs".parse::<Label>().unwrap(),
+
                "foo.hs".parse::<Label>().unwrap(),
            ]
        );
    }
@@ -42,11 +43,11 @@ mod find_file {
    fn in_root() {
        let file = File::new(b"module Banana ...");
        let mut directory = Directory::root();
-
        directory.insert_file(unsound::path::new("foo.hs"), file.clone());
+
        directory.insert_file(unsound::path::new("bar/foo.hs"), file.clone());

        assert_eq!(
-
            directory.find_file(unsound::path::new("foo.hs")),
-
            Some(file)
+
            directory.find_file(unsound::path::new("bar/foo.hs")),
+
            Some(&file)
        );
    }

@@ -90,113 +91,6 @@ mod directory_size {
}

#[cfg(test)]
-
mod properties {
-
    use nonempty::NonEmpty;
-
    use proptest::{collection, prelude::*};
-
    use radicle_surf::file_system::{unsound, *};
-
    use std::collections::HashMap;
-

-
    #[test]
-
    fn test_all_directories_and_files() {
-
        let mut directory_map = HashMap::new();
-

-
        let path1 = unsound::path::new("foo/bar/baz");
-
        let file1 = (unsound::label::new("monadic.rs"), File::new(&[]));
-
        let file2 = (unsound::label::new("oscoin.rs"), File::new(&[]));
-
        directory_map.insert(path1, NonEmpty::from((file1, vec![file2])));
-

-
        let path2 = unsound::path::new("foor/bar/quuz");
-
        let file3 = (unsound::label::new("radicle.rs"), File::new(&[]));
-

-
        directory_map.insert(path2, NonEmpty::new(file3));
-

-
        assert!(prop_all_directories_and_files(directory_map))
-
    }
-

-
    fn label_strategy() -> impl Strategy<Value = Label> {
-
        // ASCII regex, excluding '/' because of posix file paths
-
        "[ -.|0-~]+".prop_map(|label| unsound::label::new(&label))
-
    }
-

-
    fn path_strategy(max_size: usize) -> impl Strategy<Value = Path> {
-
        (
-
            label_strategy(),
-
            collection::vec(label_strategy(), 0..max_size),
-
        )
-
            .prop_map(|(label, labels)| Path((label, labels).into()))
-
    }
-

-
    fn file_strategy() -> impl Strategy<Value = (Label, File)> {
-
        // ASCII regex, see: https://catonmat.net/my-favorite-regex
-
        (label_strategy(), "[ -~]*")
-
            .prop_map(|(name, contents)| (name, File::new(contents.as_bytes())))
-
    }
-

-
    fn directory_map_strategy(
-
        path_size: usize,
-
        n_files: usize,
-
        map_size: usize,
-
    ) -> impl Strategy<Value = HashMap<Path, NonEmpty<(Label, File)>>> {
-
        collection::hash_map(
-
            path_strategy(path_size),
-
            collection::vec(file_strategy(), 1..n_files).prop_map(|files| {
-
                NonEmpty::from_slice(&files).expect("Strategy generated files of length 0")
-
            }),
-
            0..map_size,
-
        )
-
    }
-

-
    // TODO(fintan): This is a bit slow. Could be time to benchmark some functions.
-
    proptest! {
-
        #[test]
-
        fn prop_test_all_directories_and_files(directory_map in directory_map_strategy(10, 10, 10)) {
-
            prop_all_directories_and_files(directory_map);
-
        }
-
    }
-

-
    fn prop_all_directories_and_files(
-
        directory_map: HashMap<Path, NonEmpty<(Label, File)>>,
-
    ) -> bool {
-
        let mut new_directory_map = HashMap::new();
-
        for (path, files) in directory_map {
-
            new_directory_map.insert(path.clone(), files);
-
        }
-

-
        let directory = Directory::from_hash_map(new_directory_map.clone());
-

-
        for (directory_path, files) in new_directory_map {
-
            for (file_name, _) in files.iter() {
-
                let mut path = directory_path.clone();
-
                if directory.find_directory(path.clone()).is_none() {
-
                    eprintln!("Search Directory: {:#?}", directory);
-
                    eprintln!("Path to find: {:#?}", path);
-
                    return false;
-
                }
-

-
                path.push(file_name.clone());
-
                if directory.find_file(path.clone()).is_none() {
-
                    eprintln!("Search Directory: {:#?}", directory);
-
                    eprintln!("Path to find: {:#?}", path);
-
                    return false;
-
                }
-
            }
-
        }
-
        true
-
    }
-

-
    #[test]
-
    fn test_file_name_is_same_as_root() {
-
        // This test ensures that if the name is the same the root of the
-
        // directory, that search_path.split_last() doesn't toss away the prefix.
-
        let path = unsound::path::new("foo/bar/~");
-
        let mut directory_map = HashMap::new();
-
        directory_map.insert(path, NonEmpty::new((Label::root(), File::new(b"root"))));
-

-
        assert!(prop_all_directories_and_files(directory_map));
-
    }
-
}
-

-
#[cfg(test)]
mod path {
    use radicle_surf::file_system::unsound;

modified radicle-surf/t/src/git.rs
@@ -7,7 +7,7 @@
use radicle_surf::git::{Author, BranchType, Commit};
use radicle_surf::{
    diff::*,
-
    file_system::{unsound, Path},
+
    file_system::{unsound, DirectoryContents, Path},
    git::{error::Error, Branch, BranchName, Namespace, Oid, RefScope, Repository, Rev, TagName},
};

@@ -402,7 +402,7 @@ mod diff {
        assert!(commit.parents().count() == 0);
        assert!(commit.parent(0).is_err());

-
        let diff = repo.initial_diff(oid)?;
+
        let diff = repo.initial_diff(&oid.into())?;

        let expected_diff = Diff {
            created: vec![CreateFile {
@@ -433,11 +433,11 @@ mod diff {
    fn test_diff() -> Result<(), Error> {
        let repo = Repository::new(GIT_PLATINUM)?;
        let repo = repo.as_ref();
-
        let commit = repo
-
            .get_git2_commit(Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?)
-
            .unwrap();
+
        let oid = Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?;
+
        let commit = repo.get_git2_commit(oid).unwrap();
        let parent = commit.parent(0)?;
-
        let diff = repo.diff(parent.id().into(), commit.id().into())?;
+
        let parent_oid: Oid = parent.id().into();
+
        let diff = repo.diff(&parent_oid.into(), &oid.into())?;

        let expected_diff = Diff {
                created: vec![],
@@ -784,3 +784,33 @@ mod reference {
        Ok(())
    }
}
+

+
mod code_browsing {
+
    use super::*;
+
    use radicle_surf::file_system::Directory;
+

+
    #[test]
+
    fn iterate_root_dir_recursive() {
+
        let repo = Repository::new(GIT_PLATINUM).unwrap();
+
        let repo = repo.as_ref();
+
        let root_dir = repo.snapshot(&Branch::local("master").into()).unwrap();
+
        let count = println_dir(&root_dir, 0);
+
        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, indent_level: usize) -> i32 {
+
            let mut count = 0;
+
            for item in dir.contents() {
+
                println!("> {}{}", " ".repeat(indent_level * 4), &item.label());
+
                count += 1;
+
                if let DirectoryContents::Directory(sub_dir) = item {
+
                    count += println_dir(sub_dir, indent_level + 1);
+
                }
+
            }
+
            count
+
        }
+
    }
+
}
modified radicle-surf/t/src/lib.rs
@@ -9,6 +9,3 @@ mod diff;

#[cfg(test)]
mod file_system;
-

-
#[cfg(test)]
-
mod tree;
deleted radicle-surf/t/src/tree.rs
@@ -1,692 +0,0 @@
-
// Copyright © 2022 The Radicle Git Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
//! Unit tests for radicle_surf::tree.
-

-
use nonempty::NonEmpty;
-
use pretty_assertions::assert_eq;
-
use radicle_surf::tree::{Forest, SubTree, Tree};
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
struct TestNode {
-
    id: u32,
-
}
-

-
#[test]
-
fn test_is_empty() {
-
    let mut tree = Forest::root();
-
    assert!(tree.is_empty());
-

-
    let a_node = TestNode { id: 1 };
-

-
    tree.insert(NonEmpty::new(String::from("a")), a_node);
-
    assert!(!tree.is_empty());
-
}
-

-
#[test]
-
fn test_insert_root_node() {
-
    let a_label = String::from("a");
-

-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-

-
    tree.insert(NonEmpty::new(a_label), a_node.clone());
-

-
    assert_eq!(tree, Forest(Some(Tree::node(String::from("a"), a_node))));
-
}
-

-
#[test]
-
fn test_insert_with_prepending_root_nodes() {
-
    let a_label = String::from("a");
-

-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-
    let b_node = TestNode { id: 2 };
-

-
    tree.insert_with(
-
        NonEmpty::new(a_label.clone()),
-
        NonEmpty::new(a_node.clone()),
-
        |nodes| nodes.insert(0, a_node.clone()),
-
    );
-
    tree.insert_with(
-
        NonEmpty::new(a_label),
-
        NonEmpty::new(b_node.clone()),
-
        |nodes| nodes.insert(0, b_node.clone()),
-
    );
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::node(
-
            String::from("a"),
-
            NonEmpty::from((b_node, vec![a_node]))
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_with_prepending_branch_nodes() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let path = NonEmpty::from((a_label, vec![b_label]));
-

-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-
    let b_node = TestNode { id: 2 };
-

-
    tree.insert_with(path.clone(), NonEmpty::new(a_node.clone()), |nodes| {
-
        nodes.insert(0, a_node.clone())
-
    });
-
    tree.insert_with(path, NonEmpty::new(b_node.clone()), |nodes| {
-
        nodes.insert(0, b_node.clone())
-
    });
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::node(String::from("b"), NonEmpty::from((b_node, vec![a_node])))
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_single_node() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let path = NonEmpty::from((a_label, vec![b_label, c_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(path, c_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::branch(String::from("b"), Tree::node(String::from("c"), c_node))
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_two_nodes() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let d_label = String::from("d");
-
    let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
-
    let d_path = NonEmpty::from((a_label, vec![b_label, d_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(c_path, c_node.clone());
-

-
    let d_node = TestNode { id: 3 };
-

-
    tree.insert(d_path, d_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::branch(
-
                String::from("b"),
-
                Tree(NonEmpty::from((
-
                    SubTree::Node {
-
                        key: String::from("c"),
-
                        value: c_node
-
                    },
-
                    vec![SubTree::Node {
-
                        key: String::from("d"),
-
                        value: d_node
-
                    }]
-
                )))
-
            )
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_replaces_node() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let c_path = NonEmpty::from((a_label, vec![b_label, c_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(c_path.clone(), c_node);
-

-
    let new_c_node = TestNode { id: 3 };
-

-
    tree.insert(c_path, new_c_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::branch(
-
                String::from("b"),
-
                Tree(NonEmpty::new(SubTree::Node {
-
                    key: String::from("c"),
-
                    value: new_c_node
-
                },))
-
            )
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_replaces_root_node() {
-
    let c_label = String::from("c");
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(NonEmpty::new(c_label.clone()), c_node);
-

-
    let new_c_node = TestNode { id: 3 };
-

-
    tree.insert(NonEmpty::new(c_label), new_c_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::node(String::from("c"), new_c_node)))
-
    );
-
}
-

-
#[test]
-
fn test_insert_replaces_branch_node() {
-
    let a_label = String::from("a");
-
    let c_label = String::from("c");
-
    let c_path = NonEmpty::from((a_label, vec![c_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(c_path.clone(), c_node);
-

-
    let new_c_node = TestNode { id: 3 };
-

-
    tree.insert(c_path, new_c_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::node(String::from("c"), new_c_node),
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_replaces_branch_with_node() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(c_path, c_node);
-

-
    let new_c_node = TestNode { id: 3 };
-

-
    tree.insert(NonEmpty::from((a_label, vec![b_label])), new_c_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::node(String::from("b"), new_c_node),
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_replaces_node_with_branch() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()]));
-
    let c_path = NonEmpty::from((a_label, vec![b_label, c_label]));
-

-
    let mut tree = Forest::root();
-

-
    let b_node = TestNode { id: 1 };
-

-
    tree.insert(b_path, b_node);
-

-
    let new_c_node = TestNode { id: 3 };
-

-
    tree.insert(c_path, new_c_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::branch(
-
                String::from("b"),
-
                Tree(NonEmpty::new(SubTree::Node {
-
                    key: String::from("c"),
-
                    value: new_c_node
-
                },))
-
            )
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_replaces_node_with_branch_foo() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let d_label = String::from("d");
-
    let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()]));
-
    let d_path = NonEmpty::from((a_label, vec![b_label, c_label, d_label]));
-

-
    let mut tree = Forest::root();
-

-
    let b_node = TestNode { id: 1 };
-

-
    tree.insert(b_path, b_node);
-

-
    let d_node = TestNode { id: 3 };
-

-
    tree.insert(d_path, d_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::branch(
-
                String::from("b"),
-
                Tree::branch(String::from("c"), Tree::node(String::from("d"), d_node))
-
            )
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_two_nodes_out_of_order() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let d_label = String::from("d");
-
    let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
-
    let d_path = NonEmpty::from((a_label, vec![b_label, d_label]));
-

-
    let mut tree = Forest::root();
-

-
    let d_node = TestNode { id: 3 };
-

-
    tree.insert(d_path, d_node.clone());
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(c_path, c_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree::branch(
-
                String::from("b"),
-
                Tree(NonEmpty::from((
-
                    SubTree::Node {
-
                        key: String::from("c"),
-
                        value: c_node
-
                    },
-
                    vec![SubTree::Node {
-
                        key: String::from("d"),
-
                        value: d_node
-
                    }]
-
                )))
-
            )
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_branch() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let d_label = String::from("d");
-
    let e_label = String::from("e");
-
    let f_label = String::from("f");
-

-
    let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
-
    let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label]));
-
    let f_path = NonEmpty::from((a_label, vec![e_label, f_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    let d_node = TestNode { id: 3 };
-

-
    let f_node = TestNode { id: 2 };
-

-
    tree.insert(d_path, d_node.clone());
-
    tree.insert(c_path, c_node.clone());
-
    tree.insert(f_path, f_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree(NonEmpty::from((
-
                SubTree::Branch {
-
                    key: String::from("b"),
-
                    forest: Box::new(Tree(NonEmpty::from((
-
                        SubTree::Node {
-
                            key: String::from("c"),
-
                            value: c_node
-
                        },
-
                        vec![SubTree::Node {
-
                            key: String::from("d"),
-
                            value: d_node
-
                        }]
-
                    ))))
-
                },
-
                vec![SubTree::Branch {
-
                    key: String::from("e"),
-
                    forest: Box::new(Tree::node(String::from("f"), f_node))
-
                },]
-
            )))
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_insert_two_branches() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let d_label = String::from("d");
-
    let e_label = String::from("e");
-
    let f_label = String::from("f");
-

-
    let c_path = NonEmpty::from((a_label, vec![b_label, c_label]));
-
    let f_path = NonEmpty::from((d_label, vec![e_label, f_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    let f_node = TestNode { id: 2 };
-

-
    tree.insert(c_path, c_node.clone());
-
    tree.insert(f_path, f_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree(NonEmpty::from((
-
            SubTree::Branch {
-
                key: String::from("a"),
-
                forest: Box::new(Tree::branch(
-
                    String::from("b"),
-
                    Tree::node(String::from("c"), c_node)
-
                )),
-
            },
-
            vec![SubTree::Branch {
-
                key: String::from("d"),
-
                forest: Box::new(Tree::branch(
-
                    String::from("e"),
-
                    Tree::node(String::from("f"), f_node)
-
                ))
-
            }]
-
        )))))
-
    );
-
}
-

-
#[test]
-
fn test_insert_branches_and_node() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let d_label = String::from("d");
-
    let e_label = String::from("e");
-
    let f_label = String::from("f");
-
    let g_label = String::from("g");
-

-
    let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
-
    let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label]));
-
    let e_path = NonEmpty::from((a_label.clone(), vec![e_label]));
-
    let g_path = NonEmpty::from((a_label, vec![f_label, g_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    let d_node = TestNode { id: 3 };
-

-
    let e_node = TestNode { id: 2 };
-

-
    let g_node = TestNode { id: 2 };
-

-
    tree.insert(d_path, d_node.clone());
-
    tree.insert(c_path, c_node.clone());
-
    tree.insert(e_path, e_node.clone());
-
    tree.insert(g_path, g_node.clone());
-

-
    assert_eq!(
-
        tree,
-
        Forest(Some(Tree::branch(
-
            String::from("a"),
-
            Tree(NonEmpty::from((
-
                SubTree::Branch {
-
                    key: String::from("b"),
-
                    forest: Box::new(Tree(NonEmpty::from((
-
                        SubTree::Node {
-
                            key: String::from("c"),
-
                            value: c_node
-
                        },
-
                        vec![SubTree::Node {
-
                            key: String::from("d"),
-
                            value: d_node
-
                        }]
-
                    ))))
-
                },
-
                vec![
-
                    SubTree::Node {
-
                        key: String::from("e"),
-
                        value: e_node
-
                    },
-
                    SubTree::Branch {
-
                        key: String::from("f"),
-
                        forest: Box::new(Tree::node(String::from("g"), g_node))
-
                    },
-
                ]
-
            )))
-
        )))
-
    );
-
}
-

-
#[test]
-
fn test_find_root_node() {
-
    let a_label = String::from("a");
-

-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-

-
    tree.insert(NonEmpty::new(a_label), a_node.clone());
-

-
    assert_eq!(
-
        tree.find(NonEmpty::new(String::from("a"))),
-
        Some(&SubTree::Node {
-
            key: String::from("a"),
-
            value: a_node
-
        })
-
    );
-

-
    assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None);
-
}
-

-
#[test]
-
fn test_find_branch_and_node() {
-
    let a_label = String::from("a");
-
    let b_label = String::from("b");
-
    let c_label = String::from("c");
-
    let path = NonEmpty::from((a_label, vec![b_label, c_label]));
-

-
    let mut tree = Forest::root();
-

-
    let c_node = TestNode { id: 1 };
-

-
    tree.insert(path, c_node.clone());
-

-
    assert_eq!(
-
        tree.find(NonEmpty::new(String::from("a"))),
-
        Some(&SubTree::Branch {
-
            key: String::from("a"),
-
            forest: Box::new(Tree::branch(
-
                String::from("b"),
-
                Tree::node(String::from("c"), c_node.clone())
-
            ))
-
        })
-
    );
-

-
    assert_eq!(
-
        tree.find(NonEmpty::from((String::from("a"), vec![String::from("b")]))),
-
        Some(&SubTree::Branch {
-
            key: String::from("b"),
-
            forest: Box::new(Tree::node(String::from("c"), c_node.clone()))
-
        })
-
    );
-

-
    assert_eq!(
-
        tree.find(NonEmpty::from((
-
            String::from("a"),
-
            vec![String::from("b"), String::from("c")]
-
        ))),
-
        Some(&SubTree::Node {
-
            key: String::from("c"),
-
            value: c_node
-
        })
-
    );
-

-
    assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None);
-

-
    assert_eq!(
-
        tree.find(NonEmpty::from((String::from("a"), vec![String::from("c")]))),
-
        None
-
    );
-
}
-

-
#[test]
-
fn test_maximum_by_root_nodes() {
-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-

-
    let b_node = TestNode { id: 3 };
-

-
    tree.insert(NonEmpty::new(String::from("a")), a_node.clone());
-
    tree.insert(NonEmpty::new(String::from("b")), b_node.clone());
-

-
    assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
-
    assert_eq!(
-
        tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
-
        Some(&a_node)
-
    );
-
}
-

-
#[test]
-
fn test_maximum_by_branch_and_node() {
-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-

-
    let b_node = TestNode { id: 3 };
-

-
    tree.insert(
-
        NonEmpty::from((String::from("c"), vec![String::from("a")])),
-
        a_node.clone(),
-
    );
-
    tree.insert(NonEmpty::new(String::from("b")), b_node.clone());
-

-
    assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
-
    assert_eq!(
-
        tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
-
        Some(&a_node)
-
    );
-
}
-

-
#[test]
-
fn test_maximum_by_branch_and_branch() {
-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-

-
    let b_node = TestNode { id: 3 };
-

-
    tree.insert(
-
        NonEmpty::from((String::from("c"), vec![String::from("a")])),
-
        a_node.clone(),
-
    );
-
    tree.insert(
-
        NonEmpty::from((String::from("d"), vec![String::from("a")])),
-
        b_node.clone(),
-
    );
-

-
    assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
-
    assert_eq!(
-
        tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
-
        Some(&a_node)
-
    );
-
}
-

-
#[test]
-
fn test_maximum_by_branch_nodes() {
-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-

-
    let b_node = TestNode { id: 3 };
-

-
    tree.insert(
-
        NonEmpty::from((String::from("c"), vec![String::from("a")])),
-
        a_node.clone(),
-
    );
-
    tree.insert(
-
        NonEmpty::from((String::from("c"), vec![String::from("b")])),
-
        b_node.clone(),
-
    );
-

-
    assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
-
    assert_eq!(
-
        tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
-
        Some(&a_node)
-
    );
-
}
-

-
#[test]
-
fn test_fold_root_nodes() {
-
    let mut tree = Forest::root();
-

-
    let a_node = TestNode { id: 1 };
-

-
    let b_node = TestNode { id: 3 };
-

-
    tree.insert(NonEmpty::new(String::from("a")), a_node);
-
    tree.insert(NonEmpty::new(String::from("b")), b_node);
-

-
    assert_eq!(tree.iter().fold(0, |b, a| a.id + b), 4);
-
}