Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'han/design-update'
Fintan Halpenny committed 3 years ago
commit 46257a8b1423847fb3ef13a073f3047daaa85db4
parent 84d598b
10 files changed +416 -845
added radicle-surf/examples/browsing.rs
@@ -0,0 +1,66 @@
+
// 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/>.
+

+
//! An example of browsing a git repo using `radicle-surf`.
+
//!
+
//! How to run:
+
//!
+
//!     cargo run --example browsing <git_repo_path>
+
//!
+
//! This program browses the given repo and prints out the files and
+
//! the directories in a tree-like structure.
+

+
use radicle_surf::{
+
    file_system::{Directory, DirectoryEntry},
+
    git::{Repository, RepositoryRef},
+
};
+
use std::{env, time::Instant};
+

+
fn main() {
+
    let repo_path = match env::args().nth(1) {
+
        Some(path) => path,
+
        None => {
+
            print_usage();
+
            return;
+
        },
+
    };
+
    let repo = Repository::discover(&repo_path).unwrap();
+
    let repo = repo.as_ref();
+
    let now = Instant::now();
+
    let head = repo.head_oid().unwrap();
+
    let root = repo.root_dir(head).unwrap();
+
    print_directory(&root, &repo, 0);
+

+
    let elapsed_millis = now.elapsed().as_millis();
+
    println!("browse with print: {} ms", elapsed_millis);
+
}
+

+
fn print_directory(d: &Directory, repo: &RepositoryRef, indent_level: usize) {
+
    let indent = " ".repeat(indent_level * 4);
+
    println!("{}{}/", &indent, d.name());
+
    for entry in d.contents(repo).unwrap().iter() {
+
        match entry {
+
            DirectoryEntry::File(f) => println!("    {}{}", &indent, f.name()),
+
            DirectoryEntry::Directory(d) => print_directory(d, repo, indent_level + 1),
+
        }
+
    }
+
}
+

+
fn print_usage() {
+
    println!("Usage:");
+
    println!("cargo run --example browsing <repo_path>");
+
}
modified radicle-surf/src/diff.rs
@@ -17,12 +17,15 @@

#![allow(dead_code, unused_variables, missing_docs)]

-
use std::{cell::RefCell, cmp::Ordering, convert::TryFrom, ops::Deref, rc::Rc, slice};
+
use std::{convert::TryFrom, slice};

#[cfg(feature = "serialize")]
use serde::{ser, Serialize, Serializer};

-
use crate::file_system::{Directory, DirectoryContents, Path};
+
use crate::{
+
    file_system::{Directory, Path},
+
    vcs::git::{Error, RepositoryRef},
+
};

pub mod git;

@@ -293,14 +296,7 @@ impl Diff {
    // TODO: Direction of comparison is not obvious with this signature.
    // For now using conventional approach with the right being "newer".
    #[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().clone(),
-
            &[],
-
        )));
-
        Diff::collect_diff(&left, &right, &path, &mut diff);
-

+
    pub fn diff(left: Directory, right: Directory, repo: RepositoryRef) -> Result<Self, Error> {
        // TODO: Some of the deleted files may actually be moved (renamed) to one of the
        // created files. Finding out which of the deleted files were deleted
        // and which were moved will probably require performing some variant of
@@ -308,180 +304,7 @@ impl Diff {
        // decision can be based on heuristics, e.g. the file can be considered
        // moved, if len(LCS) > 0,25 * min(size(d), size(c)), and
        // deleted otherwise.
-

-
        diff
-
    }
-

-
    fn collect_diff(
-
        old: &Directory,
-
        new: &Directory,
-
        parent_path: &Rc<RefCell<Path>>,
-
        diff: &mut Diff,
-
    ) {
-
        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()) {
-
                        Ordering::Greater => {
-
                            diff.add_deleted_files(old_entry, parent_path);
-
                            old_entry_opt = old_iter.next();
-
                        },
-
                        Ordering::Less => {
-
                            diff.add_created_files(new_entry, parent_path);
-
                            new_entry_opt = new_iter.next();
-
                        },
-
                        Ordering::Equal => match (new_entry, old_entry) {
-
                            (
-
                                DirectoryContents::File {
-
                                    name: new_file_name,
-
                                    file: new_file,
-
                                },
-
                                DirectoryContents::File {
-
                                    name: old_file_name,
-
                                    file: old_file,
-
                                },
-
                            ) => {
-
                                if old_file.size != new_file.size
-
                                    || old_file.checksum() != new_file.checksum()
-
                                {
-
                                    let mut path = parent_path.borrow().clone();
-
                                    path.push(new_file_name.clone());
-

-
                                    diff.add_modified_file(path, vec![], None);
-
                                }
-
                                old_entry_opt = old_iter.next();
-
                                new_entry_opt = new_iter.next();
-
                            },
-
                            (
-
                                DirectoryContents::File {
-
                                    name: new_file_name,
-
                                    file: new_file,
-
                                },
-
                                DirectoryContents::Directory(old_dir),
-
                            ) => {
-
                                let mut path = parent_path.borrow().clone();
-
                                path.push(new_file_name.clone());
-

-
                                diff.add_created_file(
-
                                    path,
-
                                    FileDiff::Plain {
-
                                        hunks: Hunks::default(),
-
                                    },
-
                                );
-
                                diff.add_deleted_files(old_entry, parent_path);
-

-
                                old_entry_opt = old_iter.next();
-
                                new_entry_opt = new_iter.next();
-
                            },
-
                            (
-
                                DirectoryContents::Directory(new_dir),
-
                                DirectoryContents::File {
-
                                    name: old_file_name,
-
                                    file: old_file,
-
                                },
-
                            ) => {
-
                                let mut path = parent_path.borrow().clone();
-
                                path.push(old_file_name.clone());
-

-
                                diff.add_created_files(new_entry, parent_path);
-
                                diff.add_deleted_file(
-
                                    path,
-
                                    FileDiff::Plain {
-
                                        hunks: Hunks::default(),
-
                                    },
-
                                );
-

-
                                old_entry_opt = old_iter.next();
-
                                new_entry_opt = new_iter.next();
-
                            },
-
                            (
-
                                DirectoryContents::Directory(new_dir),
-
                                DirectoryContents::Directory(old_dir),
-
                            ) => {
-
                                parent_path.borrow_mut().push(new_dir.current().clone());
-
                                Diff::collect_diff(
-
                                    old_dir.deref(),
-
                                    new_dir.deref(),
-
                                    parent_path,
-
                                    diff,
-
                                );
-
                                parent_path.borrow_mut().pop();
-
                                old_entry_opt = old_iter.next();
-
                                new_entry_opt = new_iter.next();
-
                            },
-
                        },
-
                    }
-
                },
-
                (Some(old_entry), None) => {
-
                    diff.add_deleted_files(old_entry, parent_path);
-
                    old_entry_opt = old_iter.next();
-
                },
-
                (None, Some(new_entry)) => {
-
                    diff.add_created_files(new_entry, parent_path);
-
                    new_entry_opt = new_iter.next();
-
                },
-
                (None, None) => break,
-
            }
-
        }
-
    }
-

-
    // if entry is a file, then return this file,
-
    // or a list of files in the directory tree otherwise
-
    fn collect_files_from_entry<F, T>(
-
        entry: &DirectoryContents,
-
        parent_path: &Rc<RefCell<Path>>,
-
        mapper: F,
-
    ) -> Vec<T>
-
    where
-
        F: Fn(Path) -> T + Copy,
-
    {
-
        match entry {
-
            DirectoryContents::Directory(dir) => Diff::collect_files(dir, parent_path, mapper),
-
            DirectoryContents::File { name, .. } => {
-
                let mut path = parent_path.borrow().clone();
-
                path.push(name.clone());
-

-
                vec![mapper(path)]
-
            },
-
        }
-
    }
-

-
    fn collect_files<F, T>(dir: &Directory, parent_path: &Rc<RefCell<Path>>, mapper: F) -> Vec<T>
-
    where
-
        F: Fn(Path) -> T + Copy,
-
    {
-
        let mut files: Vec<T> = Vec::new();
-
        Diff::collect_files_inner(dir, parent_path, mapper, &mut files);
-
        files
-
    }
-

-
    fn collect_files_inner<'a, F, T>(
-
        dir: &'a Directory,
-
        parent_path: &Rc<RefCell<Path>>,
-
        mapper: F,
-
        files: &mut Vec<T>,
-
    ) where
-
        F: Fn(Path) -> T + Copy,
-
    {
-
        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);
-
                },
-
                DirectoryContents::File { name, .. } => {
-
                    let mut path = parent_path.borrow().clone();
-
                    path.push(name.clone());
-
                    files.push(mapper(path));
-
                },
-
            }
-
        }
-
        parent_path.borrow_mut().pop();
+
        repo.diff(left, right)
    }

    pub(crate) fn add_modified_file(
@@ -522,32 +345,10 @@ impl Diff {
        self.created.push(CreateFile { path, diff });
    }

-
    fn add_created_files(&mut self, dc: &DirectoryContents, parent_path: &Rc<RefCell<Path>>) {
-
        let mut new_files: Vec<CreateFile> =
-
            Diff::collect_files_from_entry(dc, parent_path, |path| CreateFile {
-
                path,
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
            });
-
        self.created.append(&mut new_files);
-
    }
-

    pub(crate) fn add_deleted_file(&mut self, path: Path, diff: FileDiff) {
        self.deleted.push(DeleteFile { path, diff });
    }

-
    fn add_deleted_files(&mut self, dc: &DirectoryContents, parent_path: &Rc<RefCell<Path>>) {
-
        let mut new_files: Vec<DeleteFile> =
-
            Diff::collect_files_from_entry(dc, parent_path, |path| DeleteFile {
-
                path,
-
                diff: FileDiff::Plain {
-
                    hunks: Hunks::default(),
-
                },
-
            });
-
        self.deleted.append(&mut new_files);
-
    }
-

    pub fn stats(&self) -> Stats {
        let mut deletions = 0;
        let mut insertions = 0;
modified radicle-surf/src/file_system/directory.rs
@@ -19,139 +19,105 @@
//!
//! A `Directory` is expected to be a non-empty tree of directories and files.
//! See [`Directory`] for more information.
-
//!
-
//! As well as this, this module contains [`DirectoryContents`] which is the
-
//! output of iterating over a [`Directory`].

-
use crate::file_system::path::*;
-
use nonempty::NonEmpty;
-
use std::{
-
    collections::{hash_map::DefaultHasher, BTreeMap},
-
    hash::{Hash, Hasher},
+
use crate::{
+
    file_system::{error::LabelError, path::*, Error},
+
    vcs::git::{self, RepositoryRef, Revision},
};
+
use git2::Blob;
+
use radicle_git_ext::Oid;
+
use std::{collections::BTreeMap, convert::TryFrom, path};

-
/// 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
-
/// its [`size`](#method.size).
-
#[derive(Clone, PartialEq, Eq)]
+
/// Represents a `file` in a git repo.
+
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct File {
-
    /// The contents of a `File` as a vector of bytes.
-
    pub contents: Vec<u8>,
-
    pub(crate) size: usize,
-
}
-

-
impl std::fmt::Debug for File {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        let mut contents = self.contents.clone();
-
        contents.truncate(10);
-
        write!(
-
            f,
-
            "File {{ contents: {:?}, size: {} }}",
-
            contents, self.size
-
        )
-
    }
+
    pub(crate) name: Label,
+
    pub(crate) oid: Oid,
}

impl File {
-
    /// Create a new `File` with the contents provided.
-
    pub fn new(contents: &[u8]) -> Self {
-
        let size = contents.len();
+
    /// Create a new `File`.
+
    pub fn new(name: String, oid: Oid) -> Self {
        File {
-
            contents: contents.to_vec(),
-
            size,
+
            name: Label {
+
                label: name,
+
                hidden: false,
+
            },
+
            oid,
        }
    }

-
    /// Get the size of the `File` corresponding to the number of bytes in the
-
    /// file contents.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::File;
-
    ///
-
    /// let file = File::new(b"pub mod diff;\npub mod file_system;\npub mod vcs;\npub use crate::vcs::git;\n");
-
    ///
-
    /// assert_eq!(file.size(), 73);
-
    /// ```
+
    /// Returns the file name reference.
+
    pub fn name(&self) -> &str {
+
        self.name.as_str()
+
    }
+
}
+

+
/// Represents the actual content of a file.
+
pub struct FileContent<'a> {
+
    blob: Blob<'a>,
+
}
+

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

+
    /// Returns the size of file
    pub fn size(&self) -> usize {
-
        self.size
+
        self.blob.size()
    }

-
    /// Get the hash of the `File` corresponding to the contents of the file.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::File;
-
    ///
-
    /// let file = File::new(
-
    ///     b"pub mod diff;\npub mod file_system;\npub mod vcs;\npub use crate::vcs::git;\n",
-
    /// );
-
    ///
-
    /// assert_eq!(file.checksum(), 8457766712413557403);
-
    /// ```
-
    pub fn checksum(&self) -> u64 {
-
        let mut hasher = DefaultHasher::new();
-
        self.contents.hash(&mut hasher);
-
        hasher.finish()
+
    /// Creates a `FileContent` using a blob.
+
    pub(crate) fn new(blob: Blob<'a>) -> Self {
+
        Self { blob }
    }
}

-
/// 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 {
-
    name: Label,
-
    contents: BTreeMap<Label, DirectoryContents>,
+
/// Represents the listing of a directory.
+
pub struct DirectoryContent {
+
    listing: BTreeMap<Label, DirectoryEntry>,
}

-
/// `DirectoryContents` is an enumeration of what a [`Directory`] can contain
-
/// and is used for when we are iterating through a [`Directory`].
+
impl DirectoryContent {
+
    /// Returns an iterator for the listing of a directory.
+
    pub fn iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
+
        self.listing.values()
+
    }
+
}
+

+
/// Represents an entry in a directory.
#[derive(Debug, Clone, PartialEq, Eq)]
-
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.
+
pub enum DirectoryEntry {
+
    /// When the entry is a file.
+
    File(File),
+
    /// When the entry is a directory.
    Directory(Directory),
}

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

-
impl Directory {
-
    /// Create a root directory.
-
    ///
-
    /// This function is usually used for testing and demonstation purposes.
-
    pub fn root() -> Self {
-
        Directory {
-
            name: Label::root(),
-
            contents: BTreeMap::new(),
-
        }
-
    }
+
/// 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 {
+
    pub(crate) name: Label,
+
    pub(crate) oid: Oid,
+
}

-
    /// Create a directory, similar to `root`, except with a given name.
-
    ///
-
    /// This function is usually used for testing and demonstation purposes.
-
    pub fn new(name: Label) -> Self {
-
        Directory {
-
            name,
-
            contents: BTreeMap::new(),
-
        }
+
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`.
@@ -159,19 +125,47 @@ impl Directory {
        &self.name
    }

-
    /// 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);
+
    /// Returns a `DirectoryContent` for the current directory.
+
    pub fn contents(&self, repo: &RepositoryRef) -> Result<DirectoryContent, git::Error> {
+
        let listing = repo.directory_get(self)?;
+
        Ok(DirectoryContent { listing })
    }

-
    /// 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()
+
    /// Retrieves a `DirectoryEntry` for `path` in `repo`.
+
    pub fn get_path(
+
        &self,
+
        path: &path::Path,
+
        repo: &RepositoryRef,
+
    ) -> Result<DirectoryEntry, git::Error> {
+
        // Search the path in git2 tree.
+
        let git2_tree = repo.repo_ref.find_tree(self.oid.into())?;
+
        let entry = git2_tree.get_path(path)?;
+

+
        // 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))
+
            },
+
        }
    }

    /// Find a [`File`] in the directory given the [`Path`] to the [`File`].
@@ -206,23 +200,17 @@ 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> {
-
        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.
-
            }
+
    pub fn find_file(&self, path: Path, repo: &RepositoryRef) -> Option<Oid> {
+
        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,
        }
-
        None
    }

    /// Find a `Directory` in the directory given the [`Path`] to the
@@ -260,46 +248,17 @@ 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> {
-
        let mut found = None;
-
        let mut contents = &self.contents;
+
    pub fn find_directory(&self, path: Path, repo: &RepositoryRef) -> Option<Self> {
+
        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,
+
        };

-
        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.
-
            }
+
        match entry {
+
            DirectoryEntry::File(_) => None,
+
            DirectoryEntry::Directory(d) => Some(d),
        }
-

-
        found
-
    }
-

-
    /// Get the [`Label`] of the current directory.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Directory, File, Label};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let mut root = Directory::root();
-
    /// root.insert_file(unsound::path::new("main.rs"), File::new(b"println!(\"Hello, world!\")"));
-
    /// root.insert_file(unsound::path::new("lib.rs"), File::new(b"struct Hello(String)"));
-
    /// root.insert_file(unsound::path::new("test/mod.rs"), File::new(b"assert_eq!(1 + 1, 2);"));
-
    ///
-
    /// assert_eq!(root.current(), Label::root());
-
    ///
-
    /// let test = root.find_directory(
-
    ///     unsound::path::new("test")
-
    /// ).expect("Missing test directory");
-
    /// assert_eq!(test.current(), unsound::label::new("test"));
-
    /// ```
-
    pub fn current(&self) -> &Label {
-
        &self.name
    }

    // TODO(fintan): This is going to be a bit trickier so going to leave it out for
@@ -311,94 +270,26 @@ 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`.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Directory, File};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let mut root = Directory::root();
-
    /// root.insert_file(unsound::path::new("main.rs"), File::new(b"println!(\"Hello, world!\")"));
-
    /// root.insert_file(unsound::path::new("lib.rs"), File::new(b"struct Hello(String)"));
-
    /// root.insert_file(unsound::path::new("test/mod.rs"), File::new(b"assert_eq!(1 + 1, 2);"));
-
    ///
-
    /// assert_eq!(root.size(), 66);
-
    /// ```
-
    pub fn size(&self) -> usize {
-
        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
-
    /// inclusive) and the `File` itself.
-
    ///
-
    /// This function is usually used for testing and demonstation purposes.
-
    pub fn insert_file(&mut self, path: Path, file: 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,
-
                }
+
    pub fn size(&self, repo: &RepositoryRef) -> 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)?;
+
                },
            }
        }
+
        Ok(size)
    }
+
}

-
    /// Insert files into a shared directory path.
-
    ///
-
    /// `directory_path` is used as the prefix to where the files should go. If
-
    /// empty the files will be placed in the current `Directory`.
-
    ///
-
    /// `files` are pairs of file name and the [`File`] itself.
-
    ///
-
    /// This function is usually used for testing and demonstation purposes.
-
    pub fn insert_files(&mut self, directory_path: &[Label], files: NonEmpty<(Label, File)>) {
-
        match NonEmpty::from_slice(directory_path) {
-
            None => {
-
                for (file_name, file) in files.into_iter() {
-
                    self.insert_file(Path::new(file_name), file)
-
                }
-
            },
-
            Some(path) => {
-
                for (file_name, file) in files.into_iter() {
-
                    // The clone is necessary here because we use it as a prefix.
-
                    let mut file_path = Path(path.clone());
-
                    file_path.push(file_name);
-

-
                    self.insert_file(file_path, file)
-
                }
-
            },
-
        }
+
impl Revision for Directory {
+
    fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, git::Error> {
+
        Ok(self.oid)
    }
}
modified radicle-surf/src/object/blob.rs
@@ -136,11 +136,11 @@ where
    C: FnOnce(&[u8]) -> BlobContent,
{
    let revision = maybe_revision.unwrap();
-
    let root = repo.snapshot(&revision)?;
+
    let root = repo.root_dir(&revision)?;
    let p = file_system::Path::from_str(path)?;

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

    let mut commit_path = file_system::Path::root();
@@ -151,7 +151,8 @@ where
        .map(|c| commit::Header::from(&c));
    let (_rest, last) = p.split_last();

-
    let content = content(&file.contents);
+
    let file_content = repo.file_content(file)?;
+
    let content = content(file_content.as_bytes());

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

use crate::{
    commit,
-
    file_system::{self, DirectoryContents},
+
    file_system::{self, DirectoryEntry},
    git::RepositoryRef,
    object::{Error, Info, ObjectType},
    revision::Revision,
@@ -108,17 +108,18 @@ where
        file_system::Path::from_str(&prefix)?
    };

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

    let entries_results: Result<Vec<TreeEntry>, Error> = prefix_dir
-
        .contents()
+
        .contents(repo)?
+
        .iter()
        .map(|entry| {
            let entry_path = if path.is_root() {
                file_system::Path::new(entry.label().clone())
@@ -133,8 +134,8 @@ where
            let info = Info {
                name: entry.label().to_string(),
                object_type: match entry {
-
                    DirectoryContents::Directory(_) => ObjectType::Tree,
-
                    DirectoryContents::File { .. } => ObjectType::Blob,
+
                    DirectoryEntry::Directory(_) => ObjectType::Tree,
+
                    DirectoryEntry::File { .. } => ObjectType::Blob,
                },
                last_commit: None,
            };
modified radicle-surf/src/vcs/git/commit.rs
@@ -121,11 +121,11 @@ impl Commit {
    }

    /// Retrieves the file with `path` in this commit.
-
    pub fn get_file(
-
        &self,
-
        repo: &RepositoryRef,
+
    pub fn get_file<'a>(
+
        &'a self,
+
        repo: &'a RepositoryRef,
        path: file_system::Path,
-
    ) -> Result<directory::File, Error> {
+
    ) -> Result<directory::FileContent, Error> {
        let git2_commit = repo.get_git2_commit(self.id)?;
        repo.get_commit_file(&git2_commit, path)
    }
modified radicle-surf/src/vcs/git/repo.rs
@@ -18,7 +18,7 @@
use crate::{
    diff::*,
    file_system,
-
    file_system::{directory, DirectoryContents, Label},
+
    file_system::{directory, DirectoryEntry, Label},
    vcs::git::{
        error::*,
        Branch,
@@ -34,10 +34,10 @@ use crate::{
        TagName,
    },
};
-
use directory::Directory;
+
use directory::{Directory, FileContent};
use radicle_git_ext::Oid;
use std::{
-
    collections::{btree_set, BTreeSet},
+
    collections::{btree_set, BTreeMap, BTreeSet},
    convert::TryFrom,
    path::PathBuf,
    str,
@@ -60,7 +60,7 @@ pub struct Repository(pub(super) git2::Repository);
/// `RepositoryRef`.
#[derive(Clone, Copy)]
pub struct RepositoryRef<'a> {
-
    pub(super) repo_ref: &'a git2::Repository,
+
    pub(crate) repo_ref: &'a git2::Repository,
}

// RepositoryRef should be safe to transfer across thread boundaries since it
@@ -221,11 +221,68 @@ impl<'a> RepositoryRef<'a> {
        Ok(self.repo_ref.revparse_single(oid)?.id().into())
    }

-
    /// Gets a snapshot of the repo as a Directory.
-
    pub fn snapshot<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
+
    /// Returns a top level `Directory` without nested sub-directories.
+
    ///
+
    /// To visit inside any nested sub-directories, call `directory.get(&repo)`
+
    /// on the sub-directory.
+
    pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
        let commit = commit.to_commit(self)?;
        let git2_commit = self.repo_ref.find_commit((commit.id).into())?;
-
        self.directory_of_commit(&git2_commit)
+
        let tree = git2_commit.as_object().peel_to_tree()?;
+
        Ok(Directory {
+
            name: Label::root(),
+
            oid: 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.repo_ref.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
@@ -271,6 +328,18 @@ impl<'a> RepositoryRef<'a> {
        })
    }

+
    /// Obtain the file content
+
    pub(crate) fn file_content(&self, object_id: Oid) -> Result<FileContent, Error> {
+
        let blob = self.repo_ref.find_blob(object_id.into())?;
+
        Ok(FileContent::new(blob))
+
    }
+

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

    /// Lists branch names with `filter`.
    pub fn branch_names(&self, filter: &Glob<Branch>) -> Result<Vec<BranchName>, Error> {
        let branches: Result<Vec<BranchName>, Error> =
@@ -378,12 +447,12 @@ impl<'a> RepositoryRef<'a> {
        &self,
        git2_commit: &git2::Commit,
        path: file_system::Path,
-
    ) -> Result<directory::File, Error> {
+
    ) -> Result<FileContent, Error> {
        let git2_tree = git2_commit.tree()?;
        let entry = git2_tree.get_path(PathBuf::from(&path).as_ref())?;
        let object = entry.to_object(self.repo_ref)?;
-
        let blob = object.as_blob().ok_or(Error::PathNotFound(path))?;
-
        Ok(directory::File::new(blob.content()))
+
        let blob = object.into_blob().map_err(|_| Error::PathNotFound(path))?;
+
        Ok(FileContent::new(blob))
    }

    pub(crate) fn diff_commit_and_parents(
@@ -429,100 +498,6 @@ impl<'a> RepositoryRef<'a> {
        Ok(diff)
    }

-
    /// 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| {
-
            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);
-
                    }
-
                }
-
            }
-

-
            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;
-
                            },
-
                        }
-
                    }
-
                },
-
                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
-
        })?;
-

-
        // 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.
-
            }
-
        }
-

-
        Err(Error::RevParseFailure {
-
            rev: commit.id().to_string(),
-
        })
-
    }
-

    /// Returns the history with the `head` commit.
    pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
        History::new(*self, head)
modified radicle-surf/t/src/diff.rs
@@ -4,60 +4,7 @@
//! Unit tests for radicle_surf::diff

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

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

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

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

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

-
    assert_eq!(diff, expected_diff)
-
}
-

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

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

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

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

-
    assert_eq!(diff, expected_diff)
-
}
+
use radicle_surf::diff::*;

/* TODO(fintan): Move is not detected yet
#[test]
@@ -74,122 +21,6 @@ fn test_moved_file() {
}
*/

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

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

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

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

-
    assert_eq!(diff, expected_diff)
-
}
-

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

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

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

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

-
    assert_eq!(diff, expected_diff)
-
}
-

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

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

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

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

-
    assert_eq!(diff, expected_diff)
-
}
-

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

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

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

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

-
    assert_eq!(diff, expected_diff)
-
}
-

/* TODO(fintan): Tricky stuff
#[test]
fn test_disjoint_directories() {
modified radicle-surf/t/src/file_system.rs
@@ -4,93 +4,6 @@
//! Unit tests for radicle_surf::file_system

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

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

-
        let files: Vec<Label> = directory.contents().map(|c| c.label().clone()).collect();
-
        assert_eq!(
-
            files,
-
            vec![
-
                "bar.hs".parse::<Label>().unwrap(),
-
                "baz.hs".parse::<Label>().unwrap(),
-
                "foo.hs".parse::<Label>().unwrap(),
-
            ]
-
        );
-
    }
-
}
-

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

-
    #[test]
-
    fn in_root() {
-
        let file = File::new(b"module Banana ...");
-
        let mut directory = Directory::root();
-
        directory.insert_file(unsound::path::new("bar/foo.hs"), file.clone());
-

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

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

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

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

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

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

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

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

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

@@ -114,3 +27,73 @@ mod path {
        );
    }
}
+

+
#[cfg(test)]
+
mod directory {
+
    use radicle_surf::{
+
        file_system::DirectoryEntry,
+
        git::{Branch, Repository},
+
    };
+
    use std::path::Path;
+

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

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

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

+
        // get_path 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(_)));
+

+
        // get_path 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 inner_path = Path::new("garden.txt");
+
            let inner_entry = sub_dir.get_path(inner_path, &repo).unwrap();
+
            assert!(matches!(inner_entry, DirectoryEntry::File(_)));
+
        }
+

+
        // get_path 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());
+

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

+
    #[test]
+
    fn directory_size() {
+
        let repo = Repository::open(GIT_PLATINUM).unwrap();
+
        let repo = repo.as_ref();
+
        let root = repo.root_dir(&Branch::local("master")).unwrap();
+

+
        /*
+
        git-platinum (master) $ ls -l src
+
        -rw-r--r-- 1 pi pi 10044 Oct 31 11:32 Eval.hs
+
        -rw-r--r-- 1 pi pi  6253 Oct 31 11:27 memory.rs
+

+
        10044 + 6253 = 16297
+
         */
+

+
        let path = Path::new("src");
+
        let entry = root.get_path(path, &repo).unwrap();
+
        assert!(matches!(entry, DirectoryEntry::Directory(_)));
+
        if let DirectoryEntry::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, BranchType, Commit};
use radicle_surf::{
    diff::*,
-
    file_system::{unsound, DirectoryContents, Path},
+
    file_system::{unsound, DirectoryEntry, Path},
    git::{error::Error, Branch, Glob, Namespace, Oid, Repository, TagName},
};

@@ -18,7 +18,7 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
fn test_submodule_failure() {
    let repo = Repository::discover(".").unwrap();
-
    repo.as_ref().snapshot(&Branch::local("main")).unwrap();
+
    repo.as_ref().root_dir(&Branch::local("main")).unwrap();
}

#[cfg(test)]
@@ -744,8 +744,8 @@ mod reference {
            .collect::<Result<Vec<Tag>, Error>>()
            .unwrap();
        assert_eq!(tags.len(), 6);
-
        let root_dir = repo_ref.snapshot(&tags[0]).unwrap();
-
        assert_eq!(root_dir.contents().count(), 1);
+
        let root_dir = repo_ref.root_dir(&tags[0]).unwrap();
+
        assert_eq!(root_dir.contents(&repo_ref).unwrap().iter().count(), 1);
    }

    #[test]
@@ -767,27 +767,49 @@ mod reference {

mod code_browsing {
    use super::*;
-
    use radicle_surf::file_system::Directory;
+
    use radicle_surf::{file_system::Directory, git::RepositoryRef};

    #[test]
    fn iterate_root_dir_recursive() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let repo = repo.as_ref();
-
        let root_dir = repo.snapshot(&Branch::local("master")).unwrap();
-
        let count = println_dir(&root_dir, 0);
+
        let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
+
        let count = println_dir(&root_dir, &repo, 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 {
+
        fn println_dir(dir: &Directory, repo: &RepositoryRef, indent_level: usize) -> i32 {
            let mut count = 0;
-
            for item in dir.contents() {
+
            for item in dir.contents(repo).unwrap().iter() {
                println!("> {}{}", " ".repeat(indent_level * 4), &item.label());
                count += 1;
-
                if let DirectoryContents::Directory(sub_dir) = item {
-
                    count += println_dir(sub_dir, indent_level + 1);
+
                if let DirectoryEntry::Directory(sub_dir) = item {
+
                    count += println_dir(sub_dir, repo, indent_level + 1);
+
                }
+
            }
+
            count
+
        }
+
    }
+

+
    #[test]
+
    fn browse_repo_lazily() {
+
        let repo = Repository::open(GIT_PLATINUM).unwrap();
+
        let repo = repo.as_ref();
+
        let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
+
        let count = root_dir.contents(&repo).unwrap().iter().count();
+
        assert_eq!(count, 8);
+
        let count = traverse(&root_dir, &repo);
+
        assert_eq!(count, 36);
+

+
        fn traverse(dir: &Directory, repo: &RepositoryRef) -> 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