Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
remove source/ layer and file_system/ layer.
Han Xu committed 3 years ago
commit 4e373fb8de5bfa136d9095e0c618cc22b02bf4ec
parent 824e0d5
18 files changed +1184 -1338
modified radicle-surf/examples/browsing.rs
@@ -25,7 +25,7 @@
//! the directories in a tree-like structure.

use radicle_surf::{
-
    file_system::{directory, Directory},
+
    fs::{self, Directory},
    git::Repository,
};
use std::{env, time::Instant};
@@ -53,8 +53,8 @@ fn print_directory(d: &Directory, repo: &Repository, indent_level: usize) {
    println!("{}{}/", &indent, d.name());
    for entry in d.entries(repo).unwrap() {
        match entry {
-
            directory::Entry::File(f) => println!("    {}{}", &indent, f.name()),
-
            directory::Entry::Directory(d) => print_directory(&d, repo, indent_level + 1),
+
            fs::Entry::File(f) => println!("    {}{}", &indent, f.name()),
+
            fs::Entry::Directory(d) => print_directory(&d, repo, indent_level + 1),
        }
    }
}
deleted radicle-surf/src/file_system.rs
@@ -1,114 +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/>.
-

-
//! A model of a non-empty directory data structure that can be searched,
-
//! queried, and rendered. The concept is to represent VCS directory, but is not
-
//! necessarily tied to one.
-
//!
-
//! # Examples
-
//!
-
//! ```
-
//! use nonempty::NonEmpty;
-
//! use radicle_surf::file_system as fs;
-
//!
-
//! // This used for unsafe set up of the directory, but should not be used in production code.
-
//! use radicle_surf::file_system::unsound;
-
//!
-
//! let mut directory = fs::Directory::root();
-
//!
-
//! // Set up root files
-
//! let readme = fs::File::new(b"Radicle Surfing");
-
//! let cargo = fs::File::new(b"[package]\nname = \"radicle-surf\"");
-
//! let root_files = NonEmpty::from((
-
//!     (unsound::label::new("README.md"), readme),
-
//!     vec![(unsound::label::new("Cargo.toml"), cargo)],
-
//! ));
-
//!
-
//! // Set up src files
-
//! let lib = fs::File::new(b"pub mod diff;\npub mod file_system;\n pub mod vcs;");
-
//! let file_system_mod = fs::File::new(b"pub mod directory;\npub mod error;\nmod path;");
-
//!
-
//! directory.insert_files(&[], root_files);
-
//! directory.insert_file(unsound::path::new("src/lib.rs"), lib.clone());
-
//! directory.insert_file(unsound::path::new("src/file_system/mod.rs"), file_system_mod);
-
//!
-
//! // With a directory in place we can begin to operate on it
-
//! // The first we will do is list what contents are at the root.
-
//! let root_contents = directory.list_directory();
-
//!
-
//! // Checking that we have the correct contents
-
//! assert_eq!(
-
//!     root_contents,
-
//!     vec![
-
//!         fs::SystemType::file(unsound::label::new("Cargo.toml")),
-
//!         fs::SystemType::file(unsound::label::new("README.md")),
-
//!         fs::SystemType::directory(unsound::label::new("src")),
-
//!     ]
-
//! );
-
//!
-
//! // We can then go down one level to explore sub-directories
-
//! // Note here that we can use `Path::new`, since there's guranteed to be a `Label`,
-
//! // although we cheated and created the label unsafely.
-
//! let src = directory.find_directory(fs::Path::new(unsound::label::new("src")));
-
//!
-
//! // Ensure that we found the src directory
-
//! assert!(src.is_some());
-
//! let src = src.unwrap();
-
//!
-
//! let src_contents = src.list_directory();
-
//!
-
//! // Checking we have the correct contents of 'src'
-
//! assert_eq!(
-
//!     src_contents,
-
//!     vec![
-
//!         fs::SystemType::directory(unsound::label::new("file_system")),
-
//!         fs::SystemType::file(unsound::label::new("lib.rs")),
-
//!     ]
-
//! );
-
//!
-
//! // We can dive down to 'file_system' either from the root or src, they should be the same.
-
//! assert_eq!(
-
//!     src.find_directory(unsound::path::new("file_system")),
-
//!     directory.find_directory(unsound::path::new("src/file_system")),
-
//! );
-
//!
-
//! // We can also find files
-
//! assert_eq!(
-
//!     src.find_file(unsound::path::new("lib.rs")),
-
//!     Some(lib)
-
//! );
-
//!
-
//! // From anywhere
-
//! assert_eq!(
-
//!     directory.find_file(unsound::path::new("src/file_system/mod.rs")),
-
//!     src.find_file(unsound::path::new("file_system/mod.rs")),
-
//! );
-
//!
-
//! // And we can also check the size of directories and files
-
//! assert_eq!(
-
//!     directory.find_file(unsound::path::new("src/file_system/mod.rs")).map(|f| f.size()),
-
//!     Some(43),
-
//! );
-
//!
-
//! assert_eq!(
-
//!     directory.size(),
-
//!     137,
-
//! );
-
//! ```
-

-
pub mod directory;
-
pub use directory::{Directory, Entries, Entry, File, FileContent};
deleted radicle-surf/src/file_system/directory.rs
@@ -1,540 +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/>.
-

-
//! Definition for a file system consisting of `Directory` and `File`.
-
//!
-
//! A `Directory` is expected to be a non-empty tree of directories and files.
-
//! See [`Directory`] for more information.
-

-
use std::{
-
    cmp::Ordering,
-
    collections::BTreeMap,
-
    convert::{Infallible, Into as _},
-
    path::{Path, PathBuf},
-
};
-

-
use git2::Blob;
-
use radicle_git_ext::{is_not_found_err, Oid};
-
use radicle_std_ext::result::ResultExt as _;
-

-
use crate::git::{Commit, 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),
-
        #[error("the path {0} is not valid")]
-
        InvalidPath(String),
-
    }
-

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

-
    #[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 {
-
    /// The name of the file.
-
    name: String,
-
    /// The relative path of the file, not including the `name`,
-
    /// in respect to the root of the git repository.
-
    prefix: PathBuf,
-
    /// The object identifier of the git blob of this file.
-
    id: Oid,
-
    /// The commit that created this file version.
-
    last_commit: Commit,
-
}
-

-
impl File {
-
    /// Construct a new `File`.
-
    ///
-
    /// The `path` must be the prefix location of the directory, and
-
    /// so should not end in `name`.
-
    ///
-
    /// The `id` must point to a git blob.
-
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid, last_commit: Commit) -> Self {
-
        debug_assert!(
-
            !prefix.ends_with(&name),
-
            "prefix = {:?}, name = {}",
-
            prefix,
-
            name
-
        );
-
        Self {
-
            name,
-
            prefix,
-
            id,
-
            last_commit,
-
        }
-
    }
-

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

-
    /// Return the exact path for this `File`, including the `name` of
-
    /// the directory itself.
-
    ///
-
    /// The path is relative to the git repository root.
-
    pub fn path(&self) -> PathBuf {
-
        self.prefix.join(&self.name)
-
    }
-

-
    /// Return the [`Path`] where this `File` is located, relative to the
-
    /// git repository root.
-
    pub fn location(&self) -> &Path {
-
        &self.prefix
-
    }
-

-
    /// 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.find_blob(self.id)?;
-
        Ok(FileContent { blob })
-
    }
-
}
-

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

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

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

-
    /// Creates a `FileContent` using a blob.
-
    pub(crate) fn new(blob: Blob<'a>) -> Self {
-
        Self { blob }
-
    }
-
}
-

-
/// A representations of a [`Directory`]'s entries.
-
pub struct Entries {
-
    listing: BTreeMap<String, Entry>,
-
}
-

-
impl Entries {
-
    /// Return the name of each [`Entry`].
-
    pub fn names(&self) -> impl Iterator<Item = &String> {
-
        self.listing.keys()
-
    }
-

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

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

-
impl Iterator for Entries {
-
    type Item = Entry;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        // Can be improved when `pop_first()` is stable for BTreeMap.
-
        let next_key = match self.listing.keys().next() {
-
            Some(k) => k.clone(),
-
            None => return None,
-
        };
-
        self.listing.remove(&next_key)
-
    }
-
}
-

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

-
impl PartialOrd for Entry {
-
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-
        Some(self.cmp(other))
-
    }
-
}
-

-
impl Ord for Entry {
-
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-
        match (self, other) {
-
            (Entry::File(x), Entry::File(y)) => x.name().cmp(y.name()),
-
            (Entry::File(_), Entry::Directory(_)) => Ordering::Less,
-
            (Entry::Directory(_), Entry::File(_)) => Ordering::Greater,
-
            (Entry::Directory(x), Entry::Directory(y)) => x.name().cmp(y.name()),
-
        }
-
    }
-
}
-

-
impl Entry {
-
    /// Get a label for the `Entriess`, either the name of the [`File`]
-
    /// or the name of the [`Directory`].
-
    pub fn name(&self) -> &String {
-
        match self {
-
            Entry::File(file) => &file.name,
-
            Entry::Directory(directory) => directory.name(),
-
        }
-
    }
-

-
    pub fn path(&self) -> PathBuf {
-
        match self {
-
            Entry::File(file) => file.path(),
-
            Entry::Directory(directory) => directory.path(),
-
        }
-
    }
-

-
    pub fn location(&self) -> &Path {
-
        match self {
-
            Entry::File(file) => file.location(),
-
            Entry::Directory(directory) => directory.location(),
-
        }
-
    }
-

-
    /// Returns `true` if the `Entry` is a file.
-
    pub fn is_file(&self) -> bool {
-
        matches!(self, Entry::File(_))
-
    }
-

-
    /// Returns `true` if the `Entry` is a directory.
-
    pub fn is_directory(&self) -> bool {
-
        matches!(self, Entry::Directory(_))
-
    }
-

-
    pub(crate) fn from_entry(
-
        entry: &git2::TreeEntry,
-
        path: PathBuf,
-
        repo: &Repository,
-
        parent_commit: Oid,
-
    ) -> Result<Option<Self>, error::Entry> {
-
        let name = entry.name().ok_or(error::Entry::Utf8Error)?.to_string();
-
        let id = entry.id().into();
-
        let escaped_name = name.replace('\\', r"\\");
-
        let entry_path = path.join(escaped_name);
-
        // FIXME: I don't like to use FIXME, but here it is. I would
-
        // like to simplify the error definitions and then fix these
-
        // unwrap(s).
-
        let last_commit = repo
-
            .last_commit(&entry_path, parent_commit)
-
            .unwrap()
-
            .unwrap();
-

-
        Ok(entry.kind().and_then(|kind| match kind {
-
            git2::ObjectType::Tree => {
-
                Some(Self::Directory(Directory::new(name, path, id, last_commit)))
-
            },
-
            git2::ObjectType::Blob => Some(Self::File(File::new(name, path, id, last_commit))),
-
            _ => None,
-
        }))
-
    }
-
}
-

-
/// 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 {
-
    /// The name of the directoy.
-
    name: String,
-
    /// The relative path of the directory, not including the `name`,
-
    /// in respect to the root of the git repository.
-
    prefix: PathBuf,
-
    /// The object identifier of the git tree of this directory.
-
    id: Oid,
-
    /// The commit that created this directory version.
-
    last_commit: Commit,
-
}
-

-
const ROOT_DIR: &str = "";
-

-
impl Directory {
-
    /// Creates a directory given its `tree_id`.
-
    ///
-
    /// The `name` and `prefix` are both set to be empty.
-
    pub(crate) fn root(tree_id: Oid, repo_commit: Commit) -> Self {
-
        Self::new(ROOT_DIR.to_string(), PathBuf::new(), tree_id, repo_commit)
-
    }
-

-
    /// Creates a directory given its `name` and `id`.
-
    ///
-
    /// The `path` must be the prefix location of the directory, and
-
    /// so should not end in `name`.
-
    ///
-
    /// The `id` must point to a `git` tree.
-
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid, last_commit: Commit) -> Self {
-
        debug_assert!(
-
            name.is_empty() || !prefix.ends_with(&name),
-
            "prefix = {:?}, name = {}",
-
            prefix,
-
            name
-
        );
-
        Self {
-
            name,
-
            prefix,
-
            id,
-
            last_commit,
-
        }
-
    }
-

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

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

-
    /// Return the exact path for this `Directory`, including the `name` of the
-
    /// directory itself.
-
    ///
-
    /// The path is relative to the git repository root.
-
    pub fn path(&self) -> PathBuf {
-
        self.prefix.join(&self.name)
-
    }
-

-
    /// Return the [`Path`] where this `Directory` is located, relative to the
-
    /// git repository root.
-
    pub fn location(&self) -> &Path {
-
        &self.prefix
-
    }
-

-
    /// 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.find_tree(self.id)?;
-

-
        let mut entries = BTreeMap::new();
-
        let mut error = None;
-
        let path = self.path();
-

-
        // Walks only the first level of entries. And `_entry_path` is always
-
        // empty for the first level.
-
        tree.walk(git2::TreeWalkMode::PreOrder, |_entry_path, entry| {
-
            match Entry::from_entry(entry, path.clone(), repo, self.last_commit.id) {
-
                Ok(Some(entry)) => match entry {
-
                    Entry::File(_) => {
-
                        entries.insert(entry.name().clone(), entry);
-
                        git2::TreeWalkResult::Ok
-
                    },
-
                    Entry::Directory(_) => {
-
                        entries.insert(entry.name().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 }),
-
        }
-
    }
-

-
    /// Returns the last commit that created or modified this directory.
-
    pub fn last_commit(&self) -> &Commit {
-
        &self.last_commit
-
    }
-

-
    /// Find the [`Entry`] found at `path`, if it exists.
-
    pub fn find_entry<P>(
-
        &self,
-
        path: &P,
-
        repo: &Repository,
-
    ) -> Result<Option<Entry>, error::Directory>
-
    where
-
        P: AsRef<Path>,
-
    {
-
        // Search the path in git2 tree.
-
        let path = path.as_ref();
-
        let git2_tree = repo.find_tree(self.id)?;
-
        let entry = git2_tree
-
            .get_path(path)
-
            .map(Some)
-
            .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))?;
-
        let parent = path
-
            .parent()
-
            .ok_or_else(|| error::Directory::InvalidPath(path.to_string_lossy().to_string()))?;
-
        let root_path = self.path().join(parent);
-

-
        Ok(entry
-
            .and_then(|entry| {
-
                Entry::from_entry(&entry, root_path.to_path_buf(), repo, self.last_commit.id)
-
                    .transpose()
-
            })
-
            .transpose()
-
            .unwrap())
-
    }
-

-
    /// Find the `Oid`, for a [`File`], found at `path`, if it exists.
-
    pub fn find_file<P>(
-
        &self,
-
        path: &P,
-
        repo: &Repository,
-
    ) -> Result<Option<File>, error::Directory>
-
    where
-
        P: AsRef<Path>,
-
    {
-
        Ok(match self.find_entry(path, repo)? {
-
            Some(Entry::File(file)) => Some(file),
-
            _ => None,
-
        })
-
    }
-

-
    /// Find the `Directory` found at `path`, if it exists.
-
    ///
-
    /// If `path` is `ROOT_DIR` (i.e. an empty path), returns self.
-
    pub fn find_directory<P>(
-
        &self,
-
        path: &P,
-
        repo: &Repository,
-
    ) -> Result<Option<Self>, error::Directory>
-
    where
-
        P: AsRef<Path>,
-
    {
-
        if path.as_ref() == Path::new(ROOT_DIR) {
-
            return Ok(Some(self.clone()));
-
        }
-

-
        Ok(match self.find_entry(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
-
    // now
-
    #[allow(dead_code)]
-
    fn fuzzy_find(_label: &Path) -> Vec<Self> {
-
        unimplemented!()
-
    }
-

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

-
impl Revision for Directory {
-
    type Error = Infallible;
-

-
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
-
        Ok(self.id)
-
    }
-
}
added radicle-surf/src/fs.rs
@@ -0,0 +1,540 @@
+
// 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/>.
+

+
//! Definition for a file system consisting of `Directory` and `File`.
+
//!
+
//! A `Directory` is expected to be a non-empty tree of directories and files.
+
//! See [`Directory`] for more information.
+

+
use std::{
+
    cmp::Ordering,
+
    collections::BTreeMap,
+
    convert::{Infallible, Into as _},
+
    path::{Path, PathBuf},
+
};
+

+
use git2::Blob;
+
use radicle_git_ext::{is_not_found_err, Oid};
+
use radicle_std_ext::result::ResultExt as _;
+

+
use crate::git::{Commit, 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),
+
        #[error("the path {0} is not valid")]
+
        InvalidPath(String),
+
    }
+

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

+
    #[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 {
+
    /// The name of the file.
+
    name: String,
+
    /// The relative path of the file, not including the `name`,
+
    /// in respect to the root of the git repository.
+
    prefix: PathBuf,
+
    /// The object identifier of the git blob of this file.
+
    id: Oid,
+
    /// The commit that created this file version.
+
    last_commit: Commit,
+
}
+

+
impl File {
+
    /// Construct a new `File`.
+
    ///
+
    /// The `path` must be the prefix location of the directory, and
+
    /// so should not end in `name`.
+
    ///
+
    /// The `id` must point to a git blob.
+
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid, last_commit: Commit) -> Self {
+
        debug_assert!(
+
            !prefix.ends_with(&name),
+
            "prefix = {:?}, name = {}",
+
            prefix,
+
            name
+
        );
+
        Self {
+
            name,
+
            prefix,
+
            id,
+
            last_commit,
+
        }
+
    }
+

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

+
    /// Return the exact path for this `File`, including the `name` of
+
    /// the directory itself.
+
    ///
+
    /// The path is relative to the git repository root.
+
    pub fn path(&self) -> PathBuf {
+
        self.prefix.join(&self.name)
+
    }
+

+
    /// Return the [`Path`] where this `File` is located, relative to the
+
    /// git repository root.
+
    pub fn location(&self) -> &Path {
+
        &self.prefix
+
    }
+

+
    /// 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.find_blob(self.id)?;
+
        Ok(FileContent { blob })
+
    }
+
}
+

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

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

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

+
    /// Creates a `FileContent` using a blob.
+
    pub(crate) fn new(blob: Blob<'a>) -> Self {
+
        Self { blob }
+
    }
+
}
+

+
/// A representations of a [`Directory`]'s entries.
+
pub struct Entries {
+
    listing: BTreeMap<String, Entry>,
+
}
+

+
impl Entries {
+
    /// Return the name of each [`Entry`].
+
    pub fn names(&self) -> impl Iterator<Item = &String> {
+
        self.listing.keys()
+
    }
+

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

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

+
impl Iterator for Entries {
+
    type Item = Entry;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        // Can be improved when `pop_first()` is stable for BTreeMap.
+
        let next_key = match self.listing.keys().next() {
+
            Some(k) => k.clone(),
+
            None => return None,
+
        };
+
        self.listing.remove(&next_key)
+
    }
+
}
+

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

+
impl PartialOrd for Entry {
+
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+
        Some(self.cmp(other))
+
    }
+
}
+

+
impl Ord for Entry {
+
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+
        match (self, other) {
+
            (Entry::File(x), Entry::File(y)) => x.name().cmp(y.name()),
+
            (Entry::File(_), Entry::Directory(_)) => Ordering::Less,
+
            (Entry::Directory(_), Entry::File(_)) => Ordering::Greater,
+
            (Entry::Directory(x), Entry::Directory(y)) => x.name().cmp(y.name()),
+
        }
+
    }
+
}
+

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

+
    pub fn path(&self) -> PathBuf {
+
        match self {
+
            Entry::File(file) => file.path(),
+
            Entry::Directory(directory) => directory.path(),
+
        }
+
    }
+

+
    pub fn location(&self) -> &Path {
+
        match self {
+
            Entry::File(file) => file.location(),
+
            Entry::Directory(directory) => directory.location(),
+
        }
+
    }
+

+
    /// Returns `true` if the `Entry` is a file.
+
    pub fn is_file(&self) -> bool {
+
        matches!(self, Entry::File(_))
+
    }
+

+
    /// Returns `true` if the `Entry` is a directory.
+
    pub fn is_directory(&self) -> bool {
+
        matches!(self, Entry::Directory(_))
+
    }
+

+
    pub(crate) fn from_entry(
+
        entry: &git2::TreeEntry,
+
        path: PathBuf,
+
        repo: &Repository,
+
        parent_commit: Oid,
+
    ) -> Result<Option<Self>, error::Entry> {
+
        let name = entry.name().ok_or(error::Entry::Utf8Error)?.to_string();
+
        let id = entry.id().into();
+
        let escaped_name = name.replace('\\', r"\\");
+
        let entry_path = path.join(escaped_name);
+
        // FIXME: I don't like to use FIXME, but here it is. I would
+
        // like to simplify the error definitions and then fix these
+
        // unwrap(s).
+
        let last_commit = repo
+
            .last_commit(&entry_path, parent_commit)
+
            .unwrap()
+
            .unwrap();
+

+
        Ok(entry.kind().and_then(|kind| match kind {
+
            git2::ObjectType::Tree => {
+
                Some(Self::Directory(Directory::new(name, path, id, last_commit)))
+
            },
+
            git2::ObjectType::Blob => Some(Self::File(File::new(name, path, id, last_commit))),
+
            _ => None,
+
        }))
+
    }
+
}
+

+
/// 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 {
+
    /// The name of the directoy.
+
    name: String,
+
    /// The relative path of the directory, not including the `name`,
+
    /// in respect to the root of the git repository.
+
    prefix: PathBuf,
+
    /// The object identifier of the git tree of this directory.
+
    id: Oid,
+
    /// The commit that created this directory version.
+
    last_commit: Commit,
+
}
+

+
const ROOT_DIR: &str = "";
+

+
impl Directory {
+
    /// Creates a directory given its `tree_id`.
+
    ///
+
    /// The `name` and `prefix` are both set to be empty.
+
    pub(crate) fn root(tree_id: Oid, repo_commit: Commit) -> Self {
+
        Self::new(ROOT_DIR.to_string(), PathBuf::new(), tree_id, repo_commit)
+
    }
+

+
    /// Creates a directory given its `name` and `id`.
+
    ///
+
    /// The `path` must be the prefix location of the directory, and
+
    /// so should not end in `name`.
+
    ///
+
    /// The `id` must point to a `git` tree.
+
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid, last_commit: Commit) -> Self {
+
        debug_assert!(
+
            name.is_empty() || !prefix.ends_with(&name),
+
            "prefix = {:?}, name = {}",
+
            prefix,
+
            name
+
        );
+
        Self {
+
            name,
+
            prefix,
+
            id,
+
            last_commit,
+
        }
+
    }
+

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

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

+
    /// Return the exact path for this `Directory`, including the `name` of the
+
    /// directory itself.
+
    ///
+
    /// The path is relative to the git repository root.
+
    pub fn path(&self) -> PathBuf {
+
        self.prefix.join(&self.name)
+
    }
+

+
    /// Return the [`Path`] where this `Directory` is located, relative to the
+
    /// git repository root.
+
    pub fn location(&self) -> &Path {
+
        &self.prefix
+
    }
+

+
    /// 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.find_tree(self.id)?;
+

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

+
        // Walks only the first level of entries. And `_entry_path` is always
+
        // empty for the first level.
+
        tree.walk(git2::TreeWalkMode::PreOrder, |_entry_path, entry| {
+
            match Entry::from_entry(entry, path.clone(), repo, self.last_commit.id) {
+
                Ok(Some(entry)) => match entry {
+
                    Entry::File(_) => {
+
                        entries.insert(entry.name().clone(), entry);
+
                        git2::TreeWalkResult::Ok
+
                    },
+
                    Entry::Directory(_) => {
+
                        entries.insert(entry.name().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 }),
+
        }
+
    }
+

+
    /// Returns the last commit that created or modified this directory.
+
    pub fn last_commit(&self) -> &Commit {
+
        &self.last_commit
+
    }
+

+
    /// Find the [`Entry`] found at `path`, if it exists.
+
    pub fn find_entry<P>(
+
        &self,
+
        path: &P,
+
        repo: &Repository,
+
    ) -> Result<Option<Entry>, error::Directory>
+
    where
+
        P: AsRef<Path>,
+
    {
+
        // Search the path in git2 tree.
+
        let path = path.as_ref();
+
        let git2_tree = repo.find_tree(self.id)?;
+
        let entry = git2_tree
+
            .get_path(path)
+
            .map(Some)
+
            .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))?;
+
        let parent = path
+
            .parent()
+
            .ok_or_else(|| error::Directory::InvalidPath(path.to_string_lossy().to_string()))?;
+
        let root_path = self.path().join(parent);
+

+
        Ok(entry
+
            .and_then(|entry| {
+
                Entry::from_entry(&entry, root_path.to_path_buf(), repo, self.last_commit.id)
+
                    .transpose()
+
            })
+
            .transpose()
+
            .unwrap())
+
    }
+

+
    /// Find the `Oid`, for a [`File`], found at `path`, if it exists.
+
    pub fn find_file<P>(
+
        &self,
+
        path: &P,
+
        repo: &Repository,
+
    ) -> Result<Option<File>, error::Directory>
+
    where
+
        P: AsRef<Path>,
+
    {
+
        Ok(match self.find_entry(path, repo)? {
+
            Some(Entry::File(file)) => Some(file),
+
            _ => None,
+
        })
+
    }
+

+
    /// Find the `Directory` found at `path`, if it exists.
+
    ///
+
    /// If `path` is `ROOT_DIR` (i.e. an empty path), returns self.
+
    pub fn find_directory<P>(
+
        &self,
+
        path: &P,
+
        repo: &Repository,
+
    ) -> Result<Option<Self>, error::Directory>
+
    where
+
        P: AsRef<Path>,
+
    {
+
        if path.as_ref() == Path::new(ROOT_DIR) {
+
            return Ok(Some(self.clone()));
+
        }
+

+
        Ok(match self.find_entry(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
+
    // now
+
    #[allow(dead_code)]
+
    fn fuzzy_find(_label: &Path) -> Vec<Self> {
+
        unimplemented!()
+
    }
+

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

+
impl Revision for Directory {
+
    type Error = Infallible;
+

+
    fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+
        Ok(self.id)
+
    }
+
}
modified radicle-surf/src/git/repo.rs
@@ -28,10 +28,7 @@ use thiserror::Error;

use crate::{
    diff::{self, *},
-
    file_system::{
-
        directory::{self, File, FileContent},
-
        Directory,
-
    },
+
    fs::{self, Directory, File, FileContent},
    git::{
        commit,
        glob,
@@ -47,7 +44,7 @@ use crate::{
        Tag,
        ToCommit,
    },
-
    source::{self, commit::Header, Blob, Tree, TreeEntry},
+
    object::{commit::Header, Blob, Tree, TreeEntry},
};

pub mod iter;
@@ -70,9 +67,9 @@ pub enum Error {
    Diff(#[from] diff::git::error::Diff),
    /// A wrapper around the generic [`git2::Error`].
    #[error(transparent)]
-
    Directory(#[from] directory::error::Directory),
+
    Directory(#[from] fs::error::Directory),
    #[error(transparent)]
-
    File(#[from] directory::error::File),
+
    File(#[from] fs::error::File),
    #[error(transparent)]
    Git(#[from] git2::Error),
    #[error(transparent)]
@@ -301,7 +298,7 @@ impl Repository {
        let last_commit = self
            .last_commit(path, commit)?
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
-
        let header = source::commit::Header::from(last_commit);
+
        let header = Header::from(last_commit);
        Ok(Tree::new(dir.id(), entries, header))
    }

@@ -314,7 +311,7 @@ impl Repository {
        let last_commit = self
            .last_commit(path, commit)?
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
-
        let header = source::commit::Header::from(last_commit);
+
        let header = Header::from(last_commit);

        let content = file.content(self)?;
        Ok(Blob::new(file.id(), content.as_bytes(), header))
modified radicle-surf/src/lib.rs
@@ -29,6 +29,6 @@ pub extern crate git_ref_format;
extern crate radicle_git_ext as git_ext;

pub mod diff;
-
pub mod file_system;
+
pub mod fs;
pub mod git;
-
pub mod source;
+
pub mod object;
added radicle-surf/src/object.rs
@@ -0,0 +1,49 @@
+
// 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/>.
+

+
//! Common definitions for git objects (blob and tree).
+
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+

+
use std::path::PathBuf;
+

+
pub mod blob;
+
pub use blob::{Blob, BlobContent};
+

+
pub mod tree;
+
pub use tree::{Tree, TreeEntry};
+

+
pub mod commit;
+

+
use crate::{fs, git};
+

+
/// An error reported by object types.
+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    #[error(transparent)]
+
    Directory(#[from] fs::error::Directory),
+

+
    #[error(transparent)]
+
    File(#[from] fs::error::File),
+

+
    /// An error occurred during a git operation.
+
    #[error(transparent)]
+
    Git(#[from] git::Error),
+

+
    /// Trying to find a file path which could not be found.
+
    #[error("the path '{0}' was not found")]
+
    PathNotFound(PathBuf),
+
}
added radicle-surf/src/object/blob.rs
@@ -0,0 +1,135 @@
+
// 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/>.
+

+
//! Represents git object type 'blob', i.e. actual file contents.
+
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+

+
use std::str;
+

+
use radicle_git_ext::Oid;
+
#[cfg(feature = "serde")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::object::commit;
+

+
/// Represents a git blob object.
+
pub struct Blob {
+
    id: Oid,
+
    content: BlobContent,
+
    commit: commit::Header,
+
}
+

+
impl Blob {
+
    /// Returns the [`Blob`] for a file at `revision` under `path`.
+
    pub(crate) fn new(id: Oid, content: &[u8], commit: commit::Header) -> Self {
+
        let content = BlobContent::from(content);
+
        Self {
+
            id,
+
            content,
+
            commit,
+
        }
+
    }
+

+
    /// Indicates if the content of the [`Blob`] is binary.
+
    #[must_use]
+
    pub fn is_binary(&self) -> bool {
+
        matches!(self.content, BlobContent::Binary(_))
+
    }
+

+
    pub fn object_id(&self) -> Oid {
+
        self.id
+
    }
+

+
    pub fn content(&self) -> &BlobContent {
+
        &self.content
+
    }
+

+
    /// Returns the commit that created this blob.
+
    pub fn commit(&self) -> &commit::Header {
+
        &self.commit
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for Blob {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        const FIELDS: usize = 5;
+
        let mut state = serializer.serialize_struct("Blob", FIELDS)?;
+
        state.serialize_field("binary", &self.is_binary())?;
+
        state.serialize_field("content", &self.content)?;
+
        state.serialize_field("lastCommit", &self.commit)?;
+
        state.end()
+
    }
+
}
+

+
/// Variants of blob content.
+
#[derive(PartialEq, Eq)]
+
pub enum BlobContent {
+
    /// Content is plain text and can be passed as a string.
+
    Plain(String),
+
    /// Content is binary and needs special treatment.
+
    Binary(Vec<u8>),
+
}
+

+
impl BlobContent {
+
    /// Returns the size of this `BlobContent`.
+
    pub fn size(&self) -> usize {
+
        match self {
+
            Self::Plain(content) => content.len(),
+
            Self::Binary(bytes) => bytes.len(),
+
        }
+
    }
+

+
    /// Returns the content as bytes.
+
    pub fn as_bytes(&self) -> &[u8] {
+
        match self {
+
            Self::Plain(content) => content.as_bytes(),
+
            Self::Binary(bytes) => &bytes[..],
+
        }
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for BlobContent {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        match self {
+
            Self::Plain(content) => serializer.serialize_str(content),
+
            Self::Binary(bytes) => {
+
                let encoded = base64::encode(bytes);
+
                serializer.serialize_str(&encoded)
+
            },
+
        }
+
    }
+
}
+

+
impl From<&[u8]> for BlobContent {
+
    fn from(bytes: &[u8]) -> Self {
+
        match str::from_utf8(bytes) {
+
            Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
+
            Err(_) => BlobContent::Binary(bytes.to_owned()),
+
        }
+
    }
+
}
added radicle-surf/src/object/commit.rs
@@ -0,0 +1,206 @@
+
// 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/>.
+

+
//! Represents a commit.
+

+
use std::path::PathBuf;
+

+
use git_ref_format::RefString;
+
#[cfg(feature = "serde")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::{
+
    diff,
+
    git::{self, glob, Glob, Repository},
+
};
+

+
use radicle_git_ext::Oid;
+

+
/// Representation of a changeset between two revs.
+
#[cfg_attr(feature = "serde", derive(Serialize))]
+
#[derive(Clone)]
+
pub struct Commit {
+
    /// The commit header.
+
    pub header: Header,
+
    /// The changeset introduced by this commit.
+
    pub diff: diff::Diff,
+
    /// The list of branches this commit belongs to.
+
    pub branches: Vec<RefString>,
+
}
+

+
/// Representation of a code commit.
+
#[derive(Clone, Debug)]
+
pub struct Header {
+
    /// Identifier of the commit in the form of a sha1 hash. Often referred to
+
    /// as oid or object id.
+
    pub sha1: Oid,
+
    /// The author of the commit.
+
    pub author: Person,
+
    /// The summary of the commit message body.
+
    pub summary: String,
+
    /// The entire commit message body.
+
    pub message: String,
+
    /// The committer of the commit.
+
    pub committer: Person,
+
    /// The recorded time of the committer signature. This is a convenience
+
    /// alias until we expose the actual author and commiter signatures.
+
    pub committer_time: git2::Time,
+
}
+

+
impl Header {
+
    /// Returns the commit description text. This is the text after the one-line
+
    /// summary.
+
    #[must_use]
+
    pub fn description(&self) -> &str {
+
        self.message
+
            .strip_prefix(&self.summary)
+
            .unwrap_or(&self.message)
+
            .trim()
+
    }
+
}
+

+
impl From<&git::Commit> for Header {
+
    fn from(commit: &git::Commit) -> Self {
+
        Self {
+
            sha1: commit.id,
+
            author: Person {
+
                name: commit.author.name.clone(),
+
                email: commit.author.email.clone(),
+
            },
+
            summary: commit.summary.clone(),
+
            message: commit.message.clone(),
+
            committer: Person {
+
                name: commit.committer.name.clone(),
+
                email: commit.committer.email.clone(),
+
            },
+
            committer_time: commit.committer.time,
+
        }
+
    }
+
}
+

+
impl From<git::Commit> for Header {
+
    fn from(commit: git::Commit) -> Self {
+
        Self::from(&commit)
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for Header {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Header", 6)?;
+
        state.serialize_field("sha1", &self.sha1.to_string())?;
+
        state.serialize_field("author", &self.author)?;
+
        state.serialize_field("summary", &self.summary)?;
+
        state.serialize_field("description", &self.description())?;
+
        state.serialize_field("committer", &self.committer)?;
+
        state.serialize_field("committerTime", &self.committer_time.seconds())?;
+
        state.end()
+
    }
+
}
+

+
/// A selection of commit headers and their statistics.
+
#[cfg_attr(feature = "serde", derive(Serialize))]
+
pub struct Commits {
+
    /// The commit headers
+
    pub headers: Vec<Header>,
+
    /// The statistics for the commit headers
+
    pub stats: git::Stats,
+
}
+

+
/// Retrieves a [`Commit`].
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn commit<R: git::Revision>(repo: &Repository, rev: R) -> Result<Commit, Error> {
+
    let commit = repo.commit(rev)?;
+
    let sha1 = commit.id;
+
    let header = Header::from(&commit);
+
    let diff = repo.diff_commit(commit)?;
+

+
    let branches = repo
+
        .revision_branches(&sha1, Glob::all_heads().branches().and(Glob::all_remotes()))?
+
        .into_iter()
+
        .map(|b| b.refname().into())
+
        .collect();
+

+
    Ok(Commit {
+
        header,
+
        diff,
+
        branches,
+
    })
+
}
+

+
/// Retrieves the [`Header`] for the given `sha1`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn header(repo: &Repository, sha1: Oid) -> Result<Header, Error> {
+
    let commit = repo.commit(sha1)?;
+
    Ok(Header::from(&commit))
+
}
+

+
/// Retrieves the [`Commit`] history for the given `revision`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn commits<R>(repo: &Repository, revision: &R) -> Result<Commits, Error>
+
where
+
    R: git::Revision,
+
{
+
    let stats = repo.stats_from(revision)?;
+
    let commits = repo.history(revision)?.collect::<Result<Vec<_>, _>>()?;
+
    let headers = commits.into_iter().map(Header::from).collect();
+
    Ok(Commits { headers, stats })
+
}
+

+
/// An error reported by commit API.
+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    /// An error occurred during a git operation.
+
    #[error(transparent)]
+
    Git(#[from] git::Error),
+

+
    #[error(transparent)]
+
    Glob(#[from] glob::Error),
+

+
    /// Trying to find a file path which could not be found.
+
    #[error("the path '{0}' was not found")]
+
    PathNotFound(PathBuf),
+
}
+

+
/// Representation of a person (e.g. committer, author, signer) from a
+
/// repository. Usually extracted from a signature.
+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
+
#[derive(Clone, Debug)]
+
pub struct Person {
+
    /// Name part of the commit signature.
+
    pub name: String,
+
    /// Email part of the commit signature.
+
    pub email: String,
+
}
added radicle-surf/src/object/tree.rs
@@ -0,0 +1,230 @@
+
// 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/>.
+

+
//! Represents git object type 'tree', i.e. like directory entries in Unix.
+
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+

+
use std::cmp::Ordering;
+

+
use radicle_git_ext::Oid;
+
#[cfg(feature = "serde")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::{fs, object::commit};
+

+
/// Represents a tree object as in git. It is essentially the content of
+
/// one directory. Note that multiple directories can have the same content,
+
/// i.e. have the same tree object. Hence this struct does not embed its path.
+
#[derive(Clone, Debug)]
+
pub struct Tree {
+
    /// The object id of this tree.
+
    id: Oid,
+
    entries: Vec<TreeEntry>,
+
    /// The commit object that created this tree object.
+
    commit: commit::Header,
+
}
+

+
impl Tree {
+
    /// Creates a new tree.
+
    pub(crate) fn new(id: Oid, entries: Vec<TreeEntry>, commit: commit::Header) -> Self {
+
        Self {
+
            id,
+
            entries,
+
            commit,
+
        }
+
    }
+

+
    pub fn object_id(&self) -> Oid {
+
        self.id
+
    }
+

+
    /// Returns the commit that created this tree.
+
    pub fn commit(&self) -> &commit::Header {
+
        &self.commit
+
    }
+

+
    /// Returns the entries of the tree.
+
    pub fn entries(&self) -> &Vec<TreeEntry> {
+
        &self.entries
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for Tree {
+
    /// Sample output:
+
    /// (for `<entry_1>` and `<entry_2>` sample output, see [`TreeEntry`])
+
    /// ```
+
    /// {
+
    ///   "entries": [
+
    ///     { <entry_1> },
+
    ///     { <entry_2> },
+
    ///   ],
+
    ///   "lastCommit": {
+
    ///     "author": {
+
    ///       "email": "foobar@gmail.com",
+
    ///       "name": "Foo Bar"
+
    ///     },
+
    ///     "committer": {
+
    ///       "email": "noreply@github.com",
+
    ///       "name": "GitHub"
+
    ///     },
+
    ///     "committerTime": 1582198877,
+
    ///     "description": "A sample commit.",
+
    ///     "sha1": "b57846bbc8ced6587bf8329fc4bce970eb7b757e",
+
    ///     "summary": "Add a new sample"
+
    ///   },
+
    ///   "oid": "dd52e9f8dfe1d8b374b2a118c25235349a743dd2"
+
    /// }
+
    /// ```
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        const FIELDS: usize = 4;
+
        let mut state = serializer.serialize_struct("Tree", FIELDS)?;
+
        state.serialize_field("oid", &self.id)?;
+
        state.serialize_field("entries", &self.entries)?;
+
        state.serialize_field("lastCommit", &self.commit)?;
+
        state.end()
+
    }
+
}
+

+
#[derive(Debug, Clone, Copy)]
+
pub enum Entry {
+
    Tree(Oid),
+
    Blob(Oid),
+
}
+

+
/// Entry in a Tree result.
+
#[derive(Clone, Debug)]
+
pub struct TreeEntry {
+
    name: String,
+
    entry: Entry,
+

+
    /// The commit object that created this entry object.
+
    commit: commit::Header,
+
}
+

+
impl TreeEntry {
+
    pub(crate) fn new(name: String, entry: Entry, commit: commit::Header) -> Self {
+
        Self {
+
            name,
+
            entry,
+
            commit,
+
        }
+
    }
+

+
    pub fn name(&self) -> &str {
+
        &self.name
+
    }
+

+
    pub fn entry(&self) -> &Entry {
+
        &self.entry
+
    }
+

+
    pub fn is_tree(&self) -> bool {
+
        matches!(self.entry, Entry::Tree(_))
+
    }
+

+
    pub fn commit(&self) -> &commit::Header {
+
        &self.commit
+
    }
+

+
    pub fn object_id(&self) -> Oid {
+
        match self.entry {
+
            Entry::Blob(id) => id,
+
            Entry::Tree(id) => id,
+
        }
+
    }
+
}
+

+
// To support `sort`.
+
impl Ord for TreeEntry {
+
    fn cmp(&self, other: &Self) -> Ordering {
+
        self.name.cmp(&other.name)
+
    }
+
}
+

+
impl PartialOrd for TreeEntry {
+
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+
        Some(self.cmp(other))
+
    }
+
}
+

+
impl PartialEq for TreeEntry {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.name == other.name
+
    }
+
}
+

+
impl Eq for TreeEntry {}
+

+
impl From<fs::Entry> for Entry {
+
    fn from(entry: fs::Entry) -> Self {
+
        match entry {
+
            fs::Entry::File(f) => Entry::Blob(f.id()),
+
            fs::Entry::Directory(d) => Entry::Tree(d.id()),
+
        }
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for TreeEntry {
+
    /// Sample output:
+
    /// ```
+
    ///  {
+
    ///     "kind": "blob",
+
    ///     "lastCommit": {
+
    ///       "author": {
+
    ///         "email": "foobar@gmail.com",
+
    ///         "name": "Foo Bar"
+
    ///       },
+
    ///       "committer": {
+
    ///         "email": "noreply@github.com",
+
    ///         "name": "GitHub"
+
    ///       },
+
    ///       "committerTime": 1578309972,
+
    ///       "description": "This is a sample file",
+
    ///       "sha1": "2873745c8f6ffb45c990eb23b491d4b4b6182f95",
+
    ///       "summary": "Add a new sample"
+
    ///     },
+
    ///     "name": "Sample.rs",
+
    ///     "oid": "6d6240123a8d8ea8a8376610168a0a4bcb96afd0"
+
    ///   },
+
    /// ```
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        const FIELDS: usize = 4;
+
        let mut state = serializer.serialize_struct("TreeEntry", FIELDS)?;
+
        state.serialize_field("name", &self.name)?;
+
        state.serialize_field(
+
            "kind",
+
            match self.entry {
+
                Entry::Blob(_) => "blob",
+
                Entry::Tree(_) => "tree",
+
            },
+
        )?;
+
        state.serialize_field("oid", &self.object_id())?;
+
        state.serialize_field("lastCommit", &self.commit)?;
+
        state.end()
+
    }
+
}
deleted radicle-surf/src/source.rs
@@ -1,20 +0,0 @@
-
//! The `source` module provides a layer on top of the [`crate::git`]
-
//! functionality.
-
//!
-
//! It provides data types of [`Blob`]s, [`Tree`]s, and [`Commit`]s
-
//! (see [git objects][git-objects]).  These types are analgous to
-
//! [`crate::file_system::File`], [`crate::file_system::Directory`], and
-
//! [`crate::git::Commit`]. However, they provide extra metadata and
-
//! can be serialized to serve to other applications. For example,
-
//! they could be used in an HTTP server for viewing a Git repository.
-
//!
-
//! [git-objects]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
-

-
pub mod object;
-
pub use object::{blob, tree, Blob, BlobContent, Tree, TreeEntry};
-

-
pub mod commit;
-
pub use commit::{commit, commits, Commit};
-

-
mod person;
-
pub use person::Person;
deleted radicle-surf/src/source/commit.rs
@@ -1,196 +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/>.
-

-
//! Represents a commit.
-

-
use std::path::PathBuf;
-

-
use git_ref_format::RefString;
-
#[cfg(feature = "serde")]
-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use crate::{
-
    diff,
-
    git::{self, glob, Glob, Repository},
-
    source::person::Person,
-
};
-

-
use radicle_git_ext::Oid;
-

-
/// Representation of a changeset between two revs.
-
#[cfg_attr(feature = "serde", derive(Serialize))]
-
#[derive(Clone)]
-
pub struct Commit {
-
    /// The commit header.
-
    pub header: Header,
-
    /// The changeset introduced by this commit.
-
    pub diff: diff::Diff,
-
    /// The list of branches this commit belongs to.
-
    pub branches: Vec<RefString>,
-
}
-

-
/// Representation of a code commit.
-
#[derive(Clone, Debug)]
-
pub struct Header {
-
    /// Identifier of the commit in the form of a sha1 hash. Often referred to
-
    /// as oid or object id.
-
    pub sha1: Oid,
-
    /// The author of the commit.
-
    pub author: Person,
-
    /// The summary of the commit message body.
-
    pub summary: String,
-
    /// The entire commit message body.
-
    pub message: String,
-
    /// The committer of the commit.
-
    pub committer: Person,
-
    /// The recorded time of the committer signature. This is a convenience
-
    /// alias until we expose the actual author and commiter signatures.
-
    pub committer_time: git2::Time,
-
}
-

-
impl Header {
-
    /// Returns the commit description text. This is the text after the one-line
-
    /// summary.
-
    #[must_use]
-
    pub fn description(&self) -> &str {
-
        self.message
-
            .strip_prefix(&self.summary)
-
            .unwrap_or(&self.message)
-
            .trim()
-
    }
-
}
-

-
impl From<&git::Commit> for Header {
-
    fn from(commit: &git::Commit) -> Self {
-
        Self {
-
            sha1: commit.id,
-
            author: Person {
-
                name: commit.author.name.clone(),
-
                email: commit.author.email.clone(),
-
            },
-
            summary: commit.summary.clone(),
-
            message: commit.message.clone(),
-
            committer: Person {
-
                name: commit.committer.name.clone(),
-
                email: commit.committer.email.clone(),
-
            },
-
            committer_time: commit.committer.time,
-
        }
-
    }
-
}
-

-
impl From<git::Commit> for Header {
-
    fn from(commit: git::Commit) -> Self {
-
        Self::from(&commit)
-
    }
-
}
-

-
#[cfg(feature = "serde")]
-
impl Serialize for Header {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Header", 6)?;
-
        state.serialize_field("sha1", &self.sha1.to_string())?;
-
        state.serialize_field("author", &self.author)?;
-
        state.serialize_field("summary", &self.summary)?;
-
        state.serialize_field("description", &self.description())?;
-
        state.serialize_field("committer", &self.committer)?;
-
        state.serialize_field("committerTime", &self.committer_time.seconds())?;
-
        state.end()
-
    }
-
}
-

-
/// A selection of commit headers and their statistics.
-
#[cfg_attr(feature = "serde", derive(Serialize))]
-
pub struct Commits {
-
    /// The commit headers
-
    pub headers: Vec<Header>,
-
    /// The statistics for the commit headers
-
    pub stats: git::Stats,
-
}
-

-
/// Retrieves a [`Commit`].
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn commit<R: git::Revision>(repo: &Repository, rev: R) -> Result<Commit, Error> {
-
    let commit = repo.commit(rev)?;
-
    let sha1 = commit.id;
-
    let header = Header::from(&commit);
-
    let diff = repo.diff_commit(commit)?;
-

-
    let branches = repo
-
        .revision_branches(&sha1, Glob::all_heads().branches().and(Glob::all_remotes()))?
-
        .into_iter()
-
        .map(|b| b.refname().into())
-
        .collect();
-

-
    Ok(Commit {
-
        header,
-
        diff,
-
        branches,
-
    })
-
}
-

-
/// Retrieves the [`Header`] for the given `sha1`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn header(repo: &Repository, sha1: Oid) -> Result<Header, Error> {
-
    let commit = repo.commit(sha1)?;
-
    Ok(Header::from(&commit))
-
}
-

-
/// Retrieves the [`Commit`] history for the given `revision`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn commits<R>(repo: &Repository, revision: &R) -> Result<Commits, Error>
-
where
-
    R: git::Revision,
-
{
-
    let stats = repo.stats_from(revision)?;
-
    let commits = repo.history(revision)?.collect::<Result<Vec<_>, _>>()?;
-
    let headers = commits.into_iter().map(Header::from).collect();
-
    Ok(Commits { headers, stats })
-
}
-

-
/// An error reported by commit API.
-
#[derive(Debug, thiserror::Error)]
-
pub enum Error {
-
    /// An error occurred during a git operation.
-
    #[error(transparent)]
-
    Git(#[from] git::Error),
-

-
    #[error(transparent)]
-
    Glob(#[from] glob::Error),
-

-
    /// Trying to find a file path which could not be found.
-
    #[error("the path '{0}' was not found")]
-
    PathNotFound(PathBuf),
-
}
deleted radicle-surf/src/source/object.rs
@@ -1,47 +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/>.
-

-
//! Common definitions for git objects (blob and tree).
-
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
-

-
use std::path::PathBuf;
-

-
pub mod blob;
-
pub use blob::{Blob, BlobContent};
-

-
pub mod tree;
-
pub use tree::{Tree, TreeEntry};
-

-
use crate::{file_system::directory, git};
-

-
/// An error reported by object types.
-
#[derive(Debug, thiserror::Error)]
-
pub enum Error {
-
    #[error(transparent)]
-
    Directory(#[from] directory::error::Directory),
-

-
    #[error(transparent)]
-
    File(#[from] directory::error::File),
-

-
    /// An error occurred during a git operation.
-
    #[error(transparent)]
-
    Git(#[from] git::Error),
-

-
    /// Trying to find a file path which could not be found.
-
    #[error("the path '{0}' was not found")]
-
    PathNotFound(PathBuf),
-
}
deleted radicle-surf/src/source/object/blob.rs
@@ -1,135 +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/>.
-

-
//! Represents git object type 'blob', i.e. actual file contents.
-
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
-

-
use std::str;
-

-
use radicle_git_ext::Oid;
-
#[cfg(feature = "serde")]
-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use crate::source::commit;
-

-
/// Represents a git blob object.
-
pub struct Blob {
-
    id: Oid,
-
    content: BlobContent,
-
    commit: commit::Header,
-
}
-

-
impl Blob {
-
    /// Returns the [`Blob`] for a file at `revision` under `path`.
-
    pub(crate) fn new(id: Oid, content: &[u8], commit: commit::Header) -> Self {
-
        let content = BlobContent::from(content);
-
        Self {
-
            id,
-
            content,
-
            commit,
-
        }
-
    }
-

-
    /// Indicates if the content of the [`Blob`] is binary.
-
    #[must_use]
-
    pub fn is_binary(&self) -> bool {
-
        matches!(self.content, BlobContent::Binary(_))
-
    }
-

-
    pub fn object_id(&self) -> Oid {
-
        self.id
-
    }
-

-
    pub fn content(&self) -> &BlobContent {
-
        &self.content
-
    }
-

-
    /// Returns the commit that created this blob.
-
    pub fn commit(&self) -> &commit::Header {
-
        &self.commit
-
    }
-
}
-

-
#[cfg(feature = "serde")]
-
impl Serialize for Blob {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        const FIELDS: usize = 5;
-
        let mut state = serializer.serialize_struct("Blob", FIELDS)?;
-
        state.serialize_field("binary", &self.is_binary())?;
-
        state.serialize_field("content", &self.content)?;
-
        state.serialize_field("lastCommit", &self.commit)?;
-
        state.end()
-
    }
-
}
-

-
/// Variants of blob content.
-
#[derive(PartialEq, Eq)]
-
pub enum BlobContent {
-
    /// Content is plain text and can be passed as a string.
-
    Plain(String),
-
    /// Content is binary and needs special treatment.
-
    Binary(Vec<u8>),
-
}
-

-
impl BlobContent {
-
    /// Returns the size of this `BlobContent`.
-
    pub fn size(&self) -> usize {
-
        match self {
-
            Self::Plain(content) => content.len(),
-
            Self::Binary(bytes) => bytes.len(),
-
        }
-
    }
-

-
    /// Returns the content as bytes.
-
    pub fn as_bytes(&self) -> &[u8] {
-
        match self {
-
            Self::Plain(content) => content.as_bytes(),
-
            Self::Binary(bytes) => &bytes[..],
-
        }
-
    }
-
}
-

-
#[cfg(feature = "serde")]
-
impl Serialize for BlobContent {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        match self {
-
            Self::Plain(content) => serializer.serialize_str(content),
-
            Self::Binary(bytes) => {
-
                let encoded = base64::encode(bytes);
-
                serializer.serialize_str(&encoded)
-
            },
-
        }
-
    }
-
}
-

-
impl From<&[u8]> for BlobContent {
-
    fn from(bytes: &[u8]) -> Self {
-
        match str::from_utf8(bytes) {
-
            Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
-
            Err(_) => BlobContent::Binary(bytes.to_owned()),
-
        }
-
    }
-
}
deleted radicle-surf/src/source/object/tree.rs
@@ -1,230 +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/>.
-

-
//! Represents git object type 'tree', i.e. like directory entries in Unix.
-
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
-

-
use std::cmp::Ordering;
-

-
use radicle_git_ext::Oid;
-
#[cfg(feature = "serde")]
-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use crate::{file_system::directory, source::commit};
-

-
/// Represents a tree object as in git. It is essentially the content of
-
/// one directory. Note that multiple directories can have the same content,
-
/// i.e. have the same tree object. Hence this struct does not embed its path.
-
#[derive(Clone, Debug)]
-
pub struct Tree {
-
    /// The object id of this tree.
-
    id: Oid,
-
    entries: Vec<TreeEntry>,
-
    /// The commit object that created this tree object.
-
    commit: commit::Header,
-
}
-

-
impl Tree {
-
    /// Creates a new tree.
-
    pub(crate) fn new(id: Oid, entries: Vec<TreeEntry>, commit: commit::Header) -> Self {
-
        Self {
-
            id,
-
            entries,
-
            commit,
-
        }
-
    }
-

-
    pub fn object_id(&self) -> Oid {
-
        self.id
-
    }
-

-
    /// Returns the commit that created this tree.
-
    pub fn commit(&self) -> &commit::Header {
-
        &self.commit
-
    }
-

-
    /// Returns the entries of the tree.
-
    pub fn entries(&self) -> &Vec<TreeEntry> {
-
        &self.entries
-
    }
-
}
-

-
#[cfg(feature = "serde")]
-
impl Serialize for Tree {
-
    /// Sample output:
-
    /// (for `<entry_1>` and `<entry_2>` sample output, see [`TreeEntry`])
-
    /// ```
-
    /// {
-
    ///   "entries": [
-
    ///     { <entry_1> },
-
    ///     { <entry_2> },
-
    ///   ],
-
    ///   "lastCommit": {
-
    ///     "author": {
-
    ///       "email": "foobar@gmail.com",
-
    ///       "name": "Foo Bar"
-
    ///     },
-
    ///     "committer": {
-
    ///       "email": "noreply@github.com",
-
    ///       "name": "GitHub"
-
    ///     },
-
    ///     "committerTime": 1582198877,
-
    ///     "description": "A sample commit.",
-
    ///     "sha1": "b57846bbc8ced6587bf8329fc4bce970eb7b757e",
-
    ///     "summary": "Add a new sample"
-
    ///   },
-
    ///   "oid": "dd52e9f8dfe1d8b374b2a118c25235349a743dd2"
-
    /// }
-
    /// ```
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        const FIELDS: usize = 4;
-
        let mut state = serializer.serialize_struct("Tree", FIELDS)?;
-
        state.serialize_field("oid", &self.id)?;
-
        state.serialize_field("entries", &self.entries)?;
-
        state.serialize_field("lastCommit", &self.commit)?;
-
        state.end()
-
    }
-
}
-

-
#[derive(Debug, Clone, Copy)]
-
pub enum Entry {
-
    Tree(Oid),
-
    Blob(Oid),
-
}
-

-
/// Entry in a Tree result.
-
#[derive(Clone, Debug)]
-
pub struct TreeEntry {
-
    name: String,
-
    entry: Entry,
-

-
    /// The commit object that created this entry object.
-
    commit: commit::Header,
-
}
-

-
impl TreeEntry {
-
    pub(crate) fn new(name: String, entry: Entry, commit: commit::Header) -> Self {
-
        Self {
-
            name,
-
            entry,
-
            commit,
-
        }
-
    }
-

-
    pub fn name(&self) -> &str {
-
        &self.name
-
    }
-

-
    pub fn entry(&self) -> &Entry {
-
        &self.entry
-
    }
-

-
    pub fn is_tree(&self) -> bool {
-
        matches!(self.entry, Entry::Tree(_))
-
    }
-

-
    pub fn commit(&self) -> &commit::Header {
-
        &self.commit
-
    }
-

-
    pub fn object_id(&self) -> Oid {
-
        match self.entry {
-
            Entry::Blob(id) => id,
-
            Entry::Tree(id) => id,
-
        }
-
    }
-
}
-

-
// To support `sort`.
-
impl Ord for TreeEntry {
-
    fn cmp(&self, other: &Self) -> Ordering {
-
        self.name.cmp(&other.name)
-
    }
-
}
-

-
impl PartialOrd for TreeEntry {
-
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-
        Some(self.cmp(other))
-
    }
-
}
-

-
impl PartialEq for TreeEntry {
-
    fn eq(&self, other: &Self) -> bool {
-
        self.name == other.name
-
    }
-
}
-

-
impl Eq for TreeEntry {}
-

-
impl From<directory::Entry> for Entry {
-
    fn from(entry: directory::Entry) -> Self {
-
        match entry {
-
            directory::Entry::File(f) => Entry::Blob(f.id()),
-
            directory::Entry::Directory(d) => Entry::Tree(d.id()),
-
        }
-
    }
-
}
-

-
#[cfg(feature = "serde")]
-
impl Serialize for TreeEntry {
-
    /// Sample output:
-
    /// ```
-
    ///  {
-
    ///     "kind": "blob",
-
    ///     "lastCommit": {
-
    ///       "author": {
-
    ///         "email": "foobar@gmail.com",
-
    ///         "name": "Foo Bar"
-
    ///       },
-
    ///       "committer": {
-
    ///         "email": "noreply@github.com",
-
    ///         "name": "GitHub"
-
    ///       },
-
    ///       "committerTime": 1578309972,
-
    ///       "description": "This is a sample file",
-
    ///       "sha1": "2873745c8f6ffb45c990eb23b491d4b4b6182f95",
-
    ///       "summary": "Add a new sample"
-
    ///     },
-
    ///     "name": "Sample.rs",
-
    ///     "oid": "6d6240123a8d8ea8a8376610168a0a4bcb96afd0"
-
    ///   },
-
    /// ```
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        const FIELDS: usize = 4;
-
        let mut state = serializer.serialize_struct("TreeEntry", FIELDS)?;
-
        state.serialize_field("name", &self.name)?;
-
        state.serialize_field(
-
            "kind",
-
            match self.entry {
-
                Entry::Blob(_) => "blob",
-
                Entry::Tree(_) => "tree",
-
            },
-
        )?;
-
        state.serialize_field("oid", &self.object_id())?;
-
        state.serialize_field("lastCommit", &self.commit)?;
-
        state.end()
-
    }
-
}
deleted radicle-surf/src/source/person.rs
@@ -1,29 +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/>.
-

-
//! Represents a person in a repo.
-

-
/// Representation of a person (e.g. committer, author, signer) from a
-
/// repository. Usually extracted from a signature.
-
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
-
#[derive(Clone, Debug)]
-
pub struct Person {
-
    /// Name part of the commit signature.
-
    pub name: String,
-
    /// Email part of the commit signature.
-
    pub email: String,
-
}
modified radicle-surf/t/src/file_system.rs
@@ -6,7 +6,7 @@
mod directory {
    use git_ref_format::refname;
    use radicle_surf::{
-
        file_system::{directory, Entry},
+
        fs::{self, Entry},
        git::{Branch, Repository},
    };
    use std::path::Path;
@@ -21,21 +21,21 @@ mod directory {
        // find_entry for a file.
        let path = Path::new("src/memory.rs");
        let entry = root.find_entry(&path, &repo).unwrap();
-
        assert!(matches!(entry, Some(directory::Entry::File(_))));
+
        assert!(matches!(entry, Some(fs::Entry::File(_))));

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

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

        // find_entry for non-existing file
@@ -108,8 +108,8 @@ mod directory {

        let path = Path::new("src");
        let entry = root.find_entry(&path, &repo).unwrap();
-
        assert!(matches!(entry, Some(directory::Entry::Directory(_))));
-
        if let Some(directory::Entry::Directory(d)) = entry {
+
        assert!(matches!(entry, Some(fs::Entry::Directory(_))));
+
        if let Some(fs::Entry::Directory(d)) = entry {
            assert_eq!(16297, d.size(&repo).unwrap());
        }
    }
modified radicle-surf/t/src/git/code_browsing.rs
@@ -2,7 +2,7 @@ use std::path::Path;

use git_ref_format::refname;
use radicle_surf::{
-
    file_system::{directory, Directory},
+
    fs::{self, Directory},
    git::{Branch, Repository},
};

@@ -22,14 +22,14 @@ fn iterate_root_dir_recursive() {
    /// recursively.
    /// Returns the number of items visited (i.e. printed)
    fn println_dir(dir: &Directory, repo: &Repository) -> i32 {
-
        dir.traverse::<directory::error::Directory, _, _>(
+
        dir.traverse::<fs::error::Directory, _, _>(
            repo,
            (0, 0),
            &mut |(count, indent_level), entry| {
                println!("> {}{}", " ".repeat(indent_level * 4), entry.name());
                match entry {
-
                    directory::Entry::File(_) => Ok((count + 1, indent_level)),
-
                    directory::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
+
                    fs::Entry::File(_) => Ok((count + 1, indent_level)),
+
                    fs::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
                }
            },
        )
@@ -49,7 +49,7 @@ fn browse_repo_lazily() {
    assert_eq!(count, 36);

    fn traverse(dir: &Directory, repo: &Repository) -> i32 {
-
        dir.traverse::<directory::error::Directory, _, _>(repo, 0, &mut |count, _| Ok(count + 1))
+
        dir.traverse::<fs::error::Directory, _, _>(repo, 0, &mut |count, _| Ok(count + 1))
            .unwrap()
    }
}