Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'han/test-reorg'
Fintan Halpenny committed 3 years ago
commit 93d92ede2dd9bce0dec79b7106ff8bcf346a7a14
parent d7b3da8
21 files changed +2105 -2042
modified radicle-surf/Cargo.toml
@@ -15,6 +15,10 @@ include = [
    "data/git-platinum.tgz",
]

+
[lib]
+
test = false
+
doctest = false
+

[features]
serialize = ["serde"]
# NOTE: testing `test_submodule_failure` on GH actions
modified radicle-surf/src/diff.rs
@@ -594,239 +594,3 @@ impl Diff {
        }
    }
}
-

-
#[cfg(test)]
-
mod tests {
-
    use crate::{
-
        diff::*,
-
        file_system::{unsound, *},
-
    };
-
    use pretty_assertions::assert_eq;
-

-
    #[test]
-
    fn test_create_file() {
-
        let directory = Directory::root();
-

-
        let mut new_directory = Directory::root();
-
        new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
-

-
        let diff = Diff::diff(directory, new_directory);
-

-
        let expected_diff = Diff {
-
            created: vec![CreateFile {
-
                path: Path::with_root(&[unsound::label::new("banana.rs")]),
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
            }],
-
            deleted: vec![],
-
            copied: vec![],
-
            moved: vec![],
-
            modified: vec![],
-
        };
-

-
        assert_eq!(diff, expected_diff)
-
    }
-

-
    #[test]
-
    fn test_delete_file() {
-
        let mut directory = Directory::root();
-
        directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
-

-
        let new_directory = Directory::root();
-

-
        let diff = Diff::diff(directory, new_directory);
-

-
        let expected_diff = Diff {
-
            created: vec![],
-
            deleted: vec![DeleteFile {
-
                path: Path::with_root(&[unsound::label::new("banana.rs")]),
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
            }],
-
            moved: vec![],
-
            copied: vec![],
-
            modified: vec![],
-
        };
-

-
        assert_eq!(diff, expected_diff)
-
    }
-

-
    /* TODO(fintan): Move is not detected yet
-
    #[test]
-
    fn test_moved_file() {
-
        let mut directory = Directory::root();
-
        directory.insert_file(&unsound::path::new("mod.rs"), File::new(b"use banana"));
-

-
        let mut new_directory = Directory::root();
-
        new_directory.insert_file(&unsound::path::new("banana.rs"), File::new(b"use banana"));
-

-
        let diff = Diff::diff(directory, new_directory).expect("diff failed");
-

-
        assert_eq!(diff, Diff::new())
-
    }
-
    */
-

-
    #[test]
-
    fn test_modify_file() {
-
        let mut directory = Directory::root();
-
        directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
-

-
        let mut new_directory = Directory::root();
-
        new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana;"));
-

-
        let diff = Diff::diff(directory, new_directory);
-

-
        let expected_diff = Diff {
-
            created: vec![],
-
            deleted: vec![],
-
            moved: vec![],
-
            copied: vec![],
-
            modified: vec![ModifiedFile {
-
                path: Path::with_root(&[unsound::label::new("banana.rs")]),
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
                eof: None,
-
            }],
-
        };
-

-
        assert_eq!(diff, expected_diff)
-
    }
-

-
    #[test]
-
    fn test_create_directory() {
-
        let directory = Directory::root();
-

-
        let mut new_directory = Directory::root();
-
        new_directory.insert_file(
-
            unsound::path::new("src/banana.rs"),
-
            File::new(b"use banana"),
-
        );
-

-
        let diff = Diff::diff(directory, new_directory);
-

-
        let expected_diff = Diff {
-
            created: vec![CreateFile {
-
                path: Path::with_root(&[
-
                    unsound::label::new("src"),
-
                    unsound::label::new("banana.rs"),
-
                ]),
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
            }],
-
            deleted: vec![],
-
            moved: vec![],
-
            copied: vec![],
-
            modified: vec![],
-
        };
-

-
        assert_eq!(diff, expected_diff)
-
    }
-

-
    #[test]
-
    fn test_delete_directory() {
-
        let mut directory = Directory::root();
-
        directory.insert_file(
-
            unsound::path::new("src/banana.rs"),
-
            File::new(b"use banana"),
-
        );
-

-
        let new_directory = Directory::root();
-

-
        let diff = Diff::diff(directory, new_directory);
-

-
        let expected_diff = Diff {
-
            created: vec![],
-
            deleted: vec![DeleteFile {
-
                path: Path::with_root(&[
-
                    unsound::label::new("src"),
-
                    unsound::label::new("banana.rs"),
-
                ]),
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
            }],
-
            moved: vec![],
-
            copied: vec![],
-
            modified: vec![],
-
        };
-

-
        assert_eq!(diff, expected_diff)
-
    }
-

-
    #[test]
-
    fn test_modify_file_directory() {
-
        let mut directory = Directory::root();
-
        directory.insert_file(
-
            unsound::path::new("src/banana.rs"),
-
            File::new(b"use banana"),
-
        );
-

-
        let mut new_directory = Directory::root();
-
        new_directory.insert_file(
-
            unsound::path::new("src/banana.rs"),
-
            File::new(b"use banana;"),
-
        );
-

-
        let diff = Diff::diff(directory, new_directory);
-

-
        let expected_diff = Diff {
-
            created: vec![],
-
            deleted: vec![],
-
            moved: vec![],
-
            copied: vec![],
-
            modified: vec![ModifiedFile {
-
                path: Path::with_root(&[
-
                    unsound::label::new("src"),
-
                    unsound::label::new("banana.rs"),
-
                ]),
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
                eof: None,
-
            }],
-
        };
-

-
        assert_eq!(diff, expected_diff)
-
    }
-

-
    /* TODO(fintan): Tricky stuff
-
    #[test]
-
    fn test_disjoint_directories() {
-
        let mut directory = Directory::root();
-
        directory.insert_file(
-
            &unsound::path::new("foo/src/banana.rs"),
-
            File::new(b"use banana"),
-
        );
-

-
        let mut other_directory = Directory::root();
-
        other_directory.insert_file(
-
            &unsound::path::new("bar/src/pineapple.rs"),
-
            File::new(b"use pineapple"),
-
        );
-

-
        let diff = Diff::diff(directory, other_directory).expect("diff failed");
-

-
        let expected_diff = Diff {
-
            created: vec![CreateFile(Path::from_labels(
-
                unsound::label::new("bar"),
-
                &[
-
                    unsound::label::new("src"),
-
                    unsound::label::new("pineapple.rs"),
-
                ],
-
            ))],
-
            deleted: vec![DeleteFile(Path::from_labels(
-
                unsound::label::new("foo"),
-
                &[unsound::label::new("src"), unsound::label::new("banana.rs")],
-
            ))],
-
            moved: vec![],
-
            modified: vec![],
-
        };
-

-
        assert_eq!(diff, expected_diff)
-
    }
-
    */
-
}
modified radicle-surf/src/diff/git.rs
@@ -229,78 +229,3 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
        Ok(diff)
    }
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

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

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

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

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

-
    pub(crate) fn from_hash_map(files: HashMap<Path, NonEmpty<(Label, File)>>) -> Self {
+
    /// 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() {
@@ -524,199 +525,3 @@ impl Directory {
        directory
    }
}
-

-
#[cfg(test)]
-
pub mod tests {
-
    #[cfg(test)]
-
    mod list_directory {
-
        use crate::file_system::{unsound, Directory, File, SystemType};
-

-
        #[test]
-
        fn root_files() {
-
            let mut directory = Directory::root();
-
            directory.insert_file(
-
                unsound::path::new("foo.hs"),
-
                File::new(b"module BananaFoo ..."),
-
            );
-
            directory.insert_file(
-
                unsound::path::new("bar.hs"),
-
                File::new(b"module BananaBar ..."),
-
            );
-
            directory.insert_file(
-
                unsound::path::new("baz.hs"),
-
                File::new(b"module BananaBaz ..."),
-
            );
-

-
            assert_eq!(
-
                directory.list_directory(),
-
                vec![
-
                    SystemType::file(unsound::label::new("bar.hs")),
-
                    SystemType::file(unsound::label::new("baz.hs")),
-
                    SystemType::file(unsound::label::new("foo.hs")),
-
                ]
-
            );
-
        }
-
    }
-

-
    #[cfg(test)]
-
    mod find_file {
-
        use crate::file_system::{unsound, *};
-

-
        #[test]
-
        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());
-

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

-
        #[test]
-
        fn file_does_not_exist() {
-
            let file_path = unsound::path::new("bar.hs");
-

-
            let file = File::new(b"module Banana ...");
-

-
            let mut directory = Directory::root();
-
            directory.insert_file(unsound::path::new("foo.hs"), file);
-

-
            assert_eq!(directory.find_file(file_path), None)
-
        }
-
    }
-

-
    #[cfg(test)]
-
    mod directory_size {
-
        use crate::file_system::{unsound, Directory, File};
-
        use nonempty::NonEmpty;
-

-
        #[test]
-
        fn root_directory_files() {
-
            let mut root = Directory::root();
-
            root.insert_files(
-
                &[],
-
                NonEmpty::from((
-
                    (
-
                        unsound::label::new("main.rs"),
-
                        File::new(b"println!(\"Hello, world!\")"),
-
                    ),
-
                    vec![(
-
                        unsound::label::new("lib.rs"),
-
                        File::new(b"struct Hello(String)"),
-
                    )],
-
                )),
-
            );
-

-
            assert_eq!(root.size(), 45);
-
        }
-
    }
-

-
    #[cfg(test)]
-
    mod properties {
-
        use crate::file_system::{unsound, *};
-
        use nonempty::NonEmpty;
-
        use proptest::{collection, prelude::*};
-
        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));
-
        }
-
    }
-
}
modified radicle-surf/src/file_system/path.rs
@@ -440,31 +440,3 @@ impl TryFrom<path::PathBuf> for Path {
        Ok(path)
    }
}
-

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

-
        #[test]
-
        fn split_last_root_and_foo() {
-
            let path = unsound::path::new("foo");
-
            assert_eq!(path.split_last(), (vec![], unsound::label::new("foo")));
-
        }
-

-
        #[test]
-
        fn split_last_same_labels() {
-
            // An interesting case for when first == last, but doesn't imply a singleton
-
            // Path.
-
            let path = unsound::path::new("foo/bar/foo");
-
            assert_eq!(
-
                path.split_last(),
-
                (
-
                    vec![unsound::label::new("foo"), unsound::label::new("bar")],
-
                    unsound::label::new("foo")
-
                )
-
            );
-
        }
-
    }
-
}
modified radicle-surf/src/lib.rs
@@ -103,8 +103,9 @@ pub mod syntax;
#[cfg(feature = "syntax")]
pub use syntax::SYNTAX_SET;

+
pub mod tree;
+

// Private modules
mod nonempty;
-
mod tree;

pub use crate::vcs::git;
modified radicle-surf/src/tree.rs
@@ -15,6 +15,8 @@
// 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;
@@ -152,7 +154,7 @@ impl<K, A> SubTree<K, A> {
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct Tree<K, A>(pub(crate) NonEmpty<SubTree<K, A>>);
+
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 {
@@ -161,17 +163,17 @@ impl<K, A> From<Tree<K, A>> for Forest<K, A> {
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-
pub struct Forest<K, A>(pub(crate) Option<Tree<K, A>>);
+
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.
-
    fn branch(key: K, forest: Self) -> Self {
+
    pub fn branch(key: K, forest: Self) -> Self {
        Tree(NonEmpty::new(SubTree::branch(key, forest)))
    }

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

@@ -499,693 +501,3 @@ impl<K, A> Forest<K, A> {
        self.0.iter().flat_map(|trees| trees.iter_keys())
    }
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use pretty_assertions::assert_eq;
-

-
    #[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);
-
    }
-
}
modified radicle-surf/src/vcs.rs
@@ -157,6 +157,11 @@ impl<Repo, A, Error> Browser<Repo, A, Error> {
        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;
modified radicle-surf/src/vcs/git.rs
@@ -69,14 +69,14 @@ pub use radicle_git_ext::Oid;

/// Provides ways of selecting a particular reference/revision.
mod reference;
-
pub use reference::{Ref, Rev};
+
pub use reference::{ParseError, Ref, Rev};

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

pub mod error;

-
mod ext;
+
pub mod ext;

/// Provides the data for talking about branches.
pub mod branch;
@@ -1181,591 +1181,3 @@ impl<'a> Browser<'a> {
        ))
    }
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

-
    #[cfg(not(feature = "gh-actions"))]
-
    #[test]
-
    // 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();
-
    }
-

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

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

-
            browser.branch(Branch::local("banana"))?;
-

-
            assert_ne!(history, browser.history);
-

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn me_namespace() -> Result<(), Error> {
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let browser = Browser::new(&repo, Branch::local("master"))?;
-
            let history = browser.history.clone();
-

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

-
            let browser = browser
-
                .switch_namespace(&Namespace::try_from("me")?, Branch::local("feature/#1194"))?;
-

-
            assert_eq!(
-
                browser.which_namespace(),
-
                Ok(Some(Namespace::try_from("me")?))
-
            );
-
            assert_eq!(history, browser.history);
-

-
            let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
-
            let mut branches = browser.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 {
-
                name: Some("fein".to_string()),
-
            })?;
-
            branches.sort();
-

-
            assert_eq!(expected_branches, branches);
-

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn golden_namespace() -> Result<(), Error> {
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let browser = Browser::new(&repo, Branch::local("master"))?;
-
            let history = browser.history.clone();
-

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

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

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

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

-
            assert_eq!(expected_branches, branches);
-

-
            let expected_branches: Vec<Branch> = vec![
-
                Branch::remote("fakie/bigspin", "kickflip"),
-
                Branch::remote("heelflip", "kickflip"),
-
                Branch::remote("v0.1.0", "kickflip"),
-
            ];
-
            let mut branches = golden_browser.list_branches(RefScope::Remote {
-
                name: Some("kickflip".to_string()),
-
            })?;
-
            branches.sort();
-

-
            assert_eq!(expected_branches, branches);
-

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn silver_namespace() -> Result<(), Error> {
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let browser = Browser::new(&repo, Branch::local("master"))?;
-
            let history = browser.history.clone();
-

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

-
            let silver_browser = browser.switch_namespace(
-
                &Namespace::try_from("golden/silver")?,
-
                Branch::local("master"),
-
            )?;
-

-
            assert_eq!(
-
                silver_browser.which_namespace(),
-
                Ok(Some(Namespace::try_from("golden/silver")?))
-
            );
-
            assert_ne!(history, silver_browser.history);
-

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

-
            assert_eq!(expected_branches, branches);
-

-
            Ok(())
-
        }
-
    }
-

-
    #[cfg(test)]
-
    mod rev {
-
        use super::{Branch, Browser, Error, Oid, Repository, TagName};
-
        use std::str::FromStr;
-

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

-
            let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
            assert!(
-
                browser
-
                    .history
-
                    .find(|commit| if commit.id == commit1 {
-
                        Some(commit.clone())
-
                    } else {
-
                        None
-
                    })
-
                    .is_some(),
-
                "commit_id={}, history =\n{:#?}",
-
                commit1,
-
                browser.history
-
            );
-

-
            let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
-
            assert!(
-
                browser
-
                    .history
-
                    .find(|commit| if commit.id == commit2 {
-
                        Some(commit.clone())
-
                    } else {
-
                        None
-
                    })
-
                    .is_some(),
-
                "commit_id={}, history =\n{:#?}",
-
                commit2,
-
                browser.history
-
            );
-

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn commit() -> Result<(), Error> {
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
            browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?;
-

-
            let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
            assert!(browser
-
                .history
-
                .find(|commit| if commit.id == commit1 {
-
                    Some(commit.clone())
-
                } else {
-
                    None
-
                })
-
                .is_some());
-

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn commit_parents() -> Result<(), Error> {
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
            browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?;
-
            let commit = browser.history.first();
-

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

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn commit_short() -> Result<(), Error> {
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
            browser.rev(browser.oid("3873745c8")?)?;
-

-
            let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
-
            assert!(browser
-
                .history
-
                .find(|commit| if commit.id == commit1 {
-
                    Some(commit.clone())
-
                } else {
-
                    None
-
                })
-
                .is_some());
-

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn tag() -> Result<(), Error> {
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let mut browser = Browser::new(&repo, Branch::local("master"))?;
-
            browser.rev(TagName::new("v0.2.0"))?;
-

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

-
            Ok(())
-
        }
-
    }
-

-
    #[cfg(test)]
-
    mod last_commit {
-
        use crate::{
-
            file_system::{unsound, Path},
-
            vcs::git::{Branch, Browser, Oid, Repository},
-
        };
-
        use std::str::FromStr;
-

-
        #[test]
-
        fn readme_missing_and_memory() {
-
            let repo = Repository::new("./data/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 = 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"),
-
                ]))
-
                .expect("Failed to get last commit")
-
                .map(|commit| commit.id);
-

-
            assert_eq!(memory_last_commit, None);
-

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

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

-
        #[test]
-
        fn folder_svelte() {
-
            let repo = Repository::new("./data/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 = Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97")
-
                .expect("Could not parse SHA");
-
            browser.commit(commit).unwrap();
-

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

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

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

-
        #[test]
-
        fn nest_directory() {
-
            let repo = Repository::new("./data/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 = Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97")
-
                .expect("Failed to parse SHA");
-
            browser.commit(commit).unwrap();
-

-
            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",
-
                ))
-
                .expect("Failed to get last commit")
-
                .map(|commit| commit.id);
-

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

-
        #[test]
-
        #[cfg(not(windows))]
-
        fn can_get_last_commit_for_special_filenames() {
-
            let repo = Repository::new("./data/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 = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02")
-
                .expect("Failed to parse SHA");
-
            browser.commit(commit).unwrap();
-

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

-
            let backslash_commit_id = browser
-
                .last_commit(unsound::path::new("~/special/faux\\path"))
-
                .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/👹👹👹"))
-
                .expect("Failed to get last commit")
-
                .map(|commit| commit.id);
-
            assert_eq!(ogre_commit_id, Some(expected_commit_id));
-
        }
-

-
        #[test]
-
        fn root() {
-
            let repo = Repository::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 root_last_commit_id = browser
-
                .last_commit(Path::root())
-
                .expect("Failed to get last commit")
-
                .map(|commit| commit.id);
-

-
            assert_eq!(root_last_commit_id, Some(browser.get().first().id));
-
        }
-
    }
-

-
    #[cfg(test)]
-
    mod diff {
-
        use crate::{diff::*, vcs::git::*};
-
        use std::str::FromStr;
-

-
        #[test]
-
        fn test_initial_diff() -> Result<(), Error> {
-
            use file_system::*;
-
            use pretty_assertions::assert_eq;
-

-
            let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
-
            let repo = Repository::new("./data/git-platinum")?;
-
            let commit = repo.0.find_commit(oid.into()).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 expected_diff = Diff {
-
                created: vec![CreateFile {
-
                    path: Path::with_root(&[unsound::label::new("README.md")]),
-
                    diff: FileDiff::Plain {
-
                        hunks: vec![Hunk {
-
                            header: Line(b"@@ -0,0 +1 @@\n".to_vec()),
-
                            lines: vec![
-
                                LineDiff::addition(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
-
                            ]
-
                        }].into()
-
                    },
-
                }],
-
                deleted: vec![],
-
                moved: vec![],
-
                copied: vec![],
-
                modified: vec![],
-
            };
-
            assert_eq!(expected_diff, diff);
-

-
            Ok(())
-
        }
-

-
        #[test]
-
        fn test_diff() -> Result<(), Error> {
-
            use file_system::*;
-
            use pretty_assertions::assert_eq;
-

-
            let repo = Repository::new("./data/git-platinum")?;
-
            let commit = repo
-
                .0
-
                .find_commit(git2::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 expected_diff = Diff {
-
                created: vec![],
-
                deleted: vec![],
-
                moved: vec![],
-
                copied: vec![],
-
                modified: vec![ModifiedFile {
-
                    path: Path::with_root(&[unsound::label::new("README.md")]),
-
                    diff: FileDiff::Plain {
-
                        hunks: vec![Hunk {
-
                            header: Line(b"@@ -1 +1,2 @@\n".to_vec()),
-
                            lines: vec![
-
                                LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
-
                                LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
-
                                LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
-
                            ]
-
                        }].into()
-
                    },
-
                    eof: None,
-
                }]
-
            };
-
            assert_eq!(expected_diff, diff);
-

-
            Ok(())
-
        }
-

-
        #[cfg(feature = "serialize")]
-
        #[test]
-
        fn test_diff_serde() -> Result<(), Error> {
-
            use file_system::*;
-

-
            let diff = Diff {
-
                created: vec![CreateFile{path: unsound::path::new("LICENSE"), diff: FileDiff::Plain { hunks: Hunks::default() }}],
-
                deleted: vec![],
-
                moved: vec![
-
                    MoveFile {
-
                        old_path: unsound::path::new("CONTRIBUTING"),
-
                        new_path: unsound::path::new("CONTRIBUTING.md")
-
                    }
-
                ],
-
                copied: vec![],
-
                modified: vec![ModifiedFile {
-
                    path: Path::with_root(&[unsound::label::new("README.md")]),
-
                    diff: FileDiff::Plain {
-
                        hunks: vec![Hunk {
-
                            header: Line(b"@@ -1 +1,2 @@\n".to_vec()),
-
                            lines: vec![
-
                                LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
-
                                LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
-
                                LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
-
                                LineDiff::context(b"\n".to_vec(), 3, 4),
-
                            ]
-
                        }].into()
-
                    },
-
                    eof: None,
-
                }]
-
            };
-

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

-
            Ok(())
-
        }
-
    }
-

-
    #[cfg(test)]
-
    mod threading {
-
        use crate::vcs::git::*;
-
        use pretty_assertions::assert_eq;
-
        use std::sync::{Mutex, MutexGuard};
-

-
        #[test]
-
        fn basic_test() -> Result<(), Error> {
-
            let shared_repo = Mutex::new(Repository::new("./data/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)?;
-
            branches.sort();
-

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

-
            Ok(())
-
        }
-
    }
-
}
modified radicle-surf/src/vcs/git/branch.rs
@@ -197,34 +197,3 @@ impl<'repo> TryFrom<git2::Reference<'repo>> for Branch {
        }
    }
}
-

-
#[cfg(feature = "serialize")]
-
#[cfg(test)]
-
pub mod tests {
-
    use proptest::prelude::*;
-
    use test_helpers::roundtrip;
-

-
    use super::{Branch, BranchName, BranchType};
-

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

-
    fn branch_strategy() -> impl Strategy<Value = Branch> {
-
        prop_oneof![
-
            any::<String>().prop_map(|name| Branch {
-
                name: BranchName::new(&name),
-
                locality: BranchType::Local
-
            }),
-
            (any::<String>(), any::<String>()).prop_map(|(name, remote_name)| Branch {
-
                name: BranchName::new(&name),
-
                locality: BranchType::Remote {
-
                    name: Some(remote_name),
-
                },
-
            })
-
        ]
-
    }
-
}
modified radicle-surf/src/vcs/git/commit.rs
@@ -20,7 +20,7 @@ use radicle_git_ext::Oid;
use std::{convert::TryFrom, str};

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

/// `Author` is the static information of a [`git2::Signature`].
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
@@ -166,40 +166,3 @@ impl<'repo> TryFrom<git2::Commit<'repo>> for Commit {
        })
    }
}
-

-
#[cfg(feature = "serialize")]
-
#[cfg(test)]
-
pub mod tests {
-
    use proptest::prelude::*;
-
    use radicle_git_ext::Oid;
-
    use test_helpers::roundtrip;
-

-
    use super::{Author, Commit};
-

-
    #[cfg(feature = "serialize")]
-
    proptest! {
-
        #[test]
-
        fn prop_test_commits(commit in commits_strategy()) {
-
            roundtrip::json(commit)
-
        }
-
    }
-

-
    fn commits_strategy() -> impl Strategy<Value = Commit> {
-
        ("[a-fA-F0-9]{40}", any::<String>(), any::<i64>()).prop_map(|(id, text, time)| Commit {
-
            id: Oid::from_str(&id).unwrap(),
-
            author: Author {
-
                name: text.clone(),
-
                email: text.clone(),
-
                time: git2::Time::new(time, 0),
-
            },
-
            committer: Author {
-
                name: text.clone(),
-
                email: text.clone(),
-
                time: git2::Time::new(time, 0),
-
            },
-
            message: text.clone(),
-
            summary: text,
-
            parents: vec![Oid::from_str(&id).unwrap(), Oid::from_str(&id).unwrap()],
-
        })
-
    }
-
}
modified radicle-surf/src/vcs/git/ext.rs
@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

+
#![allow(missing_docs)]
+

/// Try to strip any refs/namespaces, refs/heads, refs/remotes, and
/// refs/tags. If this fails we return the original string.
pub fn try_extract_refname(spec: &str) -> Result<String, String> {
@@ -64,48 +66,3 @@ pub fn is_remote(reference: &git2::Reference) -> bool {
        None => false,
    }
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

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

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

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

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

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

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

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

-
        assert_eq!(
-
            try_extract_refname("refs/namespaces/golden/refs/remotes/kickflip/heads/heelflip"),
-
            Ok("kickflip/heelflip".to_string())
-
        );
-
    }
-
}
modified radicle-surf/src/vcs/git/reference.rs
@@ -118,8 +118,10 @@ impl fmt::Display for Ref {
    }
}

+
/// Error when parsing a ref.
#[derive(Debug, PartialEq, Eq, Error)]
pub enum ParseError {
+
    /// The parsed ref is malformed.
    #[error("the ref provided '{0}' was malformed")]
    MalformedRef(String),
}
@@ -196,107 +198,3 @@ impl str::FromStr for Ref {
        parser::parse(reference).map_err(|_| ParseError::MalformedRef(reference.to_owned()))
    }
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use std::str::FromStr;
-

-
    #[test]
-
    fn parse_ref() -> Result<(), ParseError> {
-
        assert_eq!(
-
            Ref::from_str("refs/remotes/origin/master"),
-
            Ok(Ref::RemoteBranch {
-
                remote: "origin".to_string(),
-
                name: BranchName::new("master")
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/heads/master"),
-
            Ok(Ref::LocalBranch {
-
                name: BranchName::new("master"),
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/heads/i-am-hyphenated"),
-
            Ok(Ref::LocalBranch {
-
                name: BranchName::new("i-am-hyphenated"),
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/heads/prefix/i-am-hyphenated"),
-
            Ok(Ref::LocalBranch {
-
                name: BranchName::new("prefix/i-am-hyphenated"),
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/tags/v0.0.1"),
-
            Ok(Ref::Tag {
-
                name: TagName::new("v0.0.1")
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/moi/refs/remotes/origin/master"),
-
            Ok(Ref::Namespace {
-
                namespace: "moi".to_string(),
-
                reference: Box::new(Ref::RemoteBranch {
-
                    remote: "origin".to_string(),
-
                    name: BranchName::new("master")
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/moi/refs/namespaces/toi/refs/tags/v1.0.0"),
-
            Ok(Ref::Namespace {
-
                namespace: "moi".to_string(),
-
                reference: Box::new(Ref::Namespace {
-
                    namespace: "toi".to_string(),
-
                    reference: Box::new(Ref::Tag {
-
                        name: TagName::new("v1.0.0")
-
                    })
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/me/refs/heads/feature/#1194"),
-
            Ok(Ref::Namespace {
-
                namespace: "me".to_string(),
-
                reference: Box::new(Ref::LocalBranch {
-
                    name: BranchName::new("feature/#1194"),
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/me/refs/remotes/fein/heads/feature/#1194"),
-
            Ok(Ref::Namespace {
-
                namespace: "me".to_string(),
-
                reference: Box::new(Ref::RemoteBranch {
-
                    remote: "fein".to_string(),
-
                    name: BranchName::new("heads/feature/#1194"),
-
                })
-
            })
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/remotes/master"),
-
            Err(ParseError::MalformedRef("refs/remotes/master".to_owned())),
-
        );
-

-
        assert_eq!(
-
            Ref::from_str("refs/namespaces/refs/remotes/origin/master"),
-
            Err(ParseError::MalformedRef(
-
                "refs/namespaces/refs/remotes/origin/master".to_owned()
-
            )),
-
        );
-

-
        Ok(())
-
    }
-
}
modified radicle-surf/src/vcs/git/repo.rs
@@ -182,8 +182,8 @@ impl<'a> RepositoryRef<'a> {
        Ok(self.repo_ref.set_namespace(namespace)?)
    }

-
    /// Get a particular `Commit`.
-
    pub(super) fn get_commit(&self, oid: Oid) -> Result<git2::Commit<'a>, Error> {
+
    /// Get a particular `git2::Commit` of `oid`.
+
    pub fn get_git2_commit(&self, oid: Oid) -> Result<git2::Commit<'a>, Error> {
        let commit = self.repo_ref.find_commit(oid.into())?;
        Ok(commit)
    }
@@ -365,7 +365,7 @@ impl<'a> Vcs<Commit, Error> for RepositoryRef<'a> {
        match history_id {
            Rev::Ref(reference) => self.reference(reference, |_| None),
            Rev::Oid(oid) => {
-
                let commit = self.get_commit(oid)?;
+
                let commit = self.get_git2_commit(oid)?;
                self.commit_to_history(commit)
            },
        }
added radicle-surf/t/Cargo.toml
@@ -0,0 +1,35 @@
+
[package]
+
name = "radicle-surf-test"
+
version = "0.1.0"
+
edition = "2021"
+
license = "GPL-3.0-or-later"
+

+
publish = false
+

+
[lib]
+
test = true
+

+
[features]
+
serialize = []
+
test = []
+

+
[dev-dependencies]
+
nonempty = "0.5"
+
pretty_assertions = "1.3.0"
+
proptest = "0.9"
+
serde_json = "1"
+

+
[dev-dependencies.git2]
+
version = "0.15.0"
+
default-features = false
+
features = ["vendored-libgit2"]
+

+
[dev-dependencies.radicle-git-ext]
+
path = "../../radicle-git-ext"
+

+
[dev-dependencies.radicle-surf]
+
path = ".."
+
features = ["serialize"]
+

+
[dev-dependencies.test-helpers]
+
path = "../../test/test-helpers"
added radicle-surf/t/src/diff.rs
@@ -0,0 +1,298 @@
+
// Copyright © 2022 The Radicle Git Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
//! Unit tests for radicle_surf::diff
+

+
use pretty_assertions::assert_eq;
+
use radicle_surf::{
+
    diff::*,
+
    file_system::{unsound, *},
+
};
+

+
#[test]
+
fn test_create_file() {
+
    let directory = Directory::root();
+

+
    let mut new_directory = Directory::root();
+
    new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
+

+
    let diff = Diff::diff(directory, new_directory);
+

+
    let expected_diff = Diff {
+
        created: vec![CreateFile {
+
            path: Path::with_root(&[unsound::label::new("banana.rs")]),
+
            diff: FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
        }],
+
        deleted: vec![],
+
        copied: vec![],
+
        moved: vec![],
+
        modified: vec![],
+
    };
+

+
    assert_eq!(diff, expected_diff)
+
}
+

+
#[test]
+
fn test_delete_file() {
+
    let mut directory = Directory::root();
+
    directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
+

+
    let new_directory = Directory::root();
+

+
    let diff = Diff::diff(directory, new_directory);
+

+
    let expected_diff = Diff {
+
        created: vec![],
+
        deleted: vec![DeleteFile {
+
            path: Path::with_root(&[unsound::label::new("banana.rs")]),
+
            diff: FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
        }],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![],
+
    };
+

+
    assert_eq!(diff, expected_diff)
+
}
+

+
/* TODO(fintan): Move is not detected yet
+
#[test]
+
fn test_moved_file() {
+
    let mut directory = Directory::root();
+
    directory.insert_file(&unsound::path::new("mod.rs"), File::new(b"use banana"));
+

+
    let mut new_directory = Directory::root();
+
    new_directory.insert_file(&unsound::path::new("banana.rs"), File::new(b"use banana"));
+

+
    let diff = Diff::diff(directory, new_directory).expect("diff failed");
+

+
    assert_eq!(diff, Diff::new())
+
}
+
*/
+

+
#[test]
+
fn test_modify_file() {
+
    let mut directory = Directory::root();
+
    directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
+

+
    let mut new_directory = Directory::root();
+
    new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana;"));
+

+
    let diff = Diff::diff(directory, new_directory);
+

+
    let expected_diff = Diff {
+
        created: vec![],
+
        deleted: vec![],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![ModifiedFile {
+
            path: Path::with_root(&[unsound::label::new("banana.rs")]),
+
            diff: FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
            eof: None,
+
        }],
+
    };
+

+
    assert_eq!(diff, expected_diff)
+
}
+

+
#[test]
+
fn test_create_directory() {
+
    let directory = Directory::root();
+

+
    let mut new_directory = Directory::root();
+
    new_directory.insert_file(
+
        unsound::path::new("src/banana.rs"),
+
        File::new(b"use banana"),
+
    );
+

+
    let diff = Diff::diff(directory, new_directory);
+

+
    let expected_diff = Diff {
+
        created: vec![CreateFile {
+
            path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]),
+
            diff: FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
        }],
+
        deleted: vec![],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![],
+
    };
+

+
    assert_eq!(diff, expected_diff)
+
}
+

+
#[test]
+
fn test_delete_directory() {
+
    let mut directory = Directory::root();
+
    directory.insert_file(
+
        unsound::path::new("src/banana.rs"),
+
        File::new(b"use banana"),
+
    );
+

+
    let new_directory = Directory::root();
+

+
    let diff = Diff::diff(directory, new_directory);
+

+
    let expected_diff = Diff {
+
        created: vec![],
+
        deleted: vec![DeleteFile {
+
            path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]),
+
            diff: FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
        }],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![],
+
    };
+

+
    assert_eq!(diff, expected_diff)
+
}
+

+
#[test]
+
fn test_modify_file_directory() {
+
    let mut directory = Directory::root();
+
    directory.insert_file(
+
        unsound::path::new("src/banana.rs"),
+
        File::new(b"use banana"),
+
    );
+

+
    let mut new_directory = Directory::root();
+
    new_directory.insert_file(
+
        unsound::path::new("src/banana.rs"),
+
        File::new(b"use banana;"),
+
    );
+

+
    let diff = Diff::diff(directory, new_directory);
+

+
    let expected_diff = Diff {
+
        created: vec![],
+
        deleted: vec![],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![ModifiedFile {
+
            path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]),
+
            diff: FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
            eof: None,
+
        }],
+
    };
+

+
    assert_eq!(diff, expected_diff)
+
}
+

+
/* TODO(fintan): Tricky stuff
+
#[test]
+
fn test_disjoint_directories() {
+
    let mut directory = Directory::root();
+
    directory.insert_file(
+
        &unsound::path::new("foo/src/banana.rs"),
+
        File::new(b"use banana"),
+
    );
+

+
    let mut other_directory = Directory::root();
+
    other_directory.insert_file(
+
        &unsound::path::new("bar/src/pineapple.rs"),
+
        File::new(b"use pineapple"),
+
    );
+

+
    let diff = Diff::diff(directory, other_directory).expect("diff failed");
+

+
    let expected_diff = Diff {
+
        created: vec![CreateFile(Path::from_labels(
+
            unsound::label::new("bar"),
+
            &[
+
                unsound::label::new("src"),
+
                unsound::label::new("pineapple.rs"),
+
            ],
+
        ))],
+
        deleted: vec![DeleteFile(Path::from_labels(
+
            unsound::label::new("foo"),
+
            &[unsound::label::new("src"), unsound::label::new("banana.rs")],
+
        ))],
+
        moved: vec![],
+
        modified: vec![],
+
    };
+

+
    assert_eq!(diff, expected_diff)
+
}
+
*/
+

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

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

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

+
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
+
//#[test]
+
//     fn test_new_missing_eof_newline() {
+
//         let buf = r#"
+
// diff --git a/.env b/.env
+
// index f89e4c0..7c56eb7 100644
+
// --- a/.env
+
// +++ b/.env
+
// @@ -1 +1 @@
+
// -hello=123
+
// +hello=1234
+
// \ No newline at end of file
+
// "#;
+
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
//         let diff = Diff::try_from(diff).unwrap();
+
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::NewMissing));
+
//     }
added radicle-surf/t/src/file_system.rs
@@ -0,0 +1,222 @@
+
// Copyright © 2022 The Radicle Git Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
//! Unit tests for radicle_surf::file_system
+

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

+
    #[test]
+
    fn root_files() {
+
        let mut directory = Directory::root();
+
        directory.insert_file(
+
            unsound::path::new("foo.hs"),
+
            File::new(b"module BananaFoo ..."),
+
        );
+
        directory.insert_file(
+
            unsound::path::new("bar.hs"),
+
            File::new(b"module BananaBar ..."),
+
        );
+
        directory.insert_file(
+
            unsound::path::new("baz.hs"),
+
            File::new(b"module BananaBaz ..."),
+
        );
+

+
        assert_eq!(
+
            directory.list_directory(),
+
            vec![
+
                SystemType::file(unsound::label::new("bar.hs")),
+
                SystemType::file(unsound::label::new("baz.hs")),
+
                SystemType::file(unsound::label::new("foo.hs")),
+
            ]
+
        );
+
    }
+
}
+

+
#[cfg(test)]
+
mod find_file {
+
    use radicle_surf::file_system::{unsound, *};
+

+
    #[test]
+
    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());
+

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

+
    #[test]
+
    fn file_does_not_exist() {
+
        let file_path = unsound::path::new("bar.hs");
+

+
        let file = File::new(b"module Banana ...");
+

+
        let mut directory = Directory::root();
+
        directory.insert_file(unsound::path::new("foo.hs"), file);
+

+
        assert_eq!(directory.find_file(file_path), None)
+
    }
+
}
+

+
#[cfg(test)]
+
mod directory_size {
+
    use nonempty::NonEmpty;
+
    use radicle_surf::file_system::{unsound, Directory, File};
+

+
    #[test]
+
    fn root_directory_files() {
+
        let mut root = Directory::root();
+
        root.insert_files(
+
            &[],
+
            NonEmpty::from((
+
                (
+
                    unsound::label::new("main.rs"),
+
                    File::new(b"println!(\"Hello, world!\")"),
+
                ),
+
                vec![(
+
                    unsound::label::new("lib.rs"),
+
                    File::new(b"struct Hello(String)"),
+
                )],
+
            )),
+
        );
+

+
        assert_eq!(root.size(), 45);
+
    }
+
}
+

+
#[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;
+

+
    #[test]
+
    fn split_last_root_and_foo() {
+
        let path = unsound::path::new("foo");
+
        assert_eq!(path.split_last(), (vec![], unsound::label::new("foo")));
+
    }
+

+
    #[test]
+
    fn split_last_same_labels() {
+
        // An interesting case for when first == last, but doesn't imply a singleton
+
        // Path.
+
        let path = unsound::path::new("foo/bar/foo");
+
        assert_eq!(
+
            path.split_last(),
+
            (
+
                vec![unsound::label::new("foo"), unsound::label::new("bar")],
+
                unsound::label::new("foo")
+
            )
+
        );
+
    }
+
}
added radicle-surf/t/src/git.rs
@@ -0,0 +1,811 @@
+
// Copyright © 2022 The Radicle Git Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

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

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

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

+
#[cfg(not(feature = "gh-actions"))]
+
#[test]
+
// 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();
+
}
+

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

+
    #[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"))?;
+

+
        assert_ne!(history, browser.get());
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn me_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 browser = browser
+
            .switch_namespace(&Namespace::try_from("me")?, Branch::local("feature/#1194"))?;
+

+
        assert_eq!(
+
            browser.which_namespace(),
+
            Ok(Some(Namespace::try_from("me")?))
+
        );
+
        assert_eq!(history, browser.get());
+

+
        let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
+
        let mut branches = browser.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 {
+
            name: Some("fein".to_string()),
+
        })?;
+
        branches.sort();
+

+
        assert_eq!(expected_branches, branches);
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn golden_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 golden_browser =
+
            browser.switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?;
+

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

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

+
        assert_eq!(expected_branches, branches);
+

+
        let expected_branches: Vec<Branch> = vec![
+
            Branch::remote("fakie/bigspin", "kickflip"),
+
            Branch::remote("heelflip", "kickflip"),
+
            Branch::remote("v0.1.0", "kickflip"),
+
        ];
+
        let mut branches = golden_browser.list_branches(RefScope::Remote {
+
            name: Some("kickflip".to_string()),
+
        })?;
+
        branches.sort();
+

+
        assert_eq!(expected_branches, branches);
+

+
        Ok(())
+
    }
+

+
    #[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 silver_browser = browser.switch_namespace(
+
            &Namespace::try_from("golden/silver")?,
+
            Branch::local("master"),
+
        )?;
+

+
        assert_eq!(
+
            silver_browser.which_namespace(),
+
            Ok(Some(Namespace::try_from("golden/silver")?))
+
        );
+
        assert_ne!(history, silver_browser.get());
+

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

+
        assert_eq!(expected_branches, branches);
+

+
        Ok(())
+
    }
+
}
+

+
#[cfg(test)]
+
mod rev {
+
    use super::*;
+
    use std::str::FromStr;
+

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

+
        let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
        assert!(
+
            browser
+
                .as_history()
+
                .find(|commit| if commit.id == commit1 {
+
                    Some(commit.clone())
+
                } else {
+
                    None
+
                })
+
                .is_some(),
+
            "commit_id={}, history =\n{:#?}",
+
            commit1,
+
            browser.as_history()
+
        );
+

+
        let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
+
        assert!(
+
            browser
+
                .as_history()
+
                .find(|commit| if commit.id == commit2 {
+
                    Some(commit.clone())
+
                } else {
+
                    None
+
                })
+
                .is_some(),
+
            "commit_id={}, history =\n{:#?}",
+
            commit2,
+
            browser.as_history()
+
        );
+

+
        Ok(())
+
    }
+

+
    #[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 commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
        assert!(browser
+
            .as_history()
+
            .find(|commit| if commit.id == commit1 {
+
                Some(commit.clone())
+
            } else {
+
                None
+
            })
+
            .is_some());
+

+
        Ok(())
+
    }
+

+
    #[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();
+

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

+
        Ok(())
+
    }
+

+
    #[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 commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+
        assert!(browser
+
            .as_history()
+
            .find(|commit| if commit.id == commit1 {
+
                Some(commit.clone())
+
            } else {
+
                None
+
            })
+
            .is_some());
+

+
        Ok(())
+
    }
+

+
    #[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 commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
+
        assert_eq!(browser.as_history().first().id, commit1);
+

+
        Ok(())
+
    }
+
}
+

+
#[cfg(test)]
+
mod last_commit {
+
    use super::*;
+
    use std::str::FromStr;
+

+
    #[test]
+
    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 =
+
            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"),
+
            ]))
+
            .expect("Failed to get last commit")
+
            .map(|commit| commit.id);
+

+
        assert_eq!(memory_last_commit, None);
+

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

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

+
    #[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 =
+
            Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
+
        browser.commit(commit).unwrap();
+

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

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

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

+
    #[test]
+
    fn nest_directory() {
+
        let repo = Repository::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 =
+
            Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
+
        browser.commit(commit).unwrap();
+

+
        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",
+
            ))
+
            .expect("Failed to get last commit")
+
            .map(|commit| commit.id);
+

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

+
    #[test]
+
    #[cfg(not(windows))]
+
    fn can_get_last_commit_for_special_filenames() {
+
        let repo = Repository::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 =
+
            Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA");
+
        browser.commit(commit).unwrap();
+

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

+
        let backslash_commit_id = browser
+
            .last_commit(unsound::path::new("~/special/faux\\path"))
+
            .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/👹👹👹"))
+
            .expect("Failed to get last commit")
+
            .map(|commit| commit.id);
+
        assert_eq!(ogre_commit_id, Some(expected_commit_id));
+
    }
+

+
    #[test]
+
    fn root() {
+
        let repo = Repository::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())
+
            .expect("Failed to get last commit")
+
            .map(|commit| commit.id);
+

+
        assert_eq!(root_last_commit_id, Some(browser.get().first().id));
+
    }
+
}
+

+
#[cfg(test)]
+
mod diff {
+
    use super::*;
+
    use pretty_assertions::assert_eq;
+
    use std::str::FromStr;
+

+
    #[test]
+
    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();
+

+
        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 expected_diff = Diff {
+
            created: vec![CreateFile {
+
                path: Path::with_root(&[unsound::label::new("README.md")]),
+
                diff: FileDiff::Plain {
+
                    hunks: vec![Hunk {
+
                        header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()),
+
                        lines: vec![LineDiff::addition(
+
                            b"This repository is a data source for the Upstream front-end tests.\n"
+
                                .to_vec(),
+
                            1,
+
                        )],
+
                    }]
+
                    .into(),
+
                },
+
            }],
+
            deleted: vec![],
+
            moved: vec![],
+
            copied: vec![],
+
            modified: vec![],
+
        };
+
        assert_eq!(expected_diff, diff);
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn test_diff() -> Result<(), Error> {
+
        let repo = Repository::new(GIT_PLATINUM)?;
+
        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 expected_diff = Diff {
+
                created: vec![],
+
                deleted: vec![],
+
                moved: vec![],
+
                copied: vec![],
+
                modified: vec![ModifiedFile {
+
                    path: Path::with_root(&[unsound::label::new("README.md")]),
+
                    diff: FileDiff::Plain {
+
                        hunks: vec![Hunk {
+
                            header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
+
                            lines: vec![
+
                                LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
+
                                LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
+
                                LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
+
                            ]
+
                        }].into()
+
                    },
+
                    eof: None,
+
                }]
+
            };
+
        assert_eq!(expected_diff, diff);
+

+
        Ok(())
+
    }
+

+
    #[cfg(feature = "serialize")]
+
    #[test]
+
    fn test_diff_serde() -> Result<(), Error> {
+
        let diff = Diff {
+
                created: vec![CreateFile{path: unsound::path::new("LICENSE"), diff: FileDiff::Plain { hunks: Hunks::default() }}],
+
                deleted: vec![],
+
                moved: vec![
+
                    MoveFile {
+
                        old_path: unsound::path::new("CONTRIBUTING"),
+
                        new_path: unsound::path::new("CONTRIBUTING.md")
+
                    }
+
                ],
+
                copied: vec![],
+
                modified: vec![ModifiedFile {
+
                    path: Path::with_root(&[unsound::label::new("README.md")]),
+
                    diff: FileDiff::Plain {
+
                        hunks: vec![Hunk {
+
                            header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
+
                            lines: vec![
+
                                LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
+
                                LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
+
                                LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
+
                                LineDiff::context(b"\n".to_vec(), 3, 4),
+
                            ]
+
                        }].into()
+
                    },
+
                    eof: None,
+
                }]
+
            };
+

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

+
        Ok(())
+
    }
+
}
+

+
#[cfg(test)]
+
mod threading {
+
    use super::*;
+
    use std::sync::{Mutex, MutexGuard};
+

+
    #[test]
+
    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)?;
+
        branches.sort();
+

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

+
        Ok(())
+
    }
+
}
+

+
#[cfg(feature = "serialize")]
+
#[cfg(test)]
+
mod commit {
+
    use super::{Author, Commit};
+
    use proptest::prelude::*;
+
    use radicle_git_ext::Oid;
+
    use std::str::FromStr;
+
    use test_helpers::roundtrip;
+

+
    #[cfg(feature = "serialize")]
+
    proptest! {
+
        #[test]
+
        fn prop_test_commits(commit in commits_strategy()) {
+
            roundtrip::json(commit)
+
        }
+
    }
+

+
    fn commits_strategy() -> impl Strategy<Value = Commit> {
+
        ("[a-fA-F0-9]{40}", any::<String>(), any::<i64>()).prop_map(|(id, text, time)| Commit {
+
            id: Oid::from_str(&id).unwrap(),
+
            author: Author {
+
                name: text.clone(),
+
                email: text.clone(),
+
                time: git2::Time::new(time, 0),
+
            },
+
            committer: Author {
+
                name: text.clone(),
+
                email: text.clone(),
+
                time: git2::Time::new(time, 0),
+
            },
+
            message: text.clone(),
+
            summary: text,
+
            parents: vec![Oid::from_str(&id).unwrap(), Oid::from_str(&id).unwrap()],
+
        })
+
    }
+
}
+

+
#[cfg(feature = "serialize")]
+
#[cfg(test)]
+
mod branch {
+
    use super::*;
+
    use proptest::prelude::*;
+
    use test_helpers::roundtrip;
+

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

+
    fn branch_strategy() -> impl Strategy<Value = Branch> {
+
        prop_oneof![
+
            any::<String>().prop_map(|name| Branch {
+
                name: BranchName::new(&name),
+
                locality: BranchType::Local
+
            }),
+
            (any::<String>(), any::<String>()).prop_map(|(name, remote_name)| Branch {
+
                name: BranchName::new(&name),
+
                locality: BranchType::Remote {
+
                    name: Some(remote_name),
+
                },
+
            })
+
        ]
+
    }
+
}
+

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

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

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

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

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

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

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

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

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

+
#[cfg(test)]
+
mod reference {
+
    use super::*;
+
    use radicle_surf::vcs::git::{ParseError, Ref};
+
    use std::str::FromStr;
+

+
    #[test]
+
    fn parse_ref() -> Result<(), ParseError> {
+
        assert_eq!(
+
            Ref::from_str("refs/remotes/origin/master"),
+
            Ok(Ref::RemoteBranch {
+
                remote: "origin".to_string(),
+
                name: BranchName::new("master")
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/heads/master"),
+
            Ok(Ref::LocalBranch {
+
                name: BranchName::new("master"),
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/heads/i-am-hyphenated"),
+
            Ok(Ref::LocalBranch {
+
                name: BranchName::new("i-am-hyphenated"),
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/heads/prefix/i-am-hyphenated"),
+
            Ok(Ref::LocalBranch {
+
                name: BranchName::new("prefix/i-am-hyphenated"),
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/tags/v0.0.1"),
+
            Ok(Ref::Tag {
+
                name: TagName::new("v0.0.1")
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/namespaces/moi/refs/remotes/origin/master"),
+
            Ok(Ref::Namespace {
+
                namespace: "moi".to_string(),
+
                reference: Box::new(Ref::RemoteBranch {
+
                    remote: "origin".to_string(),
+
                    name: BranchName::new("master")
+
                })
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/namespaces/moi/refs/namespaces/toi/refs/tags/v1.0.0"),
+
            Ok(Ref::Namespace {
+
                namespace: "moi".to_string(),
+
                reference: Box::new(Ref::Namespace {
+
                    namespace: "toi".to_string(),
+
                    reference: Box::new(Ref::Tag {
+
                        name: TagName::new("v1.0.0")
+
                    })
+
                })
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/namespaces/me/refs/heads/feature/#1194"),
+
            Ok(Ref::Namespace {
+
                namespace: "me".to_string(),
+
                reference: Box::new(Ref::LocalBranch {
+
                    name: BranchName::new("feature/#1194"),
+
                })
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/namespaces/me/refs/remotes/fein/heads/feature/#1194"),
+
            Ok(Ref::Namespace {
+
                namespace: "me".to_string(),
+
                reference: Box::new(Ref::RemoteBranch {
+
                    remote: "fein".to_string(),
+
                    name: BranchName::new("heads/feature/#1194"),
+
                })
+
            })
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/remotes/master"),
+
            Err(ParseError::MalformedRef("refs/remotes/master".to_owned())),
+
        );
+

+
        assert_eq!(
+
            Ref::from_str("refs/namespaces/refs/remotes/origin/master"),
+
            Err(ParseError::MalformedRef(
+
                "refs/namespaces/refs/remotes/origin/master".to_owned()
+
            )),
+
        );
+

+
        Ok(())
+
    }
+
}
added radicle-surf/t/src/lib.rs
@@ -0,0 +1,14 @@
+
// Copyright © 2022 The Radicle Git Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

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

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

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

+
#[cfg(test)]
+
mod tree;
added radicle-surf/t/src/tree.rs
@@ -0,0 +1,692 @@
+
// 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);
+
}
modified test/Cargo.toml
@@ -29,3 +29,7 @@ features = ["test"]
[dev-dependencies.link-git-test]
path = "../link-git/t"
features = ["test"]
+

+
[dev-dependencies.radicle-surf-test]
+
path = "../radicle-surf/t"
+
features = ["test"]