mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-31 00:40:51 +08:00
refactor design: part 2
- Add a code browsing test - Update design doc, mainly Code Browsing and Diff. - Simplify `Directory`. - Remove `Tree` and `Forest`, and `SystemType`. - Use `Rev` in `diff()`. Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
@@ -1,47 +1,153 @@
|
||||
# An updated design for radicle-surf
|
||||
|
||||
Now we have ported the `radicle-surf` crate from its own github repo to be part of the `radicle-git` repo. We are taking this opportunity to refactor its design as well. Intuitively, `radicle-surf` provides an API so that one can use it to create a github-like UI for a git repo:
|
||||
## Introduction
|
||||
|
||||
- Given a commit (or other types of ref), list the content: i.e. files and directories.
|
||||
- Generate a diff between two commits.
|
||||
- List the history of the commits.
|
||||
- List refs: Branches, Tags.
|
||||
Now we have ported the `radicle-surf` crate from its own github repo to be
|
||||
part of the `radicle-git` repo. We are taking this opportunity to refactor
|
||||
its design as well. Intuitively, `radicle-surf` provides an API so that one
|
||||
can use it to create a GitHub-like UI for a git repo:
|
||||
|
||||
The main goals of the changes are:
|
||||
1. Code browsing: given a specific commit/ref, browse files and directories.
|
||||
2. Diff between two revisions that resolve into two commits.
|
||||
3. Retrieve the history of the commits.
|
||||
4. Retrieve a specific object: all its metadata.
|
||||
5. Retrieve the refs: Branches, Tags, Remotes, Notes and user-defined
|
||||
"categories", where a category is: refs/<category>/<...>.
|
||||
|
||||
- make API simpler whenever possible.
|
||||
- address open issues in the original `radicle-surf` repo as much as possible.
|
||||
- not to shy away from being `git` specific. (i.e. not to consider supporting other VCS systems)
|
||||
## Motivation
|
||||
|
||||
## Make API simpler
|
||||
The `radicle-surf` crate aims to provide a safe and easy-to-use API that
|
||||
supports the features listed in [Introduction]. Based on the existing API,
|
||||
the main goals of the refactoring are:
|
||||
|
||||
The current API has quite a bit accidental complexity that is not inherent with the requirements, especially when we can be git specific and don't care about other VCS systems.
|
||||
- API review: make API simpler whenever possible.
|
||||
- Address open issues in the original `radicle-surf` repo as much as possible.
|
||||
- Not to shy away from being `git` specific. (i.e. not to consider supporting
|
||||
other VCS systems)
|
||||
- Hide away `git2` from the API. The use of `git2` should be an implementation
|
||||
detail.
|
||||
|
||||
## API review
|
||||
|
||||
The current API has quite a bit accidental complexity that is not inherent with
|
||||
the requirements, especially when we can be git specific and don't care about
|
||||
other VCS systems.
|
||||
|
||||
### Remove the `Browser`
|
||||
|
||||
The type `Browser` is awkward as of today:
|
||||
|
||||
- it is not a source of truth of any information. For example, `list_branches` method is just a wrapper of `Repository::list_branches`.
|
||||
- it is not a source of truth of any information. For example, `list_branches`
|
||||
method is just a wrapper of `Repository::list_branches`.
|
||||
- it takes in `History`, but really works at the `Snapshot` level.
|
||||
- it is mutable but its state does not help much.
|
||||
|
||||
Can we just remove `Browser` and implement its functionalities using other types?
|
||||
Can we just remove `Browser` and implement its functionalities using other
|
||||
types?
|
||||
|
||||
- For iteratoring the history, use `History`.
|
||||
- For generating `Directory`, use `Repository` directly given a `Rev`.
|
||||
- For accessing `Branch`, `Tag` or `Commit`, use `Repository`.
|
||||
|
||||
## Remove the `Snapshot`
|
||||
## Remove the `Snapshot` type
|
||||
|
||||
A `Snapshot` should be really just a tree (or `Directory`) of a `Commit` in git. Currently it is a function that returns a `Directory`. Because it is OK to be git specific, we don't need to have this generic function to create a snapshot across different VCS systems.
|
||||
A `Snapshot` should be really just a tree (or `Directory`) of a `Commit` in
|
||||
git. Currently it is a function that returns a `Directory`. Because it is OK
|
||||
to be git specific, we don't need to have this generic function to create a
|
||||
snapshot across different VCS systems.
|
||||
|
||||
The only `snapshot` function defined currently:
|
||||
The snapshot function can be easily implement as a method of `RepositoryRef`.
|
||||
|
||||
## Simplify `Directory` and remove the `Tree` and `Forest` types
|
||||
|
||||
The `Directory` type represents the file system view of a snapshot. Its field
|
||||
`sub_directories` is defined a `Forest` based on `Tree`. The types are
|
||||
over-engineered from such a simple concept. We could refactor `Directory` to
|
||||
use `DirectoryContents` for its items and not to use `Tree` or `Forest` at all.
|
||||
|
||||
We also found the `list_directory()` method duplicates with `iter()` method.
|
||||
Hence `list_directory()` is removed, together with `SystemType` type.
|
||||
|
||||
## The new API
|
||||
|
||||
With the changes proposed in the previous section, we describe what the new API
|
||||
would look like and how they meet the requirements.
|
||||
|
||||
### Common principles
|
||||
|
||||
#### How to identify things that resolve into a commit
|
||||
|
||||
In our API, it will be good to have a single way to identify all refs and
|
||||
objects that resolve into commits. In other words, we try to avoid using
|
||||
different ways at different places. Currently there are multiple types in the
|
||||
API for this purpose:
|
||||
|
||||
- Commit
|
||||
- Oid
|
||||
- Rev
|
||||
|
||||
Because `Rev` is the most high level among those and supports refs already,
|
||||
I think we should use `Rev` in our API as much as possible.
|
||||
|
||||
#### How to identify History
|
||||
|
||||
TBD
|
||||
|
||||
### Code browsing
|
||||
|
||||
The user should be able to browse the files and directories for any given
|
||||
commits or references. The core API is:
|
||||
|
||||
- Create a root Directory:
|
||||
```Rust
|
||||
let snapshot = Box::new(|repository: &RepositoryRef<'a>, history: &History| {
|
||||
let tree = Self::get_tree(repository.repo_ref, history.0.first())?;
|
||||
Ok(directory::Directory::from_hash_map(tree))
|
||||
});
|
||||
imp RepositoryRef {
|
||||
pub fn snapshot(&self, rev: &Rev) -> Result<Directory, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
The above function can be easily implement as a method of `Repository`.
|
||||
- Browse a Directory:
|
||||
```Rust
|
||||
impl Directory {
|
||||
pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents>;
|
||||
}
|
||||
```
|
||||
where `DirectoryContents` supports both files and sub-directories:
|
||||
```Rust
|
||||
pub enum DirectoryContents {
|
||||
/// The `File` variant contains the file's name and the [`File`] itself.
|
||||
File {
|
||||
/// The name of the file.
|
||||
name: Label,
|
||||
/// The file data.
|
||||
file: File,
|
||||
},
|
||||
/// The `Directory` variant contains a sub-directory to the current one.
|
||||
Directory(Directory),
|
||||
}
|
||||
```
|
||||
|
||||
### Diffs
|
||||
|
||||
The user would be able to create a diff between any two revisions that resolve
|
||||
into two commits.
|
||||
|
||||
The main change is to use `Rev` instead of `Oid` to identify `from` and `to`.
|
||||
The core API is:
|
||||
|
||||
```Rust
|
||||
imp RepositoryRef {
|
||||
pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
To help convert from `Oid` to `Rev`, we provide a helper method:
|
||||
```Rust
|
||||
imp RepositoryRef {
|
||||
/// Returns the Oid of `rev`.
|
||||
pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
TBD
|
||||
|
||||
@@ -32,7 +32,10 @@ fn main() {
|
||||
let base_oid = Oid::from_str(&options.base_revision).unwrap();
|
||||
let now = Instant::now();
|
||||
let elapsed_nanos = now.elapsed().as_nanos();
|
||||
let diff = repo.as_ref().diff(base_oid, head_oid).unwrap();
|
||||
let diff = repo
|
||||
.as_ref()
|
||||
.diff(&base_oid.into(), &head_oid.into())
|
||||
.unwrap();
|
||||
print_diff_summary(&diff, elapsed_nanos);
|
||||
}
|
||||
|
||||
|
||||
@@ -144,12 +144,14 @@ pub struct Commits {
|
||||
///
|
||||
/// Will return [`Error`] if the project doesn't exist or the surf interaction
|
||||
/// fails.
|
||||
pub fn commit(repo: &RepositoryRef, sha1: Oid) -> Result<Commit, Error> {
|
||||
pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
|
||||
let sha1 = repo.rev_oid(rev)?;
|
||||
let commit = repo.get_commit(sha1)?;
|
||||
let diff = if let Some(parent) = commit.parents.first() {
|
||||
repo.diff(*parent, sha1)?
|
||||
let parent_rev = (*parent).into();
|
||||
repo.diff(&parent_rev, rev)?
|
||||
} else {
|
||||
repo.initial_diff(sha1)?
|
||||
repo.initial_diff(rev)?
|
||||
};
|
||||
|
||||
let mut deletions = 0;
|
||||
|
||||
+13
-10
@@ -295,7 +295,10 @@ impl Diff {
|
||||
#[allow(clippy::self_named_constructors)]
|
||||
pub fn diff(left: Directory, right: Directory) -> Self {
|
||||
let mut diff = Diff::new();
|
||||
let path = Rc::new(RefCell::new(Path::from_labels(right.current(), &[])));
|
||||
let path = Rc::new(RefCell::new(Path::from_labels(
|
||||
right.current().clone(),
|
||||
&[],
|
||||
)));
|
||||
Diff::collect_diff(&left, &right, &path, &mut diff);
|
||||
|
||||
// TODO: Some of the deleted files may actually be moved (renamed) to one of the
|
||||
@@ -315,15 +318,15 @@ impl Diff {
|
||||
parent_path: &Rc<RefCell<Path>>,
|
||||
diff: &mut Diff,
|
||||
) {
|
||||
let mut old_iter = old.iter();
|
||||
let mut new_iter = new.iter();
|
||||
let mut old_iter = old.contents();
|
||||
let mut new_iter = new.contents();
|
||||
let mut old_entry_opt = old_iter.next();
|
||||
let mut new_entry_opt = new_iter.next();
|
||||
|
||||
while old_entry_opt.is_some() || new_entry_opt.is_some() {
|
||||
match (&old_entry_opt, &new_entry_opt) {
|
||||
(Some(ref old_entry), Some(ref new_entry)) => {
|
||||
match new_entry.label().cmp(&old_entry.label()) {
|
||||
match new_entry.label().cmp(old_entry.label()) {
|
||||
Ordering::Greater => {
|
||||
diff.add_deleted_files(old_entry, parent_path);
|
||||
old_entry_opt = old_iter.next();
|
||||
@@ -414,11 +417,11 @@ impl Diff {
|
||||
},
|
||||
}
|
||||
},
|
||||
(Some(ref old_entry), None) => {
|
||||
(Some(old_entry), None) => {
|
||||
diff.add_deleted_files(old_entry, parent_path);
|
||||
old_entry_opt = old_iter.next();
|
||||
},
|
||||
(None, Some(ref new_entry)) => {
|
||||
(None, Some(new_entry)) => {
|
||||
diff.add_created_files(new_entry, parent_path);
|
||||
new_entry_opt = new_iter.next();
|
||||
},
|
||||
@@ -465,15 +468,15 @@ impl Diff {
|
||||
) where
|
||||
F: Fn(Path) -> T + Copy,
|
||||
{
|
||||
parent_path.borrow_mut().push(dir.current());
|
||||
for entry in dir.iter() {
|
||||
parent_path.borrow_mut().push(dir.current().clone());
|
||||
for entry in dir.contents() {
|
||||
match entry {
|
||||
DirectoryContents::Directory(subdir) => {
|
||||
Diff::collect_files_inner(&subdir, parent_path, mapper, files);
|
||||
Diff::collect_files_inner(subdir, parent_path, mapper, files);
|
||||
},
|
||||
DirectoryContents::File { name, .. } => {
|
||||
let mut path = parent_path.borrow().clone();
|
||||
path.push(name);
|
||||
path.push(name.clone());
|
||||
files.push(mapper(path));
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,42 +21,15 @@
|
||||
//! See [`Directory`] for more information.
|
||||
//!
|
||||
//! As well as this, this module contains [`DirectoryContents`] which is the
|
||||
//! output of iterating over a [`Directory`], and also [`SystemType`] which is
|
||||
//! an identifier of what type of [`DirectoryContents`] one is viewing when
|
||||
//! [listing](#method.list_directory) a directory.
|
||||
//! output of iterating over a [`Directory`].
|
||||
|
||||
use crate::{file_system::path::*, tree::*};
|
||||
use crate::file_system::path::*;
|
||||
use nonempty::NonEmpty;
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, HashMap},
|
||||
collections::{hash_map::DefaultHasher, BTreeMap},
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
/// `SystemType` is an enumeration over what can be found in a [`Directory`] so
|
||||
/// we can report back to the caller a [`Label`] and its type.
|
||||
///
|
||||
/// See [`SystemType::file`](#method.file) and
|
||||
/// [`SystemType::directory`](#method.directory).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SystemType {
|
||||
/// The `File` type in a directory system.
|
||||
File,
|
||||
/// The `Directory` type in a directory system.
|
||||
Directory,
|
||||
}
|
||||
|
||||
impl SystemType {
|
||||
/// A file name and [`SystemType::File`].
|
||||
pub fn file(label: Label) -> (Label, Self) {
|
||||
(label, SystemType::File)
|
||||
}
|
||||
|
||||
/// A directory name and [`SystemType::Directory`].
|
||||
pub fn directory(label: Label) -> (Label, Self) {
|
||||
(label, SystemType::Directory)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `File` consists of its file contents (a [`Vec`] of bytes).
|
||||
///
|
||||
/// The `Debug` instance of `File` will show the first few bytes of the file and
|
||||
@@ -126,29 +99,16 @@ impl File {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Location {
|
||||
Root,
|
||||
SubDirectory(Label),
|
||||
}
|
||||
|
||||
/// A `Directory` can be thought of as a non-empty set of entries of
|
||||
/// sub-directories and files. The reason for the non-empty property is that a
|
||||
/// VCS directory would have at least one artifact as a sub-directory which
|
||||
/// tracks the VCS work, e.g. git using the `.git` folder.
|
||||
///
|
||||
/// On top of that, some VCSes, such as git, will not track an empty directory,
|
||||
/// and so when creating a new directory to track it will have to contain at
|
||||
/// least one file.
|
||||
/// A `Directory` is a set of entries of sub-directories and files, ordered
|
||||
/// by their unique names in the alphabetical order.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Directory {
|
||||
current: Location,
|
||||
sub_directories: Forest<Label, File>,
|
||||
name: Label,
|
||||
contents: BTreeMap<Label, DirectoryContents>,
|
||||
}
|
||||
|
||||
/// `DirectoryContents` is an enumeration of what a [`Directory`] can contain
|
||||
/// and is used for when we are [`iter`](struct.Directory.html#method.iter)ating
|
||||
/// through a [`Directory`].
|
||||
/// and is used for when we are iterating through a [`Directory`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DirectoryContents {
|
||||
/// The `File` variant contains the file's name and the [`File`] itself.
|
||||
@@ -165,168 +125,53 @@ pub enum DirectoryContents {
|
||||
impl DirectoryContents {
|
||||
/// Get a label for the `DirectoryContents`, either the name of the [`File`]
|
||||
/// or the name of the [`Directory`].
|
||||
pub fn label(&self) -> Label {
|
||||
pub fn label(&self) -> &Label {
|
||||
match self {
|
||||
DirectoryContents::File { name, .. } => name.clone(),
|
||||
DirectoryContents::File { name, .. } => name,
|
||||
DirectoryContents::Directory(directory) => directory.current(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubTree<Label, File>> for DirectoryContents {
|
||||
fn from(sub_tree: SubTree<Label, File>) -> Self {
|
||||
match sub_tree {
|
||||
SubTree::Node { key, value } => DirectoryContents::File {
|
||||
name: key,
|
||||
file: value,
|
||||
},
|
||||
SubTree::Branch { key, forest } => DirectoryContents::Directory(Directory {
|
||||
current: Location::SubDirectory(key),
|
||||
sub_directories: (*forest).into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
/// Create a root directory.
|
||||
///
|
||||
/// This function is usually used for testing and demonstation purposes.
|
||||
pub fn root() -> Self {
|
||||
Directory {
|
||||
current: Location::Root,
|
||||
sub_directories: Forest::root(),
|
||||
name: Label::root(),
|
||||
contents: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a directory, similar to `root`, except with a given name.
|
||||
///
|
||||
/// This function is usually used for testing and demonstation purposes.
|
||||
pub fn new(label: Label) -> Self {
|
||||
pub fn new(name: Label) -> Self {
|
||||
Directory {
|
||||
current: Location::SubDirectory(label),
|
||||
sub_directories: Forest::root(),
|
||||
name,
|
||||
contents: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// List the current `Directory`'s files and sub-directories.
|
||||
///
|
||||
/// The listings are a pair of [`Label`] and [`SystemType`], where the
|
||||
/// [`Label`] represents the name of the file or directory.
|
||||
///
|
||||
/// ```
|
||||
/// use nonempty::NonEmpty;
|
||||
/// use radicle_surf::file_system::{Directory, File, SystemType};
|
||||
/// use radicle_surf::file_system::unsound;
|
||||
///
|
||||
/// let mut directory = Directory::root();
|
||||
///
|
||||
/// // Root files set up
|
||||
/// let root_files = NonEmpty::from((
|
||||
/// (unsound::label::new("foo.rs"), File::new(b"use crate::bar")),
|
||||
/// vec![(
|
||||
/// unsound::label::new("bar.rs"),
|
||||
/// File::new(b"fn hello_world()"),
|
||||
/// )],
|
||||
/// ));
|
||||
/// directory.insert_files(&[], root_files);
|
||||
///
|
||||
/// // Haskell files set up
|
||||
/// let haskell_files = NonEmpty::from((
|
||||
/// (
|
||||
/// unsound::label::new("foo.hs"),
|
||||
/// File::new(b"module Foo where"),
|
||||
/// ),
|
||||
/// vec![(
|
||||
/// unsound::label::new("bar.hs"),
|
||||
/// File::new(b"module Bar where"),
|
||||
/// )],
|
||||
/// ));
|
||||
///
|
||||
/// directory.insert_files(&[unsound::label::new("haskell")], haskell_files);
|
||||
///
|
||||
/// let mut directory_contents = directory.list_directory();
|
||||
/// directory_contents.sort();
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// directory_contents,
|
||||
/// vec![
|
||||
/// SystemType::file(unsound::label::new("bar.rs")),
|
||||
/// SystemType::file(unsound::label::new("foo.rs")),
|
||||
/// SystemType::directory(unsound::label::new("haskell")),
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn list_directory(&self) -> Vec<(Label, SystemType)> {
|
||||
let forest = &self.sub_directories;
|
||||
match &forest.0 {
|
||||
None => vec![],
|
||||
Some(trees) => trees
|
||||
.0
|
||||
.iter()
|
||||
.map(|tree| match tree {
|
||||
SubTree::Node { key: name, .. } => SystemType::file(name.clone()),
|
||||
SubTree::Branch { key: name, .. } => SystemType::directory(name.clone()),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
/// Get the name of the current `Directory`.
|
||||
pub fn name(&self) -> &Label {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Get the [`Label`] of the current directory.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use radicle_surf::file_system::{Directory, DirectoryContents, File, Label};
|
||||
/// use radicle_surf::file_system::unsound;
|
||||
///
|
||||
/// let mut root = Directory::root();
|
||||
///
|
||||
/// let main = File::new(b"println!(\"Hello, world!\")");
|
||||
/// root.insert_file(unsound::path::new("main.rs"), main.clone());
|
||||
///
|
||||
/// let lib = File::new(b"struct Hello(String)");
|
||||
/// root.insert_file(unsound::path::new("lib.rs"), lib.clone());
|
||||
///
|
||||
/// let test_mod = File::new(b"assert_eq!(1 + 1, 2);");
|
||||
/// root.insert_file(unsound::path::new("test/mod.rs"), test_mod.clone());
|
||||
///
|
||||
/// let mut root_iter = root.iter();
|
||||
///
|
||||
/// assert_eq!(root_iter.next(), Some(DirectoryContents::File {
|
||||
/// name: unsound::label::new("lib.rs"),
|
||||
/// file: lib
|
||||
/// }));
|
||||
///
|
||||
/// assert_eq!(root_iter.next(), Some(DirectoryContents::File {
|
||||
/// name: unsound::label::new("main.rs"),
|
||||
/// file: main
|
||||
/// }));
|
||||
///
|
||||
/// let mut test_dir = Directory::new(unsound::label::new("test"));
|
||||
/// test_dir.insert_file(unsound::path::new("mod.rs"), test_mod);
|
||||
///
|
||||
/// assert_eq!(root_iter.next(), Some(DirectoryContents::Directory(test_dir)));
|
||||
/// ```
|
||||
pub fn iter(&self) -> impl Iterator<Item = DirectoryContents> + '_ {
|
||||
let mut empty_iter = None;
|
||||
let mut trees_iter = None;
|
||||
match &self.sub_directories.0 {
|
||||
None => empty_iter = Some(std::iter::empty()),
|
||||
Some(trees) => {
|
||||
trees_iter = Some(
|
||||
trees
|
||||
.iter_subtrees()
|
||||
.cloned()
|
||||
.map(|sub_tree| sub_tree.into()),
|
||||
)
|
||||
},
|
||||
}
|
||||
/// Add the `content` under `name` to the current `Directory`.
|
||||
/// If `name` already exists in this directory, then the previous contents
|
||||
/// are replaced.
|
||||
pub fn insert(&mut self, name: Label, content: DirectoryContents) {
|
||||
self.contents.insert(name, content);
|
||||
}
|
||||
|
||||
empty_iter
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(trees_iter.into_iter().flatten())
|
||||
/// Returns an iterator for the contents of the current directory.
|
||||
///
|
||||
/// Note that the returned iterator only iterates the current level,
|
||||
/// without going recursively into sub-directories.
|
||||
pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents> {
|
||||
self.contents.values()
|
||||
}
|
||||
|
||||
/// Find a [`File`] in the directory given the [`Path`] to the [`File`].
|
||||
@@ -361,8 +206,23 @@ impl Directory {
|
||||
/// // We shouldn't be able to find a file that doesn't exist
|
||||
/// assert_eq!(directory.find_file(unsound::path::new("foo/bar/qux.rs")), None);
|
||||
/// ```
|
||||
pub fn find_file(&self, path: Path) -> Option<File> {
|
||||
self.sub_directories.find_node(path.0).cloned()
|
||||
pub fn find_file(&self, path: Path) -> Option<&File> {
|
||||
let mut contents = &self.contents;
|
||||
let path_depth = path.0.len();
|
||||
for (idx, label) in path.iter().enumerate() {
|
||||
match contents.get(label) {
|
||||
Some(DirectoryContents::Directory(d)) => contents = &d.contents,
|
||||
Some(DirectoryContents::File { name: _, file }) => {
|
||||
if idx + 1 == path_depth {
|
||||
return Some(file);
|
||||
} else {
|
||||
break; // Abort: finding a file before the last label.
|
||||
}
|
||||
},
|
||||
None => break, // Abort: a label not found.
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Find a `Directory` in the directory given the [`Path`] to the
|
||||
@@ -400,17 +260,22 @@ impl Directory {
|
||||
/// // 'baz.rs' is a file and not a directory
|
||||
/// assert!(directory.find_directory(unsound::path::new("foo/bar/baz.rs")).is_none());
|
||||
/// ```
|
||||
pub fn find_directory(&self, path: Path) -> Option<Self> {
|
||||
self.sub_directories
|
||||
.find_branch(path.0.clone())
|
||||
.cloned()
|
||||
.map(|tree| {
|
||||
let (_, current) = path.split_last();
|
||||
Directory {
|
||||
current: Location::SubDirectory(current),
|
||||
sub_directories: tree.into(),
|
||||
}
|
||||
})
|
||||
pub fn find_directory(&self, path: Path) -> Option<&Self> {
|
||||
let mut found = None;
|
||||
let mut contents = &self.contents;
|
||||
|
||||
for label in path.iter() {
|
||||
match contents.get(label) {
|
||||
Some(DirectoryContents::Directory(d)) => {
|
||||
found = Some(d);
|
||||
contents = &d.contents;
|
||||
},
|
||||
Some(DirectoryContents::File { .. }) => break, // Abort: should not be a file.
|
||||
None => break, // Abort: a label not found.
|
||||
}
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
|
||||
/// Get the [`Label`] of the current directory.
|
||||
@@ -433,11 +298,8 @@ impl Directory {
|
||||
/// ).expect("Missing test directory");
|
||||
/// assert_eq!(test.current(), unsound::label::new("test"));
|
||||
/// ```
|
||||
pub fn current(&self) -> Label {
|
||||
match &self.current {
|
||||
Location::Root => Label::root(),
|
||||
Location::SubDirectory(label) => label.clone(),
|
||||
}
|
||||
pub fn current(&self) -> &Label {
|
||||
&self.name
|
||||
}
|
||||
|
||||
// TODO(fintan): This is going to be a bit trickier so going to leave it out for
|
||||
@@ -464,9 +326,13 @@ impl Directory {
|
||||
/// assert_eq!(root.size(), 66);
|
||||
/// ```
|
||||
pub fn size(&self) -> usize {
|
||||
self.sub_directories
|
||||
.iter()
|
||||
.fold(0, |size, file| size + file.size())
|
||||
self.contents().fold(0, |size, item| {
|
||||
if let DirectoryContents::File { name: _, file } = item {
|
||||
size + file.size()
|
||||
} else {
|
||||
size
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert a file into a directory, given the full path to file (file name
|
||||
@@ -474,7 +340,39 @@ impl Directory {
|
||||
///
|
||||
/// This function is usually used for testing and demonstation purposes.
|
||||
pub fn insert_file(&mut self, path: Path, file: File) {
|
||||
self.sub_directories.insert(path.0, file)
|
||||
let name = path.0.last().clone();
|
||||
let f = DirectoryContents::File {
|
||||
name: name.clone(),
|
||||
file,
|
||||
};
|
||||
|
||||
let mut contents = &mut self.contents;
|
||||
let path_depth = path.0.len() - 1; // exclude the last label: file name
|
||||
|
||||
if path_depth == 0 {
|
||||
contents.insert(name, f);
|
||||
} else {
|
||||
for (idx, label) in path.iter().enumerate() {
|
||||
// if label does not exist, create a sub directory.
|
||||
if contents.get(label).is_none() {
|
||||
let new_dir = Directory::new(label.clone());
|
||||
contents.insert(label.clone(), DirectoryContents::Directory(new_dir));
|
||||
}
|
||||
|
||||
match contents.get_mut(label) {
|
||||
Some(DirectoryContents::Directory(d)) => {
|
||||
contents = &mut d.contents;
|
||||
if idx + 1 == path_depth {
|
||||
// We are in the last directory level, insert the file.
|
||||
contents.insert(name, f);
|
||||
return;
|
||||
}
|
||||
},
|
||||
Some(DirectoryContents::File { .. }) => return, // Abort: should not be a file.
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert files into a shared directory path.
|
||||
@@ -503,25 +401,4 @@ impl Directory {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Directory` from a HashMap `files`.
|
||||
pub fn from_hash_map(files: HashMap<Path, NonEmpty<(Label, File)>>) -> Self {
|
||||
let mut directory: Self = Directory::root();
|
||||
|
||||
for (path, files) in files.into_iter() {
|
||||
for (file_name, file) in files.into_iter() {
|
||||
let file_path = if path.is_root() {
|
||||
Path::new(file_name)
|
||||
} else {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(file_name);
|
||||
new_path
|
||||
};
|
||||
|
||||
directory.insert_file(file_path, file)
|
||||
}
|
||||
}
|
||||
|
||||
directory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ impl Deref for Label {
|
||||
}
|
||||
}
|
||||
|
||||
/// The label for the root directory.
|
||||
pub const ROOT_LABEL: &str = "~";
|
||||
|
||||
impl Label {
|
||||
/// The root label for the root directory, i.e. `"~"`.
|
||||
///
|
||||
@@ -64,7 +67,7 @@ impl Label {
|
||||
/// ```
|
||||
pub fn root() -> Self {
|
||||
Label {
|
||||
label: "~".into(),
|
||||
label: ROOT_LABEL.into(),
|
||||
hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +103,6 @@ pub mod syntax;
|
||||
#[cfg(feature = "syntax")]
|
||||
pub use syntax::SYNTAX_SET;
|
||||
|
||||
pub mod tree;
|
||||
|
||||
// Private modules
|
||||
mod nonempty;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ use serde::{
|
||||
|
||||
use crate::{
|
||||
commit,
|
||||
file_system,
|
||||
file_system::{self, DirectoryContents},
|
||||
git::RepositoryRef,
|
||||
object::{Error, Info, ObjectType},
|
||||
revision::Revision,
|
||||
@@ -113,33 +113,31 @@ where
|
||||
|
||||
let root_dir = repo.snapshot(&rev)?;
|
||||
let prefix_dir = if path.is_root() {
|
||||
root_dir
|
||||
&root_dir
|
||||
} else {
|
||||
root_dir
|
||||
.find_directory(path.clone())
|
||||
.ok_or_else(|| Error::PathNotFound(path.clone()))?
|
||||
};
|
||||
let mut prefix_contents = prefix_dir.list_directory();
|
||||
prefix_contents.sort();
|
||||
|
||||
let entries_results: Result<Vec<TreeEntry>, Error> = prefix_contents
|
||||
.iter()
|
||||
.map(|(label, system_type)| {
|
||||
let entries_results: Result<Vec<TreeEntry>, Error> = prefix_dir
|
||||
.contents()
|
||||
.map(|entry| {
|
||||
let entry_path = if path.is_root() {
|
||||
file_system::Path::new(label.clone())
|
||||
file_system::Path::new(entry.label().clone())
|
||||
} else {
|
||||
let mut p = path.clone();
|
||||
p.push(label.clone());
|
||||
p.push(entry.label().clone());
|
||||
p
|
||||
};
|
||||
let mut commit_path = file_system::Path::root();
|
||||
commit_path.append(entry_path.clone());
|
||||
|
||||
let info = Info {
|
||||
name: label.to_string(),
|
||||
object_type: match system_type {
|
||||
file_system::SystemType::Directory => ObjectType::Tree,
|
||||
file_system::SystemType::File => ObjectType::Blob,
|
||||
name: entry.label().to_string(),
|
||||
object_type: match entry {
|
||||
DirectoryContents::Directory(_) => ObjectType::Tree,
|
||||
DirectoryContents::File { .. } => ObjectType::Blob,
|
||||
},
|
||||
last_commit: None,
|
||||
};
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::nonempty::split_last;
|
||||
use nonempty::NonEmpty;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SubTree<K, A> {
|
||||
Node { key: K, value: A },
|
||||
Branch { key: K, forest: Box<Tree<K, A>> },
|
||||
}
|
||||
|
||||
impl<K, A> SubTree<K, A> {
|
||||
/// Create a new `Branch` from a key and sub-tree.
|
||||
///
|
||||
/// This function is a convenience for now having to
|
||||
/// remember to use `Box::new`.
|
||||
fn branch(key: K, tree: Tree<K, A>) -> Self {
|
||||
SubTree::Branch {
|
||||
key,
|
||||
forest: Box::new(tree),
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&self) -> &K {
|
||||
match self {
|
||||
SubTree::Node { key, .. } => key,
|
||||
SubTree::Branch { key, .. } => key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find(&self, keys: NonEmpty<K>) -> Option<&Self>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
let (head, tail) = keys.into();
|
||||
let tail = NonEmpty::from_vec(tail);
|
||||
match self {
|
||||
SubTree::Node { key, .. } => match tail {
|
||||
None if *key == head => Some(self),
|
||||
_ => None,
|
||||
},
|
||||
SubTree::Branch { key, ref forest } => match tail {
|
||||
None if *key == head => Some(self),
|
||||
None => None,
|
||||
Some(keys) => forest.find(keys),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_nonempty(&self) -> NonEmpty<A>
|
||||
where
|
||||
A: Clone,
|
||||
K: Clone,
|
||||
{
|
||||
match self {
|
||||
Self::Node { value, .. } => NonEmpty::new(value.clone()),
|
||||
Self::Branch { forest, .. } => forest.to_nonempty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &A> + 'a> {
|
||||
match self {
|
||||
SubTree::Node { value, .. } => Box::new(std::iter::once(value)),
|
||||
SubTree::Branch { ref forest, .. } => Box::new(forest.iter()),
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_keys<'a>(&'a self) -> Box<dyn Iterator<Item = &K> + 'a> {
|
||||
match self {
|
||||
SubTree::Node { key, .. } => Box::new(std::iter::once(key)),
|
||||
SubTree::Branch {
|
||||
ref key,
|
||||
ref forest,
|
||||
} => Box::new(std::iter::once(key).chain(forest.iter_keys())),
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_by<F>(&self, other: &Self, f: &F) -> Ordering
|
||||
where
|
||||
F: Fn(&A, &A) -> Ordering,
|
||||
{
|
||||
match (self, other) {
|
||||
(
|
||||
SubTree::Node { value, .. },
|
||||
SubTree::Node {
|
||||
value: other_value, ..
|
||||
},
|
||||
) => f(value, other_value),
|
||||
(SubTree::Branch { forest, .. }, SubTree::Node { value, .. }) => {
|
||||
let max_forest = forest.maximum_by(f);
|
||||
f(max_forest, value)
|
||||
},
|
||||
(SubTree::Node { value, .. }, SubTree::Branch { forest, .. }) => {
|
||||
let max_forest = &forest.maximum_by(f);
|
||||
f(value, max_forest)
|
||||
},
|
||||
(
|
||||
SubTree::Branch { forest, .. },
|
||||
SubTree::Branch {
|
||||
forest: other_forest,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
let max_forest = forest.maximum_by(f);
|
||||
let max_other_forest = other_forest.maximum_by(f);
|
||||
f(max_forest, max_other_forest)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maximum_by<F>(&self, f: &F) -> &A
|
||||
where
|
||||
F: Fn(&A, &A) -> Ordering,
|
||||
{
|
||||
match self {
|
||||
SubTree::Node { value, .. } => value,
|
||||
SubTree::Branch { forest, .. } => forest.maximum_by(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<F, B>(self, f: &mut F) -> SubTree<K, B>
|
||||
where
|
||||
F: FnMut(A) -> B,
|
||||
{
|
||||
match self {
|
||||
SubTree::Node { key, value } => SubTree::Node {
|
||||
key,
|
||||
value: f(value),
|
||||
},
|
||||
SubTree::Branch { key, forest } => SubTree::Branch {
|
||||
key,
|
||||
forest: Box::new(forest.map(f)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Tree<K, A>(pub NonEmpty<SubTree<K, A>>);
|
||||
|
||||
impl<K, A> From<Tree<K, A>> for Forest<K, A> {
|
||||
fn from(tree: Tree<K, A>) -> Self {
|
||||
Forest(Some(tree))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Forest<K, A>(pub Option<Tree<K, A>>);
|
||||
|
||||
impl<K, A> Tree<K, A> {
|
||||
/// Create a new `Tree` containing a single `Branch` given
|
||||
/// the key and sub-tree.
|
||||
pub fn branch(key: K, forest: Self) -> Self {
|
||||
Tree(NonEmpty::new(SubTree::branch(key, forest)))
|
||||
}
|
||||
|
||||
/// Create a new `Tree` containing a single `Node`.
|
||||
pub fn node(key: K, value: A) -> Self {
|
||||
Tree(NonEmpty::new(SubTree::Node { key, value }))
|
||||
}
|
||||
|
||||
/// Create a new `Tree` that creates a series of
|
||||
/// `Branch`es built using the `keys`. The final `Branch`
|
||||
/// will contain the `node`.
|
||||
fn new(keys: NonEmpty<K>, node: A) -> Self
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
let (start, mut middle) = keys.into();
|
||||
let last = middle.pop();
|
||||
|
||||
match last {
|
||||
None => Tree::node(start, node),
|
||||
Some(last) => {
|
||||
let mut branch = Tree::node(last, node);
|
||||
|
||||
for key in middle.into_iter().rev() {
|
||||
branch = Tree(NonEmpty::new(SubTree::branch(key, branch)))
|
||||
}
|
||||
|
||||
Tree::branch(start, branch)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a binary search in the sub-trees, based on comparing
|
||||
/// each of the sub-trees' key to the provided `key`.
|
||||
fn search(&self, key: &K) -> Result<usize, usize>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
self.0.binary_search_by(|tree| tree.key().cmp(key))
|
||||
}
|
||||
|
||||
pub fn map<F, B>(self, mut f: F) -> Tree<K, B>
|
||||
where
|
||||
F: FnMut(A) -> B,
|
||||
{
|
||||
Tree(self.0.map(|tree| tree.map(&mut f)))
|
||||
}
|
||||
|
||||
/// Insert a `node` into the list of sub-trees.
|
||||
///
|
||||
/// The node's position will be based on the `Ord` instance
|
||||
/// of `K`.
|
||||
fn insert_node_with<F>(&mut self, key: K, value: A, f: F)
|
||||
where
|
||||
F: FnOnce(&mut A),
|
||||
K: Ord,
|
||||
{
|
||||
let result = self.search(&key);
|
||||
|
||||
match result {
|
||||
Ok(index) => {
|
||||
let old_node = self.0.get_mut(index).unwrap();
|
||||
match old_node {
|
||||
SubTree::Node { value: old, .. } => f(old),
|
||||
SubTree::Branch { .. } => *old_node = SubTree::Node { key, value },
|
||||
}
|
||||
},
|
||||
Err(index) => self.0.insert(index, SubTree::Node { key, value }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert the `node` in the position given by `keys`.
|
||||
///
|
||||
/// If the same path to a node is provided the `node` will replace the old
|
||||
/// one, i.e. if `a/b/c` exists in the tree and `a/b/c` is the full path
|
||||
/// to the node, then `c` will be replaced.
|
||||
///
|
||||
/// If the path points to a branch, then the `node` will be inserted in this
|
||||
/// branch.
|
||||
///
|
||||
/// If a portion of the path points to a node then a branch will be created
|
||||
/// in its place, i.e. if `a/b/c` exists in the tree and the provided
|
||||
/// path is `a/b/c/d`, then the node `c` will be replaced by a branch
|
||||
/// `c/d`.
|
||||
///
|
||||
/// If the path does not exist it will be inserted into the set of
|
||||
/// sub-trees.
|
||||
fn insert_with<F>(&mut self, keys: NonEmpty<K>, value: A, f: F)
|
||||
where
|
||||
F: FnOnce(&mut A),
|
||||
K: Ord,
|
||||
{
|
||||
let (head, tail) = keys.into();
|
||||
let maybe_keys = NonEmpty::from_vec(tail);
|
||||
match self.search(&head) {
|
||||
// Found the label in our set of sub-trees
|
||||
Ok(index) => match maybe_keys {
|
||||
// The keys have been exhausted and so its time to insert the node
|
||||
None => {
|
||||
let sub_tree = self.0.get_mut(index).unwrap();
|
||||
match sub_tree {
|
||||
// Our sub-tree was a node.
|
||||
SubTree::Node { key, value } => {
|
||||
let _ = std::mem::replace(key, head);
|
||||
f(value);
|
||||
},
|
||||
SubTree::Branch { .. } => *sub_tree = SubTree::Node { key: head, value },
|
||||
}
|
||||
},
|
||||
Some(keys) => {
|
||||
let sub_tree = self.0.get_mut(index).unwrap();
|
||||
match sub_tree {
|
||||
// We have reached a node, but still have keys left to get through.
|
||||
SubTree::Node { .. } => {
|
||||
let new_tree = SubTree::branch(head, Tree::new(keys, value));
|
||||
*sub_tree = new_tree
|
||||
},
|
||||
// We keep moving down the set of keys to find where to insert this node.
|
||||
SubTree::Branch { forest, .. } => forest.insert_with(keys, value, f),
|
||||
}
|
||||
},
|
||||
},
|
||||
// The label was not found and we have an index for insertion.
|
||||
Err(index) => match maybe_keys {
|
||||
// We create the branch with the head label and node, since there are
|
||||
// no more labels left.
|
||||
None => self.0.insert(index, SubTree::Node { key: head, value }),
|
||||
// We insert an entirely new branch with the full list of keys.
|
||||
Some(tail) => self
|
||||
.0
|
||||
.insert(index, SubTree::branch(head, Tree::new(tail, value))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, keys: NonEmpty<K>, value: A)
|
||||
where
|
||||
A: Clone,
|
||||
K: Ord,
|
||||
{
|
||||
self.insert_with(keys, value.clone(), |old| *old = value)
|
||||
}
|
||||
|
||||
pub fn to_nonempty(&self) -> NonEmpty<A>
|
||||
where
|
||||
A: Clone,
|
||||
K: Clone,
|
||||
{
|
||||
self.0.clone().flat_map(|sub_tree| sub_tree.to_nonempty())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &A> {
|
||||
self.0.iter().flat_map(|tree| tree.iter())
|
||||
}
|
||||
|
||||
pub fn iter_keys(&self) -> impl Iterator<Item = &K> {
|
||||
self.0.iter().flat_map(|tree| tree.iter_keys())
|
||||
}
|
||||
|
||||
pub fn iter_subtrees(&self) -> impl Iterator<Item = &SubTree<K, A>> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub fn find_node(&self, keys: NonEmpty<K>) -> Option<&A>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
self.find(keys).and_then(|tree| match tree {
|
||||
SubTree::Node { value, .. } => Some(value),
|
||||
SubTree::Branch { .. } => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_branch(&self, keys: NonEmpty<K>) -> Option<&Self>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
self.find(keys).and_then(|tree| match tree {
|
||||
SubTree::Node { .. } => None,
|
||||
SubTree::Branch { ref forest, .. } => Some(&**forest),
|
||||
})
|
||||
}
|
||||
|
||||
/// Find a `SubTree` given a search path. If the path does not match
|
||||
/// it will return `None`.
|
||||
pub fn find(&self, keys: NonEmpty<K>) -> Option<&SubTree<K, A>>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
let (head, tail) = keys.into();
|
||||
let tail = NonEmpty::from_vec(tail);
|
||||
match self.search(&head) {
|
||||
Err(_) => None,
|
||||
Ok(index) => {
|
||||
let sub_tree = self.0.get(index).unwrap();
|
||||
match tail {
|
||||
None => match sub_tree {
|
||||
SubTree::Node { .. } => Some(sub_tree),
|
||||
SubTree::Branch { .. } => Some(sub_tree),
|
||||
},
|
||||
Some(mut tail) => {
|
||||
tail.insert(0, head);
|
||||
sub_tree.find(tail)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maximum_by<F>(&self, f: &F) -> &A
|
||||
where
|
||||
F: Fn(&A, &A) -> Ordering,
|
||||
{
|
||||
self.0.maximum_by(|s, t| s.compare_by(t, f)).maximum_by(f)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn maximum(&self) -> &A
|
||||
where
|
||||
A: Ord,
|
||||
{
|
||||
self.maximum_by(&|a, b| a.cmp(b))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, A> Forest<K, A> {
|
||||
pub fn root() -> Self {
|
||||
Forest(None)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_none()
|
||||
}
|
||||
|
||||
fn insert_forest(&mut self, forest: Tree<K, A>) {
|
||||
self.0 = Some(forest)
|
||||
}
|
||||
|
||||
/// Insert the `node` in the position given by `keys`.
|
||||
///
|
||||
/// If the same path to a node is provided the `node` will replace the old
|
||||
/// one, i.e. if `a/b/c` exists in the tree and `a/b/c` is the full path
|
||||
/// to the node, then `c` will be replaced.
|
||||
///
|
||||
/// If the path points to a branch, then the `node` will be inserted in this
|
||||
/// branch.
|
||||
///
|
||||
/// If a portion of the path points to a node then a branch will be created
|
||||
/// in its place, i.e. if `a/b/c` exists in the tree and the provided
|
||||
/// path is `a/b/c/d`, then the node `c` will be replaced by a branch
|
||||
/// `c/d`.
|
||||
///
|
||||
/// If the path does not exist it will be inserted into the set of
|
||||
/// sub-trees.
|
||||
#[allow(dead_code)]
|
||||
pub fn insert(&mut self, keys: NonEmpty<K>, node: A)
|
||||
where
|
||||
A: Clone,
|
||||
K: Ord,
|
||||
{
|
||||
self.insert_with(keys, node.clone(), |old| *old = node)
|
||||
}
|
||||
|
||||
pub fn insert_with<F>(&mut self, keys: NonEmpty<K>, node: A, f: F)
|
||||
where
|
||||
F: FnOnce(&mut A),
|
||||
K: Ord,
|
||||
{
|
||||
let (prefix, node_key) = split_last(keys);
|
||||
match self.0.as_mut() {
|
||||
Some(forest) => match NonEmpty::from_vec(prefix) {
|
||||
None => {
|
||||
// Insert the node at the root
|
||||
forest.insert_node_with(node_key, node, f)
|
||||
},
|
||||
Some(mut keys) => {
|
||||
keys.push(node_key);
|
||||
forest.insert_with(keys, node, f)
|
||||
},
|
||||
},
|
||||
None => match NonEmpty::from_vec(prefix) {
|
||||
None => self.insert_forest(Tree::node(node_key, node)),
|
||||
Some(mut keys) => {
|
||||
keys.push(node_key);
|
||||
self.insert_forest(Tree::new(keys, node))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_node(&self, keys: NonEmpty<K>) -> Option<&A>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
self.0.as_ref().and_then(|trees| trees.find_node(keys))
|
||||
}
|
||||
|
||||
pub fn find_branch(&self, keys: NonEmpty<K>) -> Option<&Tree<K, A>>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
self.0.as_ref().and_then(|trees| trees.find_branch(keys))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Find a `SubTree` given a search path. If the path does not match
|
||||
/// it will return `None`.
|
||||
pub fn find(&self, keys: NonEmpty<K>) -> Option<&SubTree<K, A>>
|
||||
where
|
||||
K: Ord,
|
||||
{
|
||||
self.0.as_ref().and_then(|trees| trees.find(keys))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn maximum_by<F>(&self, f: F) -> Option<&A>
|
||||
where
|
||||
F: Fn(&A, &A) -> Ordering,
|
||||
{
|
||||
self.0.as_ref().map(|trees| trees.maximum_by(&f))
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &A> {
|
||||
self.0.iter().flat_map(|trees| trees.iter())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn iter_keys(&self) -> impl Iterator<Item = &K> {
|
||||
self.0.iter().flat_map(|trees| trees.iter_keys())
|
||||
}
|
||||
}
|
||||
@@ -89,38 +89,3 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
}
|
||||
|
||||
/// A private enum that captures a recoverable and
|
||||
/// non-recoverable error when walking the git tree.
|
||||
///
|
||||
/// In the case of `NotBlob` we abort the the computation but do
|
||||
/// a check for it and recover.
|
||||
///
|
||||
/// In the of `Git` we abort both computations.
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum TreeWalkError {
|
||||
#[error("entry is not a blob")]
|
||||
NotBlob,
|
||||
#[error("git object is a commit")]
|
||||
Commit,
|
||||
#[error(transparent)]
|
||||
Git(#[from] Error),
|
||||
}
|
||||
|
||||
impl From<git2::Error> for TreeWalkError {
|
||||
fn from(err: git2::Error) -> Self {
|
||||
TreeWalkError::Git(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<file_system::Error> for TreeWalkError {
|
||||
fn from(err: file_system::Error) -> Self {
|
||||
err.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<str::Utf8Error> for TreeWalkError {
|
||||
fn from(err: str::Utf8Error) -> Self {
|
||||
err.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
use crate::{
|
||||
diff::*,
|
||||
file_system,
|
||||
file_system::directory,
|
||||
file_system::{directory, DirectoryContents, Label},
|
||||
vcs,
|
||||
vcs::{
|
||||
git::{
|
||||
@@ -37,10 +37,11 @@ use crate::{
|
||||
Vcs,
|
||||
},
|
||||
};
|
||||
use directory::Directory;
|
||||
use nonempty::NonEmpty;
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap, HashSet},
|
||||
collections::{BTreeSet, HashSet},
|
||||
convert::TryFrom,
|
||||
str,
|
||||
};
|
||||
@@ -163,14 +164,17 @@ impl<'a> RepositoryRef<'a> {
|
||||
}
|
||||
|
||||
/// Get the [`Diff`] between two commits.
|
||||
pub fn diff(&self, from: Oid, to: Oid) -> Result<Diff, Error> {
|
||||
self.diff_commits(None, Some(from.into()), to.into())
|
||||
pub fn diff(&self, from: &Rev, to: &Rev) -> Result<Diff, Error> {
|
||||
let from_commit = self.rev_to_commit(from)?;
|
||||
let to_commit = self.rev_to_commit(to)?;
|
||||
self.diff_commits(None, Some(&from_commit), &to_commit)
|
||||
.and_then(|diff| Diff::try_from(diff).map_err(Error::from))
|
||||
}
|
||||
|
||||
/// Get the [`Diff`] of a commit with no parents.
|
||||
pub fn initial_diff(&self, oid: Oid) -> Result<Diff, Error> {
|
||||
self.diff_commits(None, None, oid.into())
|
||||
pub fn initial_diff(&self, rev: &Rev) -> Result<Diff, Error> {
|
||||
let commit = self.rev_to_commit(rev)?;
|
||||
self.diff_commits(None, None, &commit)
|
||||
.and_then(|diff| Diff::try_from(diff).map_err(Error::from))
|
||||
}
|
||||
|
||||
@@ -180,11 +184,9 @@ impl<'a> RepositoryRef<'a> {
|
||||
}
|
||||
|
||||
/// Gets a snapshot of the repo as a Directory.
|
||||
pub fn snapshot(&self, rev: &Rev) -> Result<directory::Directory, Error> {
|
||||
pub fn snapshot(&self, rev: &Rev) -> Result<Directory, Error> {
|
||||
let commit = self.rev_to_commit(rev)?;
|
||||
let oid: Oid = commit.id().into();
|
||||
let tree = self.get_tree(&oid)?;
|
||||
Ok(directory::Directory::from_hash_map(tree))
|
||||
self.directory_of_commit(&commit)
|
||||
}
|
||||
|
||||
/// Returns the last commit, if exists, for a `path` in the history of
|
||||
@@ -254,6 +256,12 @@ impl<'a> RepositoryRef<'a> {
|
||||
Ok(head_commit.id().into())
|
||||
}
|
||||
|
||||
/// Returns the Oid of `rev`.
|
||||
pub fn rev_oid(&self, rev: &Rev) -> Result<Oid, Error> {
|
||||
let commit = self.rev_to_commit(rev)?;
|
||||
Ok(commit.id().into())
|
||||
}
|
||||
|
||||
pub(super) fn rev_to_commit(&self, rev: &Rev) -> Result<git2::Commit, Error> {
|
||||
match rev {
|
||||
Rev::Oid(oid) => Ok(self.repo_ref.find_commit((*oid).into())?),
|
||||
@@ -406,9 +414,8 @@ impl<'a> RepositoryRef<'a> {
|
||||
commit: &git2::Commit,
|
||||
) -> Result<Option<file_system::Path>, Error> {
|
||||
let mut parents = commit.parents();
|
||||
let parent = parents.next().map(|c| c.id());
|
||||
|
||||
let diff = self.diff_commits(Some(path), parent, commit.id())?;
|
||||
let diff = self.diff_commits(Some(path), parents.next().as_ref(), commit)?;
|
||||
if let Some(_delta) = diff.deltas().next() {
|
||||
Ok(Some(path.clone()))
|
||||
} else {
|
||||
@@ -419,13 +426,11 @@ impl<'a> RepositoryRef<'a> {
|
||||
fn diff_commits(
|
||||
&self,
|
||||
path: Option<&file_system::Path>,
|
||||
from: Option<git2::Oid>,
|
||||
to: git2::Oid,
|
||||
from: Option<&git2::Commit>,
|
||||
to: &git2::Commit,
|
||||
) -> Result<git2::Diff, Error> {
|
||||
let new_tree = self.repo_ref.find_commit(to)?.tree()?;
|
||||
let old_tree = from.map_or(Ok(None), |oid| {
|
||||
self.repo_ref.find_commit(oid)?.tree().map(Some)
|
||||
})?;
|
||||
let new_tree = to.tree()?;
|
||||
let old_tree = from.map_or(Ok(None), |c| c.tree().map(Some))?;
|
||||
|
||||
let mut opts = git2::DiffOptions::new();
|
||||
if let Some(path) = path {
|
||||
@@ -441,97 +446,98 @@ impl<'a> RepositoryRef<'a> {
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
fn update_file_map(
|
||||
path: file_system::Path,
|
||||
name: file_system::Label,
|
||||
file: directory::File,
|
||||
files: &mut HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
|
||||
) {
|
||||
files
|
||||
.entry(path)
|
||||
.and_modify(|entries| entries.push((name.clone(), file.clone())))
|
||||
.or_insert_with(|| NonEmpty::new((name, file)));
|
||||
}
|
||||
|
||||
/// Do a pre-order TreeWalk of the given commit. This turns a Tree
|
||||
/// into a HashMap of Paths and a list of Files. We can then turn that
|
||||
/// into a Directory.
|
||||
fn get_tree(
|
||||
&self,
|
||||
oid: &Oid,
|
||||
) -> Result<HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>, Error>
|
||||
{
|
||||
let mut file_paths_or_error: Result<
|
||||
HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
|
||||
Error,
|
||||
> = Ok(HashMap::new());
|
||||
|
||||
let commit = self.repo_ref.find_commit((*oid).into())?;
|
||||
/// Generates a Directory for the commit.
|
||||
fn directory_of_commit(&self, commit: &git2::Commit) -> Result<Directory, Error> {
|
||||
let mut parent_dirs = vec![Directory::root()];
|
||||
let tree = commit.as_object().peel_to_tree()?;
|
||||
|
||||
tree.walk(git2::TreeWalkMode::PreOrder, |s, entry| {
|
||||
match self.tree_entry_to_file_and_path(s, entry) {
|
||||
Ok((path, name, file)) => {
|
||||
match file_paths_or_error.as_mut() {
|
||||
Ok(files) => Self::update_file_map(path, name, file, files),
|
||||
let tree_level = s.split('/').count();
|
||||
if tree_level < parent_dirs.len() {
|
||||
// As it is PreOrder, the last directory A was visited
|
||||
// completely and we are back to the level. Now insert A
|
||||
// into its parent directory.
|
||||
if let Some(last_dir) = parent_dirs.pop() {
|
||||
if let Some(parent) = parent_dirs.last_mut() {
|
||||
let name = last_dir.name().clone();
|
||||
let content = DirectoryContents::Directory(last_dir);
|
||||
parent.insert(name, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need to update, we want to keep the error.
|
||||
Err(_err) => {},
|
||||
match entry.kind() {
|
||||
Some(git2::ObjectType::Tree) => {
|
||||
if let Some(name) = entry.name() {
|
||||
// Add a new level of directory.
|
||||
match name.parse() {
|
||||
Ok(label) => parent_dirs.push(Directory::new(label)),
|
||||
Err(_) => {
|
||||
return git2::TreeWalkResult::Abort;
|
||||
},
|
||||
}
|
||||
}
|
||||
git2::TreeWalkResult::Ok
|
||||
},
|
||||
Err(err) => match err {
|
||||
// We want to continue if the entry was not a Blob.
|
||||
TreeWalkError::NotBlob => git2::TreeWalkResult::Ok,
|
||||
|
||||
// We found a ObjectType::Commit (likely a submodule) and
|
||||
// so we can skip it.
|
||||
TreeWalkError::Commit => git2::TreeWalkResult::Ok,
|
||||
|
||||
// But we want to keep the error and abort otherwise.
|
||||
TreeWalkError::Git(err) => {
|
||||
file_paths_or_error = Err(err);
|
||||
git2::TreeWalkResult::Abort
|
||||
},
|
||||
Some(git2::ObjectType::Blob) => {
|
||||
// Construct a File to insert into its parent directory.
|
||||
let object = match entry.to_object(self.repo_ref) {
|
||||
Ok(obj) => obj,
|
||||
Err(_) => {
|
||||
return git2::TreeWalkResult::Abort;
|
||||
},
|
||||
};
|
||||
let blob = match object.as_blob() {
|
||||
Some(b) => b,
|
||||
None => return git2::TreeWalkResult::Abort,
|
||||
};
|
||||
let f = directory::File::new(blob.content());
|
||||
let label = match entry.name() {
|
||||
Some(name) => match name.parse::<Label>() {
|
||||
Ok(label) => label,
|
||||
Err(_) => {
|
||||
return git2::TreeWalkResult::Abort;
|
||||
},
|
||||
},
|
||||
None => return git2::TreeWalkResult::Abort,
|
||||
};
|
||||
let content = DirectoryContents::File {
|
||||
name: label.clone(),
|
||||
file: f,
|
||||
};
|
||||
let parent = match parent_dirs.last_mut() {
|
||||
Some(parent_dir) => parent_dir,
|
||||
None => return git2::TreeWalkResult::Abort,
|
||||
};
|
||||
parent.insert(label, content);
|
||||
},
|
||||
_ => {
|
||||
return git2::TreeWalkResult::Skip;
|
||||
},
|
||||
}
|
||||
|
||||
git2::TreeWalkResult::Ok
|
||||
})?;
|
||||
|
||||
file_paths_or_error
|
||||
}
|
||||
|
||||
fn tree_entry_to_file_and_path(
|
||||
&self,
|
||||
tree_path: &str,
|
||||
entry: &git2::TreeEntry,
|
||||
) -> Result<(file_system::Path, file_system::Label, directory::File), TreeWalkError> {
|
||||
// Account for the "root" of git being the empty string
|
||||
let path = if tree_path.is_empty() {
|
||||
Ok(file_system::Path::root())
|
||||
} else {
|
||||
file_system::Path::try_from(tree_path)
|
||||
}?;
|
||||
|
||||
// We found a Commit object in the Tree, likely a submodule.
|
||||
// We will skip this entry.
|
||||
if let Some(git2::ObjectType::Commit) = entry.kind() {
|
||||
return Err(TreeWalkError::Commit);
|
||||
// Tree walk is complete but there are some levels of dirs
|
||||
// that are not popped up from `parent_dirs` yet. Note that
|
||||
// the root dir is `parent_dirs[0]`.
|
||||
//
|
||||
// We need to pop up `parent_dirs` fully and update the directory
|
||||
// content at each level.
|
||||
while let Some(curr_dir) = parent_dirs.pop() {
|
||||
match parent_dirs.last_mut() {
|
||||
Some(parent) => {
|
||||
let name = curr_dir.name().clone();
|
||||
let content = DirectoryContents::Directory(curr_dir);
|
||||
parent.insert(name, content);
|
||||
},
|
||||
None => return Ok(curr_dir), // No more parent, we're at the root.
|
||||
}
|
||||
}
|
||||
|
||||
let object = entry.to_object(self.repo_ref)?;
|
||||
let blob = object.as_blob().ok_or(TreeWalkError::NotBlob)?;
|
||||
let name = str::from_utf8(entry.name_bytes())?;
|
||||
|
||||
let name = file_system::Label::try_from(name).map_err(Error::FileSystem)?;
|
||||
|
||||
Ok((
|
||||
path,
|
||||
name,
|
||||
directory::File {
|
||||
contents: blob.content().to_owned(),
|
||||
size: blob.size(),
|
||||
},
|
||||
))
|
||||
Err(Error::RevParseFailure {
|
||||
rev: commit.id().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod list_directory {
|
||||
use radicle_surf::file_system::{unsound, Directory, File, SystemType};
|
||||
use radicle_surf::file_system::{unsound, Directory, File, Label};
|
||||
|
||||
#[test]
|
||||
fn root_files() {
|
||||
@@ -23,12 +23,13 @@ mod list_directory {
|
||||
File::new(b"module BananaBaz ..."),
|
||||
);
|
||||
|
||||
let files: Vec<Label> = directory.contents().map(|c| c.label().clone()).collect();
|
||||
assert_eq!(
|
||||
directory.list_directory(),
|
||||
files,
|
||||
vec![
|
||||
SystemType::file(unsound::label::new("bar.hs")),
|
||||
SystemType::file(unsound::label::new("baz.hs")),
|
||||
SystemType::file(unsound::label::new("foo.hs")),
|
||||
"bar.hs".parse::<Label>().unwrap(),
|
||||
"baz.hs".parse::<Label>().unwrap(),
|
||||
"foo.hs".parse::<Label>().unwrap(),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -42,11 +43,11 @@ mod find_file {
|
||||
fn in_root() {
|
||||
let file = File::new(b"module Banana ...");
|
||||
let mut directory = Directory::root();
|
||||
directory.insert_file(unsound::path::new("foo.hs"), file.clone());
|
||||
directory.insert_file(unsound::path::new("bar/foo.hs"), file.clone());
|
||||
|
||||
assert_eq!(
|
||||
directory.find_file(unsound::path::new("foo.hs")),
|
||||
Some(file)
|
||||
directory.find_file(unsound::path::new("bar/foo.hs")),
|
||||
Some(&file)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,113 +90,6 @@ mod directory_size {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod properties {
|
||||
use nonempty::NonEmpty;
|
||||
use proptest::{collection, prelude::*};
|
||||
use radicle_surf::file_system::{unsound, *};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_all_directories_and_files() {
|
||||
let mut directory_map = HashMap::new();
|
||||
|
||||
let path1 = unsound::path::new("foo/bar/baz");
|
||||
let file1 = (unsound::label::new("monadic.rs"), File::new(&[]));
|
||||
let file2 = (unsound::label::new("oscoin.rs"), File::new(&[]));
|
||||
directory_map.insert(path1, NonEmpty::from((file1, vec![file2])));
|
||||
|
||||
let path2 = unsound::path::new("foor/bar/quuz");
|
||||
let file3 = (unsound::label::new("radicle.rs"), File::new(&[]));
|
||||
|
||||
directory_map.insert(path2, NonEmpty::new(file3));
|
||||
|
||||
assert!(prop_all_directories_and_files(directory_map))
|
||||
}
|
||||
|
||||
fn label_strategy() -> impl Strategy<Value = Label> {
|
||||
// ASCII regex, excluding '/' because of posix file paths
|
||||
"[ -.|0-~]+".prop_map(|label| unsound::label::new(&label))
|
||||
}
|
||||
|
||||
fn path_strategy(max_size: usize) -> impl Strategy<Value = Path> {
|
||||
(
|
||||
label_strategy(),
|
||||
collection::vec(label_strategy(), 0..max_size),
|
||||
)
|
||||
.prop_map(|(label, labels)| Path((label, labels).into()))
|
||||
}
|
||||
|
||||
fn file_strategy() -> impl Strategy<Value = (Label, File)> {
|
||||
// ASCII regex, see: https://catonmat.net/my-favorite-regex
|
||||
(label_strategy(), "[ -~]*")
|
||||
.prop_map(|(name, contents)| (name, File::new(contents.as_bytes())))
|
||||
}
|
||||
|
||||
fn directory_map_strategy(
|
||||
path_size: usize,
|
||||
n_files: usize,
|
||||
map_size: usize,
|
||||
) -> impl Strategy<Value = HashMap<Path, NonEmpty<(Label, File)>>> {
|
||||
collection::hash_map(
|
||||
path_strategy(path_size),
|
||||
collection::vec(file_strategy(), 1..n_files).prop_map(|files| {
|
||||
NonEmpty::from_slice(&files).expect("Strategy generated files of length 0")
|
||||
}),
|
||||
0..map_size,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(fintan): This is a bit slow. Could be time to benchmark some functions.
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_test_all_directories_and_files(directory_map in directory_map_strategy(10, 10, 10)) {
|
||||
prop_all_directories_and_files(directory_map);
|
||||
}
|
||||
}
|
||||
|
||||
fn prop_all_directories_and_files(
|
||||
directory_map: HashMap<Path, NonEmpty<(Label, File)>>,
|
||||
) -> bool {
|
||||
let mut new_directory_map = HashMap::new();
|
||||
for (path, files) in directory_map {
|
||||
new_directory_map.insert(path.clone(), files);
|
||||
}
|
||||
|
||||
let directory = Directory::from_hash_map(new_directory_map.clone());
|
||||
|
||||
for (directory_path, files) in new_directory_map {
|
||||
for (file_name, _) in files.iter() {
|
||||
let mut path = directory_path.clone();
|
||||
if directory.find_directory(path.clone()).is_none() {
|
||||
eprintln!("Search Directory: {:#?}", directory);
|
||||
eprintln!("Path to find: {:#?}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
path.push(file_name.clone());
|
||||
if directory.find_file(path.clone()).is_none() {
|
||||
eprintln!("Search Directory: {:#?}", directory);
|
||||
eprintln!("Path to find: {:#?}", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_name_is_same_as_root() {
|
||||
// This test ensures that if the name is the same the root of the
|
||||
// directory, that search_path.split_last() doesn't toss away the prefix.
|
||||
let path = unsound::path::new("foo/bar/~");
|
||||
let mut directory_map = HashMap::new();
|
||||
directory_map.insert(path, NonEmpty::new((Label::root(), File::new(b"root"))));
|
||||
|
||||
assert!(prop_all_directories_and_files(directory_map));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod path {
|
||||
use radicle_surf::file_system::unsound;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use radicle_surf::git::{Author, BranchType, Commit};
|
||||
use radicle_surf::{
|
||||
diff::*,
|
||||
file_system::{unsound, Path},
|
||||
file_system::{unsound, DirectoryContents, Path},
|
||||
git::{error::Error, Branch, BranchName, Namespace, Oid, RefScope, Repository, Rev, TagName},
|
||||
};
|
||||
|
||||
@@ -402,7 +402,7 @@ mod diff {
|
||||
assert!(commit.parents().count() == 0);
|
||||
assert!(commit.parent(0).is_err());
|
||||
|
||||
let diff = repo.initial_diff(oid)?;
|
||||
let diff = repo.initial_diff(&oid.into())?;
|
||||
|
||||
let expected_diff = Diff {
|
||||
created: vec![CreateFile {
|
||||
@@ -433,11 +433,11 @@ mod diff {
|
||||
fn test_diff() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let commit = repo
|
||||
.get_git2_commit(Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?)
|
||||
.unwrap();
|
||||
let oid = Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?;
|
||||
let commit = repo.get_git2_commit(oid).unwrap();
|
||||
let parent = commit.parent(0)?;
|
||||
let diff = repo.diff(parent.id().into(), commit.id().into())?;
|
||||
let parent_oid: Oid = parent.id().into();
|
||||
let diff = repo.diff(&parent_oid.into(), &oid.into())?;
|
||||
|
||||
let expected_diff = Diff {
|
||||
created: vec![],
|
||||
@@ -784,3 +784,33 @@ mod reference {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod code_browsing {
|
||||
use super::*;
|
||||
use radicle_surf::file_system::Directory;
|
||||
|
||||
#[test]
|
||||
fn iterate_root_dir_recursive() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let root_dir = repo.snapshot(&Branch::local("master").into()).unwrap();
|
||||
let count = println_dir(&root_dir, 0);
|
||||
assert_eq!(count, 36); // Check total file count.
|
||||
|
||||
/// Prints items in `dir` with `indent_level`.
|
||||
/// For sub-directories, will do Depth-First-Search and print
|
||||
/// recursively.
|
||||
/// Returns the number of items visited (i.e. printed)
|
||||
fn println_dir(dir: &Directory, indent_level: usize) -> i32 {
|
||||
let mut count = 0;
|
||||
for item in dir.contents() {
|
||||
println!("> {}{}", " ".repeat(indent_level * 4), &item.label());
|
||||
count += 1;
|
||||
if let DirectoryContents::Directory(sub_dir) = item {
|
||||
count += println_dir(sub_dir, indent_level + 1);
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,3 @@ mod diff;
|
||||
|
||||
#[cfg(test)]
|
||||
mod file_system;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tree;
|
||||
|
||||
@@ -1,692 +0,0 @@
|
||||
// Copyright © 2022 The Radicle Git Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
//! Unit tests for radicle_surf::tree.
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use pretty_assertions::assert_eq;
|
||||
use radicle_surf::tree::{Forest, SubTree, Tree};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct TestNode {
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_empty() {
|
||||
let mut tree = Forest::root();
|
||||
assert!(tree.is_empty());
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(NonEmpty::new(String::from("a")), a_node);
|
||||
assert!(!tree.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_root_node() {
|
||||
let a_label = String::from("a");
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(NonEmpty::new(a_label), a_node.clone());
|
||||
|
||||
assert_eq!(tree, Forest(Some(Tree::node(String::from("a"), a_node))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_with_prepending_root_nodes() {
|
||||
let a_label = String::from("a");
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
let b_node = TestNode { id: 2 };
|
||||
|
||||
tree.insert_with(
|
||||
NonEmpty::new(a_label.clone()),
|
||||
NonEmpty::new(a_node.clone()),
|
||||
|nodes| nodes.insert(0, a_node.clone()),
|
||||
);
|
||||
tree.insert_with(
|
||||
NonEmpty::new(a_label),
|
||||
NonEmpty::new(b_node.clone()),
|
||||
|nodes| nodes.insert(0, b_node.clone()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::node(
|
||||
String::from("a"),
|
||||
NonEmpty::from((b_node, vec![a_node]))
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_with_prepending_branch_nodes() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let path = NonEmpty::from((a_label, vec![b_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
let b_node = TestNode { id: 2 };
|
||||
|
||||
tree.insert_with(path.clone(), NonEmpty::new(a_node.clone()), |nodes| {
|
||||
nodes.insert(0, a_node.clone())
|
||||
});
|
||||
tree.insert_with(path, NonEmpty::new(b_node.clone()), |nodes| {
|
||||
nodes.insert(0, b_node.clone())
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::node(String::from("b"), NonEmpty::from((b_node, vec![a_node])))
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_single_node() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let path = NonEmpty::from((a_label, vec![b_label, c_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(path, c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::branch(String::from("b"), Tree::node(String::from("c"), c_node))
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_two_nodes() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let d_label = String::from("d");
|
||||
let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
|
||||
let d_path = NonEmpty::from((a_label, vec![b_label, d_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(c_path, c_node.clone());
|
||||
|
||||
let d_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(d_path, d_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::branch(
|
||||
String::from("b"),
|
||||
Tree(NonEmpty::from((
|
||||
SubTree::Node {
|
||||
key: String::from("c"),
|
||||
value: c_node
|
||||
},
|
||||
vec![SubTree::Node {
|
||||
key: String::from("d"),
|
||||
value: d_node
|
||||
}]
|
||||
)))
|
||||
)
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_replaces_node() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let c_path = NonEmpty::from((a_label, vec![b_label, c_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(c_path.clone(), c_node);
|
||||
|
||||
let new_c_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(c_path, new_c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::branch(
|
||||
String::from("b"),
|
||||
Tree(NonEmpty::new(SubTree::Node {
|
||||
key: String::from("c"),
|
||||
value: new_c_node
|
||||
},))
|
||||
)
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_replaces_root_node() {
|
||||
let c_label = String::from("c");
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(NonEmpty::new(c_label.clone()), c_node);
|
||||
|
||||
let new_c_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(NonEmpty::new(c_label), new_c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::node(String::from("c"), new_c_node)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_replaces_branch_node() {
|
||||
let a_label = String::from("a");
|
||||
let c_label = String::from("c");
|
||||
let c_path = NonEmpty::from((a_label, vec![c_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(c_path.clone(), c_node);
|
||||
|
||||
let new_c_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(c_path, new_c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::node(String::from("c"), new_c_node),
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_replaces_branch_with_node() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(c_path, c_node);
|
||||
|
||||
let new_c_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(NonEmpty::from((a_label, vec![b_label])), new_c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::node(String::from("b"), new_c_node),
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_replaces_node_with_branch() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()]));
|
||||
let c_path = NonEmpty::from((a_label, vec![b_label, c_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let b_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(b_path, b_node);
|
||||
|
||||
let new_c_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(c_path, new_c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::branch(
|
||||
String::from("b"),
|
||||
Tree(NonEmpty::new(SubTree::Node {
|
||||
key: String::from("c"),
|
||||
value: new_c_node
|
||||
},))
|
||||
)
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_replaces_node_with_branch_foo() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let d_label = String::from("d");
|
||||
let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()]));
|
||||
let d_path = NonEmpty::from((a_label, vec![b_label, c_label, d_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let b_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(b_path, b_node);
|
||||
|
||||
let d_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(d_path, d_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::branch(
|
||||
String::from("b"),
|
||||
Tree::branch(String::from("c"), Tree::node(String::from("d"), d_node))
|
||||
)
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_two_nodes_out_of_order() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let d_label = String::from("d");
|
||||
let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
|
||||
let d_path = NonEmpty::from((a_label, vec![b_label, d_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let d_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(d_path, d_node.clone());
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(c_path, c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree::branch(
|
||||
String::from("b"),
|
||||
Tree(NonEmpty::from((
|
||||
SubTree::Node {
|
||||
key: String::from("c"),
|
||||
value: c_node
|
||||
},
|
||||
vec![SubTree::Node {
|
||||
key: String::from("d"),
|
||||
value: d_node
|
||||
}]
|
||||
)))
|
||||
)
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_branch() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let d_label = String::from("d");
|
||||
let e_label = String::from("e");
|
||||
let f_label = String::from("f");
|
||||
|
||||
let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
|
||||
let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label]));
|
||||
let f_path = NonEmpty::from((a_label, vec![e_label, f_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
let d_node = TestNode { id: 3 };
|
||||
|
||||
let f_node = TestNode { id: 2 };
|
||||
|
||||
tree.insert(d_path, d_node.clone());
|
||||
tree.insert(c_path, c_node.clone());
|
||||
tree.insert(f_path, f_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree(NonEmpty::from((
|
||||
SubTree::Branch {
|
||||
key: String::from("b"),
|
||||
forest: Box::new(Tree(NonEmpty::from((
|
||||
SubTree::Node {
|
||||
key: String::from("c"),
|
||||
value: c_node
|
||||
},
|
||||
vec![SubTree::Node {
|
||||
key: String::from("d"),
|
||||
value: d_node
|
||||
}]
|
||||
))))
|
||||
},
|
||||
vec![SubTree::Branch {
|
||||
key: String::from("e"),
|
||||
forest: Box::new(Tree::node(String::from("f"), f_node))
|
||||
},]
|
||||
)))
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_two_branches() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let d_label = String::from("d");
|
||||
let e_label = String::from("e");
|
||||
let f_label = String::from("f");
|
||||
|
||||
let c_path = NonEmpty::from((a_label, vec![b_label, c_label]));
|
||||
let f_path = NonEmpty::from((d_label, vec![e_label, f_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
let f_node = TestNode { id: 2 };
|
||||
|
||||
tree.insert(c_path, c_node.clone());
|
||||
tree.insert(f_path, f_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree(NonEmpty::from((
|
||||
SubTree::Branch {
|
||||
key: String::from("a"),
|
||||
forest: Box::new(Tree::branch(
|
||||
String::from("b"),
|
||||
Tree::node(String::from("c"), c_node)
|
||||
)),
|
||||
},
|
||||
vec![SubTree::Branch {
|
||||
key: String::from("d"),
|
||||
forest: Box::new(Tree::branch(
|
||||
String::from("e"),
|
||||
Tree::node(String::from("f"), f_node)
|
||||
))
|
||||
}]
|
||||
)))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_branches_and_node() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let d_label = String::from("d");
|
||||
let e_label = String::from("e");
|
||||
let f_label = String::from("f");
|
||||
let g_label = String::from("g");
|
||||
|
||||
let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label]));
|
||||
let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label]));
|
||||
let e_path = NonEmpty::from((a_label.clone(), vec![e_label]));
|
||||
let g_path = NonEmpty::from((a_label, vec![f_label, g_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
let d_node = TestNode { id: 3 };
|
||||
|
||||
let e_node = TestNode { id: 2 };
|
||||
|
||||
let g_node = TestNode { id: 2 };
|
||||
|
||||
tree.insert(d_path, d_node.clone());
|
||||
tree.insert(c_path, c_node.clone());
|
||||
tree.insert(e_path, e_node.clone());
|
||||
tree.insert(g_path, g_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree,
|
||||
Forest(Some(Tree::branch(
|
||||
String::from("a"),
|
||||
Tree(NonEmpty::from((
|
||||
SubTree::Branch {
|
||||
key: String::from("b"),
|
||||
forest: Box::new(Tree(NonEmpty::from((
|
||||
SubTree::Node {
|
||||
key: String::from("c"),
|
||||
value: c_node
|
||||
},
|
||||
vec![SubTree::Node {
|
||||
key: String::from("d"),
|
||||
value: d_node
|
||||
}]
|
||||
))))
|
||||
},
|
||||
vec![
|
||||
SubTree::Node {
|
||||
key: String::from("e"),
|
||||
value: e_node
|
||||
},
|
||||
SubTree::Branch {
|
||||
key: String::from("f"),
|
||||
forest: Box::new(Tree::node(String::from("g"), g_node))
|
||||
},
|
||||
]
|
||||
)))
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_root_node() {
|
||||
let a_label = String::from("a");
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(NonEmpty::new(a_label), a_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree.find(NonEmpty::new(String::from("a"))),
|
||||
Some(&SubTree::Node {
|
||||
key: String::from("a"),
|
||||
value: a_node
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_branch_and_node() {
|
||||
let a_label = String::from("a");
|
||||
let b_label = String::from("b");
|
||||
let c_label = String::from("c");
|
||||
let path = NonEmpty::from((a_label, vec![b_label, c_label]));
|
||||
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let c_node = TestNode { id: 1 };
|
||||
|
||||
tree.insert(path, c_node.clone());
|
||||
|
||||
assert_eq!(
|
||||
tree.find(NonEmpty::new(String::from("a"))),
|
||||
Some(&SubTree::Branch {
|
||||
key: String::from("a"),
|
||||
forest: Box::new(Tree::branch(
|
||||
String::from("b"),
|
||||
Tree::node(String::from("c"), c_node.clone())
|
||||
))
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tree.find(NonEmpty::from((String::from("a"), vec![String::from("b")]))),
|
||||
Some(&SubTree::Branch {
|
||||
key: String::from("b"),
|
||||
forest: Box::new(Tree::node(String::from("c"), c_node.clone()))
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tree.find(NonEmpty::from((
|
||||
String::from("a"),
|
||||
vec![String::from("b"), String::from("c")]
|
||||
))),
|
||||
Some(&SubTree::Node {
|
||||
key: String::from("c"),
|
||||
value: c_node
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None);
|
||||
|
||||
assert_eq!(
|
||||
tree.find(NonEmpty::from((String::from("a"), vec![String::from("c")]))),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximum_by_root_nodes() {
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
let b_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(NonEmpty::new(String::from("a")), a_node.clone());
|
||||
tree.insert(NonEmpty::new(String::from("b")), b_node.clone());
|
||||
|
||||
assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
|
||||
assert_eq!(
|
||||
tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
|
||||
Some(&a_node)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximum_by_branch_and_node() {
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
let b_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(
|
||||
NonEmpty::from((String::from("c"), vec![String::from("a")])),
|
||||
a_node.clone(),
|
||||
);
|
||||
tree.insert(NonEmpty::new(String::from("b")), b_node.clone());
|
||||
|
||||
assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
|
||||
assert_eq!(
|
||||
tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
|
||||
Some(&a_node)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximum_by_branch_and_branch() {
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
let b_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(
|
||||
NonEmpty::from((String::from("c"), vec![String::from("a")])),
|
||||
a_node.clone(),
|
||||
);
|
||||
tree.insert(
|
||||
NonEmpty::from((String::from("d"), vec![String::from("a")])),
|
||||
b_node.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
|
||||
assert_eq!(
|
||||
tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
|
||||
Some(&a_node)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximum_by_branch_nodes() {
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
let b_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(
|
||||
NonEmpty::from((String::from("c"), vec![String::from("a")])),
|
||||
a_node.clone(),
|
||||
);
|
||||
tree.insert(
|
||||
NonEmpty::from((String::from("c"), vec![String::from("b")])),
|
||||
b_node.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node));
|
||||
assert_eq!(
|
||||
tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()),
|
||||
Some(&a_node)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fold_root_nodes() {
|
||||
let mut tree = Forest::root();
|
||||
|
||||
let a_node = TestNode { id: 1 };
|
||||
|
||||
let b_node = TestNode { id: 3 };
|
||||
|
||||
tree.insert(NonEmpty::new(String::from("a")), a_node);
|
||||
tree.insert(NonEmpty::new(String::from("b")), b_node);
|
||||
|
||||
assert_eq!(tree.iter().fold(0, |b, a| a.id + b), 4);
|
||||
}
|
||||
Reference in New Issue
Block a user