Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'origin/directory-generic'
Fintan Halpenny committed 3 years ago
commit 887cdc80106ba73dcd30afe6cc536de1bd43ad90
parent aad5f04
10 files changed +344 -326
modified radicle-surf/Cargo.toml
@@ -48,6 +48,10 @@ version = "0.2.0"
path = "../radicle-git-ext"
features = ["serde"]

+
[dependencies.radicle-std-ext]
+
version = "0.1.0"
+
path = "../radicle-std-ext"
+

[build-dependencies]
anyhow = "1.0"
flate2 = "1"
modified radicle-surf/examples/browsing.rs
@@ -25,7 +25,7 @@
//! the directories in a tree-like structure.

use radicle_surf::{
-
    file_system::{Directory, DirectoryEntry},
+
    file_system::{directory, Directory},
    git::Repository,
};
use std::{env, time::Instant};
@@ -51,10 +51,10 @@ fn main() {
fn print_directory(d: &Directory, repo: &Repository, indent_level: usize) {
    let indent = " ".repeat(indent_level * 4);
    println!("{}{}/", &indent, d.name());
-
    for entry in d.contents(repo).unwrap().iter() {
+
    for entry in d.entries(repo).unwrap().entries() {
        match entry {
-
            DirectoryEntry::File(f) => println!("    {}{}", &indent, f.name()),
-
            DirectoryEntry::Directory(d) => print_directory(d, repo, indent_level + 1),
+
            directory::Entry::File(f) => println!("    {}{}", &indent, f.name()),
+
            directory::Entry::Directory(d) => print_directory(d, repo, indent_level + 1),
        }
    }
}
modified radicle-surf/src/file_system.rs
@@ -111,8 +111,9 @@
//! ```

pub mod directory;
+
pub use directory::{Directory, Entries, Entry, File, FileContent};
mod error;
pub use error::Error;
mod path;

-
pub use self::{directory::*, path::*};
+
pub use self::path::*;
modified radicle-surf/src/file_system/directory.rs
@@ -20,55 +20,114 @@
//! A `Directory` is expected to be a non-empty tree of directories and files.
//! See [`Directory`] for more information.

-
use crate::{
-
    file_system::{error::LabelError, path::*, Error},
-
    git::{self, Repository, Revision},
-
};
-
use git2::Blob;
-
use radicle_git_ext::Oid;
use std::{
    collections::BTreeMap,
-
    convert::{Infallible, TryFrom},
+
    convert::{Infallible, Into},
    path,
};

-
/// Represents a `file` in a git repo.
+
use git2::Blob;
+
use radicle_git_ext::{is_not_found_err, Oid};
+
use radicle_std_ext::result::ResultExt as _;
+

+
use crate::{
+
    file_system::{path::*, Error},
+
    git::{Repository, Revision},
+
};
+

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

+
    #[derive(Debug, Error, PartialEq)]
+
    pub enum Directory {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error(transparent)]
+
        Entry(#[from] Entry),
+
        #[error(transparent)]
+
        File(#[from] File),
+
    }
+

+
    #[derive(Debug, Error, PartialEq, Eq)]
+
    pub enum Entry {
+
        #[error("the entry name was not valid UTF-8")]
+
        Utf8Error,
+
        #[error(transparent)]
+
        Label(#[from] super::Error),
+
    }
+

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

+
/// A `File` in a git repository.
+
///
+
/// The representation is lightweight and contains the [`Oid`] that
+
/// points to the git blob which is this file.
+
///
+
/// The name of a file can be retrieved via [`File::name`].
+
///
+
/// The [`FileContent`] of a file can be retrieved via
+
/// [`File::content`].
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct File {
    pub(crate) name: Label,
-
    pub(crate) oid: Oid,
+
    pub(crate) id: Oid,
}

impl File {
-
    /// Create a new `File`.
-
    pub fn new(name: String, oid: Oid) -> Self {
-
        File {
+
    /// The name of this `File`.
+
    pub fn name(&self) -> &str {
+
        self.name.as_str()
+
    }
+

+
    /// The object identifier of this `File`.
+
    pub fn id(&self) -> Oid {
+
        self.id
+
    }
+

+
    /// Create a new `File` with the `name` and `id` provided.
+
    ///
+
    /// The `id` must point to a `git` blob.
+
    pub fn new(name: String, id: Oid) -> Self {
+
        Self {
            name: Label {
                label: name,
                hidden: false,
            },
-
            oid,
+
            id,
        }
    }

-
    /// Returns the file name reference.
-
    pub fn name(&self) -> &str {
-
        self.name.as_str()
+
    /// Get the [`FileContent`] for this `File`.
+
    ///
+
    /// # Errors
+
    ///
+
    /// This function will fail if it could not find the `git` blob
+
    /// for the `Oid` of this `File`.
+
    pub fn content<'a>(&self, repo: &'a Repository) -> Result<FileContent<'a>, error::File> {
+
        let blob = repo.git2_repo().find_blob(self.id.into())?;
+
        Ok(FileContent { blob })
    }
}

-
/// Represents the actual content of a file.
+
/// The contents of a [`File`].
+
///
+
/// To construct a `FileContent` use [`File::content`].
pub struct FileContent<'a> {
    blob: Blob<'a>,
}

impl<'a> FileContent<'a> {
-
    /// Returns the file content as a byte slice.
+
    /// Return the file contents as a byte slice.
    pub fn as_bytes(&self) -> &[u8] {
        self.blob.content()
    }

-
    /// Returns the size of file
+
    /// Return the size of the file contents.
    pub fn size(&self) -> usize {
        self.blob.size()
    }
@@ -79,190 +138,176 @@ impl<'a> FileContent<'a> {
    }
}

-
/// Represents the listing of a directory.
-
pub struct DirectoryContent {
-
    listing: BTreeMap<Label, DirectoryEntry>,
+
/// A representations of a [`Directory`]'s entries.
+
pub struct Entries {
+
    listing: BTreeMap<Label, Entry>,
}

-
impl DirectoryContent {
-
    /// Returns an iterator for the listing of a directory.
-
    pub fn iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
+
impl Entries {
+
    /// Return the [`Label`]s of each [`Entry`].
+
    pub fn names(&self) -> impl Iterator<Item = &Label> {
+
        self.listing.keys()
+
    }
+

+
    /// Return each [`Entry`].
+
    pub fn entries(&self) -> impl Iterator<Item = &Entry> {
        self.listing.values()
    }
+

+
    /// Return each [`Label`] and [`Entry`].
+
    pub fn iter(&self) -> impl Iterator<Item = (&Label, &Entry)> {
+
        self.listing.iter()
+
    }
}

-
/// Represents an entry in a directory.
+
/// An `Entry` is either a [`File`] entry or a [`Directory`] entry.
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum DirectoryEntry {
-
    /// When the entry is a file.
+
pub enum Entry {
+
    /// A file entry within a [`Directory`].
    File(File),
-
    /// When the entry is a directory.
+
    /// A sub-directory of a [`Directory`].
    Directory(Directory),
}

-
impl DirectoryEntry {
-
    /// Returns the label (short name, without the parent path) of the entry.
+
impl Entry {
+
    /// Get a label for the `Entriess`, either the name of the [`File`]
+
    /// or the name of the [`Directory`].
    pub fn label(&self) -> &Label {
        match self {
-
            DirectoryEntry::File(file) => &file.name,
-
            DirectoryEntry::Directory(directory) => directory.name(),
+
            Entry::File(file) => &file.name,
+
            Entry::Directory(directory) => directory.name(),
        }
    }
+

+
    pub(crate) fn from_entry(entry: &git2::TreeEntry) -> Result<Option<Self>, error::Entry> {
+
        let name = Label {
+
            label: entry.name().ok_or(error::Entry::Utf8Error)?.to_string(),
+
            hidden: false,
+
        };
+
        let id = entry.id().into();
+
        Ok(entry.kind().and_then(|kind| match kind {
+
            git2::ObjectType::Tree => Some(Self::Directory(Directory { name, id })),
+
            git2::ObjectType::Blob => Some(Self::File(File { name, id })),
+
            _ => None,
+
        }))
+
    }
}

-
/// A `Directory` is a set of entries of sub-directories and files, ordered
-
/// by their unique names in the alphabetical order.
+
/// A `Directory` is the representation of a file system directory, for a given
+
/// [`git` tree][git-tree].
+
///
+
/// The name of a directory can be retrieved via [`File::name`].
+
///
+
/// The [`Entries`] of a directory can be retrieved via
+
/// [`Directory::entries`].
+
///
+
/// [git-tree]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Directory {
    pub(crate) name: Label,
-
    pub(crate) oid: Oid,
+
    pub(crate) id: Oid,
}

impl Directory {
-
    /// Creates a directory given `name` and `oid`, with empty contents.
-
    pub fn new(name: Label, oid: Oid) -> Self {
-
        Directory { name, oid }
-
    }
-

    /// Get the name of the current `Directory`.
    pub fn name(&self) -> &Label {
        &self.name
    }

-
    /// Returns a `DirectoryContent` for the current directory.
-
    pub fn contents(&self, repo: &Repository) -> Result<DirectoryContent, git::Error> {
-
        let listing = repo.directory_get(self)?;
-
        Ok(DirectoryContent { listing })
+
    /// Creates a directory given its `name` and `id`.
+
    ///
+
    /// The `id` must point to a `git` tree.
+
    pub fn new(name: Label, id: Oid) -> Self {
+
        Self { name, id }
+
    }
+

+
    /// Return the [`Entries`] for this `Directory`'s `Oid`.
+
    ///
+
    /// The resulting `Entries` will only resolve to this
+
    /// `Directory`'s entries. Any sub-directories will need to be
+
    /// resolved independently.
+
    ///
+
    /// # Errors
+
    ///
+
    /// This function will fail if it could not find the `git` tree
+
    /// for the `Oid`.
+
    pub fn entries(&self, repo: &Repository) -> Result<Entries, error::Directory> {
+
        let tree = repo.git2_repo().find_tree(self.id.into())?;
+

+
        let mut entries = BTreeMap::new();
+
        let mut error = None;
+

+
        // Walks only the first level of entries.
+
        tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| {
+
            match Entry::from_entry(entry) {
+
                Ok(Some(entry)) => match entry {
+
                    Entry::File(_) => {
+
                        entries.insert(entry.label().clone(), entry);
+
                        git2::TreeWalkResult::Ok
+
                    },
+
                    Entry::Directory(_) => {
+
                        entries.insert(entry.label().clone(), entry);
+
                        // Skip nested directories
+
                        git2::TreeWalkResult::Skip
+
                    },
+
                },
+
                Ok(None) => git2::TreeWalkResult::Skip,
+
                Err(err) => {
+
                    error = Some(err);
+
                    git2::TreeWalkResult::Abort
+
                },
+
            }
+
        })?;
+

+
        match error {
+
            Some(err) => Err(err.into()),
+
            None => Ok(Entries { listing: entries }),
+
        }
    }

-
    /// Retrieves a `DirectoryEntry` for `path` in `repo`.
-
    pub fn get_path(
+
    /// Find the [`Entry`] found at `path`, if it exists.
+
    pub fn find_entry(
        &self,
        path: &path::Path,
        repo: &Repository,
-
    ) -> Result<DirectoryEntry, git::Error> {
+
    ) -> Result<Option<Entry>, crate::git::Error> {
        // Search the path in git2 tree.
-
        let git2_tree = repo.git2_repo().find_tree(self.oid.into())?;
-
        let entry = git2_tree.get_path(path)?;
+
        let git2_tree = repo.git2_repo().find_tree(self.id.into())?;
+
        let entry = git2_tree
+
            .get_path(path)
+
            .map(Some)
+
            .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))?;

-
        // Construct the DirectoryEntry.
-
        let name = entry.name().ok_or_else(|| {
-
            Error::Label(LabelError::InvalidUTF8 {
-
                label: String::from_utf8_lossy(entry.name_bytes()).into(),
-
            })
-
        })?;
-
        let label = Label {
-
            label: name.to_string(),
-
            hidden: false,
-
        };
-
        let oid: Oid = entry.id().into();
-
        match entry.kind() {
-
            Some(git2::ObjectType::Tree) => {
-
                let dir = Directory::new(label, oid);
-
                Ok(DirectoryEntry::Directory(dir))
-
            },
-
            Some(git2::ObjectType::Blob) => {
-
                let f = File { name: label, oid };
-
                Ok(DirectoryEntry::File(f))
-
            },
-
            _ => {
-
                let file_path = Path::try_from(path.to_path_buf())?;
-
                Err(git::Error::PathNotFound(file_path))
-
            },
-
        }
+
        Ok(entry
+
            .and_then(|entry| Entry::from_entry(&entry).transpose())
+
            .transpose()
+
            .unwrap())
    }

-
    /// Find a [`File`] in the directory given the [`Path`] to the [`File`].
-
    ///
-
    /// # Failures
-
    ///
-
    /// This operation fails if the path does not lead to a [`File`].
-
    /// If the search is for a `Directory` then use `find_directory`.
-
    ///
-
    /// # Examples
-
    ///
-
    /// Search for a file in the path:
-
    ///     * `foo/bar/baz.hs`
-
    ///     * `foo`
-
    ///     * `foo/bar/qux.rs`
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Directory, File};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let file = File::new(b"module Banana ...");
-
    ///
-
    /// let mut directory = Directory::root();
-
    /// directory.insert_file(unsound::path::new("foo/bar/baz.rs"), file.clone());
-
    ///
-
    /// // The file is succesfully found
-
    /// assert_eq!(directory.find_file(unsound::path::new("foo/bar/baz.rs")), Some(file));
-
    ///
-
    /// // We shouldn't be able to find a directory
-
    /// assert_eq!(directory.find_file(unsound::path::new("foo")), None);
-
    ///
-
    /// // 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, repo: &Repository) -> Option<Oid> {
+
    /// Find the `Oid`, for a [`File`], found at `path`, if it exists.
+
    pub fn find_file(
+
        &self,
+
        path: Path,
+
        repo: &Repository,
+
    ) -> Result<Option<Oid>, crate::git::Error> {
        let path_buf: std::path::PathBuf = (&path).into();
-
        let entry = match self.get_path(path_buf.as_path(), repo) {
-
            Ok(entry) => entry,
-
            Err(_) => return None,
-
        };
-

-
        match entry {
-
            DirectoryEntry::File(f) => Some(f.oid),
-
            DirectoryEntry::Directory(_) => None,
-
        }
+
        Ok(match self.find_entry(path_buf.as_path(), repo)? {
+
            Some(Entry::File(f)) => Some(f.id),
+
            _ => None,
+
        })
    }

-
    /// Find a `Directory` in the directory given the [`Path`] to the
-
    /// `Directory`.
-
    ///
-
    /// # Failures
-
    ///
-
    /// This operation fails if the path does not lead to the `Directory`.
-
    ///
-
    /// # Examples
-
    ///
-
    /// Search for directories in the path:
-
    ///     * `foo`
-
    ///     * `foo/bar`
-
    ///     * `foo/baz`
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Directory, File};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let file = File::new(b"module Banana ...");
-
    ///
-
    /// let mut directory = Directory::root();
-
    /// directory.insert_file(unsound::path::new("foo/bar/baz.rs"), file.clone());
-
    ///
-
    /// // Can find the first level
-
    /// assert!(directory.find_directory(unsound::path::new("foo")).is_some());
-
    ///
-
    /// // Can find the second level
-
    /// assert!(directory.find_directory(unsound::path::new("foo/bar")).is_some());
-
    ///
-
    /// // Cannot find 'baz' since it does not exist
-
    /// assert!(directory.find_directory(unsound::path::new("foo/baz")).is_none());
-
    ///
-
    /// // '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, repo: &Repository) -> Option<Self> {
+
    /// Find the `Directory` found at `path`, if it exists.
+
    pub fn find_directory(
+
        &self,
+
        path: Path,
+
        repo: &Repository,
+
    ) -> Result<Option<Self>, crate::git::Error> {
        let path_buf: std::path::PathBuf = (&path).into();
-
        let entry = match self.get_path(path_buf.as_path(), repo) {
-
            Ok(entry) => entry,
-
            Err(_) => return None,
-
        };
-

-
        match entry {
-
            DirectoryEntry::File(_) => None,
-
            DirectoryEntry::Directory(d) => Some(d),
-
        }
+
        Ok(match self.find_entry(path_buf.as_path(), repo)? {
+
            Some(Entry::Directory(d)) => Some(d),
+
            _ => None,
+
        })
    }

    // TODO(fintan): This is going to be a bit trickier so going to leave it out for
@@ -274,21 +319,43 @@ impl Directory {

    /// Get the total size, in bytes, of a `Directory`. The size is
    /// the sum of all files that can be reached from this `Directory`.
-
    pub fn size(&self, repo: &Repository) -> Result<usize, git::Error> {
-
        let mut size = 0;
-
        let contents = self.contents(repo)?;
-
        for item in contents.iter() {
-
            match item {
-
                DirectoryEntry::File(f) => {
-
                    let sz = repo.file_size(f.oid)?;
-
                    size += sz;
-
                },
-
                DirectoryEntry::Directory(d) => {
-
                    size += d.size(repo)?;
+
    pub fn size(&self, repo: &Repository) -> Result<usize, error::Directory> {
+
        self.traverse(repo, 0, &mut |size, entry| match entry {
+
            Entry::File(file) => Ok(size + file.content(repo)?.size()),
+
            Entry::Directory(dir) => Ok(size + dir.size(repo)?),
+
        })
+
    }
+

+
    /// Traverse the entire `Directory` using the `initial`
+
    /// accumulator and the function `f`.
+
    ///
+
    /// For each [`Entry::Directory`] this will recursively call
+
    /// [`Directory::traverse`] and obtain its [`Entries`].
+
    ///
+
    /// `Error` is the error type of the fallible function.
+
    /// `B` is the type of the accumulator.
+
    /// `F` is the fallible function that takes the accumulator and
+
    /// the next [`Entry`], possibly providing the next accumulator
+
    /// value.
+
    pub fn traverse<Error, B, F>(
+
        &self,
+
        repo: &Repository,
+
        initial: B,
+
        f: &mut F,
+
    ) -> Result<B, Error>
+
    where
+
        Error: From<error::Directory>,
+
        F: FnMut(B, &Entry) -> Result<B, Error>,
+
    {
+
        self.entries(repo)?
+
            .entries()
+
            .try_fold(initial, |acc, entry| match entry {
+
                Entry::File(_) => f(acc, entry),
+
                Entry::Directory(directory) => {
+
                    let acc = directory.traverse(repo, acc, f)?;
+
                    f(acc, entry)
                },
-
            }
-
        }
-
        Ok(size)
+
            })
    }
}

@@ -296,6 +363,6 @@ impl Revision for Directory {
    type Error = Infallible;

    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
-
        Ok(self.oid)
+
        Ok(self.id)
    }
}
modified radicle-surf/src/git/repo.rs
@@ -15,14 +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/>.

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

-
use directory::{Directory, FileContent};
use git_ref_format::{refspec::QualifiedPattern, Qualified};
use radicle_git_ext::Oid;
use thiserror::Error;
@@ -30,7 +24,7 @@ use thiserror::Error;
use crate::{
    diff::{self, *},
    file_system,
-
    file_system::{directory, DirectoryEntry, Label},
+
    file_system::{directory::FileContent, Directory, Label},
    git::{
        commit,
        glob,
@@ -195,60 +189,10 @@ impl Repository {
        let tree = git2_commit.as_object().peel_to_tree()?;
        Ok(Directory {
            name: Label::root(),
-
            oid: tree.id().into(),
+
            id: tree.id().into(),
        })
    }

-
    /// Retrieves the content of a directory.
-
    pub(crate) fn directory_get(
-
        &self,
-
        d: &Directory,
-
    ) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
-
        let git2_tree = self.inner.find_tree(d.oid.into())?;
-
        let map = self.tree_first_level(git2_tree)?;
-
        Ok(map)
-
    }
-

-
    /// Returns a map of the first level entries in `tree`.
-
    fn tree_first_level(&self, tree: git2::Tree) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
-
        let mut map = BTreeMap::new();
-

-
        // Walks only the first level of entries.
-
        tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| {
-
            let oid = entry.id().into();
-
            let label = match entry.name() {
-
                Some(name) => match name.parse::<Label>() {
-
                    Ok(label) => label,
-
                    Err(_) => return git2::TreeWalkResult::Abort,
-
                },
-
                None => return git2::TreeWalkResult::Abort,
-
            };
-

-
            match entry.kind() {
-
                Some(git2::ObjectType::Tree) => {
-
                    let dir = Directory::new(label.clone(), oid);
-
                    map.insert(label, DirectoryEntry::Directory(dir));
-
                    return git2::TreeWalkResult::Skip; // Not go into nested
-
                                                       // directories.
-
                },
-
                Some(git2::ObjectType::Blob) => {
-
                    let f = directory::File {
-
                        name: label.clone(),
-
                        oid,
-
                    };
-
                    map.insert(label, DirectoryEntry::File(f));
-
                },
-
                _ => {
-
                    return git2::TreeWalkResult::Skip;
-
                },
-
            }
-

-
            git2::TreeWalkResult::Ok
-
        })?;
-

-
        Ok(map)
-
    }
-

    /// Returns the last commit, if exists, for a `path` in the history of
    /// `rev`.
    pub fn last_commit<C: ToCommit>(
@@ -294,12 +238,6 @@ impl Repository {
        Ok(FileContent::new(blob))
    }

-
    /// Return the size of a file
-
    pub(crate) fn file_size(&self, oid: Oid) -> Result<usize, Error> {
-
        let blob = self.inner.find_blob(oid.into())?;
-
        Ok(blob.size())
-
    }
-

    /// Retrieves the file with `path` in this commit.
    pub fn get_commit_file<R: Revision>(
        &self,
modified radicle-surf/src/object.rs
@@ -30,7 +30,11 @@ pub use blob::{Blob, BlobContent};
pub mod tree;
pub use tree::{tree, Tree, TreeEntry};

-
use crate::{commit, file_system, git};
+
use crate::{
+
    commit,
+
    file_system::{self, directory},
+
    git,
+
};

/// Git object types.
///
@@ -84,6 +88,9 @@ impl Serialize for Info {
/// An error reported by object types.
#[derive(Debug, thiserror::Error)]
pub enum Error {
+
    #[error(transparent)]
+
    Directory(#[from] directory::error::Directory),
+

    /// An error occurred during a file system operation.
    #[error(transparent)]
    FileSystem(#[from] file_system::Error),
modified radicle-surf/src/object/blob.rs
@@ -136,7 +136,7 @@ where
    let p = file_system::Path::from_str(path)?;

    let file = root
-
        .find_file(p.clone(), repo)
+
        .find_file(p.clone(), repo)?
        .ok_or_else(|| Error::PathNotFound(p.clone()))?;

    let mut commit_path = file_system::Path::root();
modified radicle-surf/src/object/tree.rs
@@ -29,7 +29,7 @@ use serde::{

use crate::{
    commit,
-
    file_system::{self, DirectoryEntry},
+
    file_system::{self, directory},
    git::Repository,
    object::{Error, Info, ObjectType},
    revision::Revision,
@@ -111,41 +111,40 @@ pub fn tree(
        root_dir
    } else {
        root_dir
-
            .find_directory(path.clone(), repo)
+
            .find_directory(path.clone(), repo)?
            .ok_or_else(|| Error::PathNotFound(path.clone()))?
    };

-
    let entries_results: Result<Vec<TreeEntry>, Error> = prefix_dir
-
        .contents(repo)?
-
        .iter()
-
        .map(|entry| {
-
            let entry_path = if path.is_root() {
-
                file_system::Path::new(entry.label().clone())
-
            } else {
-
                let mut p = path.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: entry.label().to_string(),
-
                object_type: match entry {
-
                    DirectoryEntry::Directory(_) => ObjectType::Tree,
-
                    DirectoryEntry::File { .. } => ObjectType::Blob,
-
                },
-
                last_commit: None,
-
            };
-

-
            Ok(TreeEntry {
-
                info,
-
                path: entry_path.to_string(),
-
            })
-
        })
-
        .collect();
-

-
    let mut entries = entries_results?;
+
    let mut entries =
+
        prefix_dir
+
            .entries(repo)?
+
            .iter()
+
            .fold(Vec::new(), |mut entries, (label, entry)| {
+
                let entry_path = if path.is_root() {
+
                    file_system::Path::new(label.clone())
+
                } else {
+
                    let mut p = path.clone();
+
                    p.push(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 entry {
+
                        directory::Entry::Directory(_) => ObjectType::Tree,
+
                        directory::Entry::File { .. } => ObjectType::Blob,
+
                    },
+
                    last_commit: None,
+
                };
+

+
                entries.push(TreeEntry {
+
                    info,
+
                    path: entry_path.to_string(),
+
                });
+
                entries
+
            });

    // We want to ensure that in the response Tree entries come first. `Ord` being
    // derived on the enum ensures Variant declaration order.
modified radicle-surf/t/src/file_system.rs
@@ -32,7 +32,7 @@ mod path {
mod directory {
    use git_ref_format::refname;
    use radicle_surf::{
-
        file_system::DirectoryEntry,
+
        file_system::directory,
        git::{Branch, Repository},
    };
    use std::path::Path;
@@ -40,38 +40,38 @@ mod directory {
    const GIT_PLATINUM: &str = "../data/git-platinum";

    #[test]
-
    fn directory_get_path() {
+
    fn directory_find_entry() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let root = repo.root_dir(&Branch::local(refname!("master"))).unwrap();

-
        // get_path for a file.
+
        // find_entry for a file.
        let path = Path::new("src/memory.rs");
-
        let entry = root.get_path(path, &repo).unwrap();
-
        assert!(matches!(entry, DirectoryEntry::File(_)));
+
        let entry = root.find_entry(path, &repo).unwrap();
+
        assert!(matches!(entry, Some(directory::Entry::File(_))));

-
        // get_path for a directory.
+
        // find_entry for a directory.
        let path = Path::new("this/is/a/really/deeply/nested/directory/tree");
-
        let entry = root.get_path(path, &repo).unwrap();
-
        assert!(matches!(entry, DirectoryEntry::Directory(_)));
+
        let entry = root.find_entry(path, &repo).unwrap();
+
        assert!(matches!(entry, Some(directory::Entry::Directory(_))));

-
        // get_path for a non-leaf directory and its relative path.
+
        // find_entry for a non-leaf directory and its relative path.
        let path = Path::new("text");
-
        let entry = root.get_path(path, &repo).unwrap();
-
        assert!(matches!(entry, DirectoryEntry::Directory(_)));
-
        if let DirectoryEntry::Directory(sub_dir) = entry {
+
        let entry = root.find_entry(path, &repo).unwrap();
+
        assert!(matches!(entry, Some(directory::Entry::Directory(_))));
+
        if let Some(directory::Entry::Directory(sub_dir)) = entry {
            let inner_path = Path::new("garden.txt");
-
            let inner_entry = sub_dir.get_path(inner_path, &repo).unwrap();
-
            assert!(matches!(inner_entry, DirectoryEntry::File(_)));
+
            let inner_entry = sub_dir.find_entry(inner_path, &repo).unwrap();
+
            assert!(matches!(inner_entry, Some(directory::Entry::File(_))));
        }

-
        // get_path for non-existing file
+
        // find_entry for non-existing file
        let path = Path::new("this/is/a/really/missing_file");
-
        let result = root.get_path(path, &repo);
-
        assert!(result.is_err());
+
        let result = root.find_entry(path, &repo).unwrap();
+
        assert_eq!(result, None);

-
        // get_path for absolute path: fail.
+
        // find_entry for absolute path: fail.
        let path = Path::new("/src/memory.rs");
-
        let result = root.get_path(path, &repo);
+
        let result = root.find_entry(path, &repo);
        assert!(result.is_err());
    }

@@ -89,9 +89,9 @@ mod directory {
         */

        let path = Path::new("src");
-
        let entry = root.get_path(path, &repo).unwrap();
-
        assert!(matches!(entry, DirectoryEntry::Directory(_)));
-
        if let DirectoryEntry::Directory(d) = entry {
+
        let entry = root.find_entry(path, &repo).unwrap();
+
        assert!(matches!(entry, Some(directory::Entry::Directory(_))));
+
        if let Some(directory::Entry::Directory(d)) = entry {
            assert_eq!(16297, d.size(&repo).unwrap());
        }
    }
modified radicle-surf/t/src/git.rs
@@ -7,7 +7,7 @@
use radicle_surf::git::{Author, Commit};
use radicle_surf::{
    diff::*,
-
    file_system::{unsound, DirectoryEntry, Path},
+
    file_system::{directory, unsound, Path},
    git::{Branch, Error, Glob, Oid, Repository},
};

@@ -697,7 +697,7 @@ mod reference {
            .unwrap();
        assert_eq!(tags.len(), 6);
        let root_dir = repo.root_dir(&tags[0]).unwrap();
-
        assert_eq!(root_dir.contents(&repo).unwrap().iter().count(), 1);
+
        assert_eq!(root_dir.entries(&repo).unwrap().entries().count(), 1);
    }

    #[test]
@@ -727,7 +727,7 @@ mod code_browsing {
        let repo = Repository::open(GIT_PLATINUM).unwrap();

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

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

@@ -735,16 +735,20 @@ mod code_browsing {
        /// For sub-directories, will do Depth-First-Search and print
        /// recursively.
        /// Returns the number of items visited (i.e. printed)
-
        fn println_dir(dir: &Directory, repo: &Repository, indent_level: usize) -> i32 {
-
            let mut count = 0;
-
            for item in dir.contents(repo).unwrap().iter() {
-
                println!("> {}{}", " ".repeat(indent_level * 4), &item.label());
-
                count += 1;
-
                if let DirectoryEntry::Directory(sub_dir) = item {
-
                    count += println_dir(sub_dir, repo, indent_level + 1);
-
                }
-
            }
-
            count
+
        fn println_dir(dir: &Directory, repo: &Repository) -> i32 {
+
            dir.traverse::<directory::error::Directory, _, _>(
+
                repo,
+
                (0, 0),
+
                &mut |(count, indent_level), entry| {
+
                    println!("> {}{}", " ".repeat(indent_level * 4), entry.label());
+
                    match entry {
+
                        directory::Entry::File(_) => Ok((count + 1, indent_level)),
+
                        directory::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
+
                    }
+
                },
+
            )
+
            .unwrap()
+
            .0
        }
    }

@@ -752,20 +756,18 @@ mod code_browsing {
    fn browse_repo_lazily() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let root_dir = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
-
        let count = root_dir.contents(&repo).unwrap().iter().count();
+
        let count = root_dir.entries(&repo).unwrap().entries().count();
        assert_eq!(count, 8);
        let count = traverse(&root_dir, &repo);
        assert_eq!(count, 36);

        fn traverse(dir: &Directory, repo: &Repository) -> i32 {
-
            let mut count = 0;
-
            for item in dir.contents(repo).unwrap().iter() {
-
                count += 1;
-
                if let DirectoryEntry::Directory(sub_dir) = item {
-
                    count += traverse(sub_dir, repo)
-
                }
-
            }
-
            count
+
            dir.traverse::<directory::error::Directory, _, _>(
+
                repo,
+
                0,
+
                &mut |count, _| Ok(count + 1),
+
            )
+
            .unwrap()
        }
    }