Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
refactored Directory, File, Tree and Blob.
Han Xu committed 3 years ago
commit 922206e08ea2675f2480f71867ce979f5a664aec
parent 79a9472
9 files changed +476 -178
modified radicle-surf/src/file_system/directory.rs
@@ -31,7 +31,7 @@ use git2::Blob;
use radicle_git_ext::{is_not_found_err, Oid};
use radicle_std_ext::result::ResultExt as _;

-
use crate::git::{Repository, Revision};
+
use crate::git::{Commit, Repository, Revision};

pub mod error {
    use thiserror::Error;
@@ -79,6 +79,8 @@ pub struct File {
    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 {
@@ -88,14 +90,19 @@ impl File {
    /// so should not end in `name`.
    ///
    /// The `id` must point to a git blob.
-
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
+
    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 }
+
        Self {
+
            name,
+
            prefix,
+
            id,
+
            last_commit,
+
        }
    }

    /// The name of this `File`.
@@ -256,12 +263,26 @@ impl Entry {
    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))),
-
            git2::ObjectType::Blob => Some(Self::File(File::new(name, path, id))),
+
            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,
        }))
    }
@@ -285,16 +306,18 @@ pub struct Directory {
    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 `id`.
+
    /// Creates a directory given its `tree_id`.
    ///
    /// The `name` and `prefix` are both set to be empty.
-
    ///
-
    /// The `id` must point to a `git` tree.
-
    pub(crate) fn root(id: Oid) -> Self {
-
        Self::new("".to_string(), PathBuf::new(), id)
+
    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`.
@@ -303,14 +326,19 @@ impl Directory {
    /// so should not end in `name`.
    ///
    /// The `id` must point to a `git` tree.
-
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
+
    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 }
+
        Self {
+
            name,
+
            prefix,
+
            id,
+
            last_commit,
+
        }
    }

    /// Get the name of the current `Directory`.
@@ -354,9 +382,10 @@ impl Directory {
        let mut error = None;
        let path = self.path();

-
        // Walks only the first level of entries.
-
        tree.walk(git2::TreeWalkMode::PreOrder, |_, entry| {
-
            match Entry::from_entry(entry, path.clone()) {
+
        // 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);
@@ -382,6 +411,11 @@ impl Directory {
        }
    }

+
    /// 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,
@@ -401,9 +435,13 @@ impl Directory {
        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, parent.to_path_buf()).transpose())
+
            .and_then(|entry| {
+
                Entry::from_entry(&entry, root_path.to_path_buf(), repo, self.last_commit.id)
+
                    .transpose()
+
            })
            .transpose()
            .unwrap())
    }
@@ -424,6 +462,8 @@ impl Directory {
    }

    /// 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,
@@ -432,6 +472,10 @@ impl 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,
modified radicle-surf/src/git/history.rs
@@ -62,7 +62,7 @@ impl<'a> History<'a> {
    ///
    /// Note that it is possible that a filtered History becomes empty,
    /// even though calling `.head()` still returns the original head.
-
    pub fn by_path<P>(mut self, path: P) -> Self
+
    pub fn by_path<P>(mut self, path: &P) -> Self
    where
        P: AsRef<Path>,
    {
modified radicle-surf/src/git/repo.rs
@@ -28,7 +28,10 @@ use thiserror::Error;

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

pub mod iter;
@@ -66,6 +70,10 @@ pub enum Error {
    Diff(#[from] diff::git::error::Diff),
    /// A wrapper around the generic [`git2::Error`].
    #[error(transparent)]
+
    Directory(#[from] directory::error::Directory),
+
    #[error(transparent)]
+
    File(#[from] directory::error::File),
+
    #[error(transparent)]
    Git(#[from] git2::Error),
    #[error(transparent)]
    Glob(#[from] glob::Error),
@@ -249,12 +257,72 @@ impl Repository {
            .map_err(|err| Error::ToCommit(err.into()))?;
        let git2_commit = self.inner.find_commit((commit.id).into())?;
        let tree = git2_commit.as_object().peel_to_tree()?;
-
        Ok(Directory::root(tree.id().into()))
+
        Ok(Directory::root(tree.id().into(), commit))
+
    }
+

+
    /// Returns a [`Directory`] for `path` in `commit`.
+
    pub fn directory<C: ToCommit, P: AsRef<Path>>(
+
        &self,
+
        commit: C,
+
        path: &P,
+
    ) -> Result<Directory, Error> {
+
        let root = self.root_dir(commit)?;
+
        root.find_directory(path, self)?
+
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
+
    }
+

+
    /// Returns a [`File`] for `path` in `commit`.
+
    pub fn file<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<File, Error> {
+
        let root = self.root_dir(commit)?;
+
        root.find_file(path, self)?
+
            .ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
+
    }
+

+
    /// Returns a [`Tree`] for `path` in `commit`.
+
    pub fn tree<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Tree, Error> {
+
        let commit = commit
+
            .to_commit(self)
+
            .map_err(|e| Error::ToCommit(e.into()))?;
+
        let dir = self.directory(commit.id, path)?;
+
        let mut entries = dir
+
            .entries(self)?
+
            .map(|en| {
+
                let name = en.name().to_string();
+
                let path = en.path();
+
                let commit = self
+
                    .last_commit(&path, commit.id)?
+
                    .ok_or(Error::PathNotFound(path))?;
+
                let commit_header = Header::from(commit);
+
                Ok(TreeEntry::new(name, en.into(), commit_header))
+
            })
+
            .collect::<Result<Vec<TreeEntry>, Error>>()?;
+
        entries.sort();
+

+
        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);
+
        Ok(Tree::new(dir.id(), entries, header))
+
    }
+

+
    /// Returns a [`Blob`] for `path` in `commit`.
+
    pub fn blob<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Blob, Error> {
+
        let commit = commit
+
            .to_commit(self)
+
            .map_err(|e| Error::ToCommit(e.into()))?;
+
        let file = self.file(commit.id, path)?;
+
        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 content = file.content(self)?;
+
        Ok(Blob::new(file.id(), content.as_bytes(), header))
    }

    /// Returns the last commit, if exists, for a `path` in the history of
    /// `rev`.
-
    pub fn last_commit<P, C>(&self, path: P, rev: C) -> Result<Option<Commit>, Error>
+
    pub fn last_commit<P, C>(&self, path: &P, rev: C) -> Result<Option<Commit>, Error>
    where
        P: AsRef<Path>,
        C: ToCommit,
@@ -444,7 +512,7 @@ impl Repository {

        let mut opts = git2::DiffOptions::new();
        if let Some(path) = path {
-
            opts.pathspec(path);
+
            opts.pathspec(path.to_string_lossy().to_string());
            // We're skipping the binary pass because we won't be inspecting deltas.
            opts.skip_binary_check(true);
        }
modified radicle-surf/src/source/object/blob.rs
@@ -18,70 +18,33 @@
//! 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::{path::Path, str};
+
use std::str;

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

-
use crate::{
-
    file_system::{File, FileContent},
-
    git::{self, Repository},
-
    source::{commit, object::Error},
-
};
+
use crate::source::commit;

-
/// File data abstraction.
+
/// Represents a git blob object.
pub struct Blob {
-
    pub file: File,
-
    pub content: BlobContent,
-
    pub commit: Option<commit::Header>,
+
    id: Oid,
+
    content: BlobContent,
+
    commit: commit::Header,
}

impl Blob {
    /// Returns the [`Blob`] for a file at `revision` under `path`.
-
    ///
-
    /// # Errors
-
    ///
-
    /// Will return [`Error`] if the project doesn't exist or a surf interaction
-
    /// fails.
-
    pub fn new<P, R>(repo: &Repository, revision: &R, path: &P) -> Result<Blob, Error>
-
    where
-
        P: AsRef<Path>,
-
        R: git::Revision,
-
    {
-
        Self::make_blob(repo, revision, path, |c| BlobContent::from(c))
-
    }
-

-
    fn make_blob<P, R>(
-
        repo: &Repository,
-
        revision: &R,
-
        path: &P,
-
        content: impl FnOnce(FileContent) -> BlobContent,
-
    ) -> Result<Blob, Error>
-
    where
-
        P: AsRef<Path>,
-
        R: git::Revision,
-
    {
-
        let path = path.as_ref();
-
        let root = repo.root_dir(revision)?;
-

-
        let file = root
-
            .find_file(&path, repo)?
-
            .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?;
-

-
        let last_commit = repo
-
            .last_commit(path, revision)?
-
            .map(|c| commit::Header::from(&c));
-

-
        let content = content(file.content(repo)?);
-

-
        Ok(Blob {
-
            file,
+
    pub(crate) fn new(id: Oid, content: &[u8], commit: commit::Header) -> Self {
+
        let content = BlobContent::from(content);
+
        Self {
+
            id,
            content,
-
            commit: last_commit,
-
        })
+
            commit,
+
        }
    }

    /// Indicates if the content of the [`Blob`] is binary.
@@ -89,6 +52,19 @@ impl Blob {
    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")]
@@ -102,8 +78,6 @@ impl Serialize for Blob {
        state.serialize_field("binary", &self.is_binary())?;
        state.serialize_field("content", &self.content)?;
        state.serialize_field("lastCommit", &self.commit)?;
-
        state.serialize_field("name", &self.file.name())?;
-
        state.serialize_field("path", &self.file.path())?;
        state.end()
    }
}
@@ -117,6 +91,24 @@ pub enum BlobContent {
    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>
@@ -133,12 +125,11 @@ impl Serialize for BlobContent {
    }
}

-
impl From<FileContent<'_>> for BlobContent {
-
    fn from(content: FileContent) -> Self {
-
        let content = content.as_bytes();
-
        match str::from_utf8(content) {
+
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(content.to_owned()),
+
            Err(_) => BlobContent::Binary(bytes.to_owned()),
        }
    }
}
modified radicle-surf/src/source/object/tree.rs
@@ -18,125 +18,213 @@
//! 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::path::Path;
+
use std::cmp::Ordering;

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

-
use crate::{
-
    file_system::{directory, Directory},
-
    git::{self, Repository},
-
    source::{commit, object::Error},
-
};
+
use crate::{file_system::directory, source::commit};

-
/// Result of a directory listing, carries other trees and blobs.
+
/// 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 {
-
    pub directory: Directory,
-
    pub commit: Option<commit::Header>,
-
    /// Entries listed in that tree result.
-
    pub entries: Vec<TreeEntry>,
+
    /// The object id of this tree.
+
    id: Oid,
+
    entries: Vec<TreeEntry>,
+
    /// The commit object that created this tree object.
+
    commit: commit::Header,
}

impl Tree {
-
    /// Retrieve the [`Tree`] for the given `revision` and directory `prefix`.
-
    ///
-
    /// # Errors
-
    ///
-
    /// Will return [`Error`] if any of the surf interactions fail.
-
    pub fn new<P, R>(repo: &Repository, revision: &R, prefix: Option<&P>) -> Result<Tree, Error>
-
    where
-
        P: AsRef<Path>,
-
        R: git::Revision,
-
    {
-
        let prefix = prefix.map(|p| p.as_ref());
-

-
        let prefix_dir = match prefix {
-
            None => repo.root_dir(revision)?,
-
            Some(path) => repo
-
                .root_dir(revision)?
-
                .find_directory(&path, repo)?
-
                .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
-
        };
-

-
        let mut entries = prefix_dir
-
            .entries(repo)?
-
            .entries()
-
            .cloned()
-
            .map(TreeEntry::from)
-
            .collect::<Vec<_>>();
-
        entries.sort();
-

-
        let last_commit = if prefix.is_none() {
-
            let history = repo.history(revision)?;
-
            Some(commit::Header::from(history.head()))
-
        } else {
-
            None
-
        };
-

-
        Ok(Tree {
+
    /// Creates a new tree.
+
    pub(crate) fn new(id: Oid, entries: Vec<TreeEntry>, commit: commit::Header) -> Self {
+
        Self {
+
            id,
            entries,
-
            directory: prefix_dir,
-
            commit: last_commit,
-
        })
+
            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.serialize_field("name", &self.directory.name())?;
-
        state.serialize_field("path", &self.directory.path())?;
        state.end()
    }
}

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

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

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

-
impl From<directory::Entry> for TreeEntry {
-
    fn from(entry: directory::Entry) -> Self {
-
        Self { entry }
+
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 From<TreeEntry> for directory::Entry {
-
    fn from(TreeEntry { entry }: TreeEntry) -> Self {
-
        entry
+
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("path", &self.entry.path())?;
-
        state.serialize_field("name", &self.entry.name())?;
-
        state.serialize_field("lastCommit", &None::<commit::Header>)?;
+
        state.serialize_field("name", &self.name)?;
        state.serialize_field(
            "kind",
            match self.entry {
-
                directory::Entry::File(_) => "blob",
-
                directory::Entry::Directory(_) => "tree",
+
                Entry::Blob(_) => "blob",
+
                Entry::Tree(_) => "tree",
            },
        )?;
+
        state.serialize_field("oid", &self.object_id())?;
+
        state.serialize_field("lastCommit", &self.commit)?;
        state.end()
    }
}
modified radicle-surf/t/src/file_system.rs
@@ -85,6 +85,7 @@ mod directory {
            .find_directory(&Path::new("src"), &repo)
            .unwrap()
            .unwrap();
+
        assert_eq!(src.path(), Path::new("src").to_path_buf());
        let src_contents: Vec<Entry> = src.entries(&repo).unwrap().collect();
        assert_eq!(src_contents.len(), 3);
        assert_eq!(src_contents[0].name(), "Eval.hs");
@@ -112,4 +113,16 @@ mod directory {
            assert_eq!(16297, d.size(&repo).unwrap());
        }
    }
+

+
    #[test]
+
    fn directory_last_commit() {
+
        let repo = Repository::open(GIT_PLATINUM).unwrap();
+
        let root = repo.root_dir(Branch::local(refname!("dev"))).unwrap();
+
        let dir = root.find_directory(&"this/is", &repo).unwrap().unwrap();
+
        let last_commit = dir.last_commit();
+
        assert_eq!(
+
            last_commit.id.to_string(),
+
            "2429f097664f9af0c5b7b389ab998b2199ffa977"
+
        );
+
    }
}
modified radicle-surf/t/src/git/code_browsing.rs
@@ -59,11 +59,9 @@ fn test_file_history() {
    let repo = Repository::open(GIT_PLATINUM).unwrap();
    let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
    let path = Path::new("README.md");
-
    let mut file_history = history.by_path(path);
+
    let mut file_history = history.by_path(&path);
    let commit = file_history.next().unwrap().unwrap();
-
    let file = repo
-
        .get_commit_file(&commit.id, &Path::new("README.md"))
-
        .unwrap();
+
    let file = repo.get_commit_file(&commit.id, &path).unwrap();
    assert_eq!(file.size(), 67);
}

modified radicle-surf/t/src/git/last_commit.rs
@@ -1,7 +1,4 @@
-
use std::{
-
    path::{Path, PathBuf},
-
    str::FromStr,
-
};
+
use std::{path::PathBuf, str::FromStr};

use git_ref_format::refname;
use radicle_git_ext::Oid;
@@ -18,7 +15,7 @@ fn readme_missing_and_memory() {

    // memory.rs is commited later so it should not exist here.
    let memory_last_commit_oid = repo
-
        .last_commit(Path::new("src/memory.rs"), oid)
+
        .last_commit(&"src/memory.rs", oid)
        .expect("Failed to get last commit")
        .map(|commit| commit.id);

@@ -26,7 +23,7 @@ fn readme_missing_and_memory() {

    // README.md exists in this commit.
    let readme_last_commit = repo
-
        .last_commit(Path::new("README.md"), oid)
+
        .last_commit(&"README.md", oid)
        .expect("Failed to get last commit")
        .map(|commit| commit.id);

@@ -44,7 +41,7 @@ fn folder_svelte() {
    let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();

    let folder_svelte = repo
-
        .last_commit(Path::new("examples/Folder.svelte"), oid)
+
        .last_commit(&"examples/Folder.svelte", oid)
        .expect("Failed to get last commit")
        .map(|commit| commit.id);

@@ -62,10 +59,7 @@ fn nest_directory() {
    let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap();

    let nested_directory_tree_commit_id = repo
-
        .last_commit(
-
            Path::new("this/is/a/really/deeply/nested/directory/tree"),
-
            oid,
-
        )
+
        .last_commit(&"this/is/a/really/deeply/nested/directory/tree", oid)
        .expect("Failed to get last commit")
        .map(|commit| commit.id);

@@ -85,13 +79,13 @@ fn can_get_last_commit_for_special_filenames() {
    let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();

    let backslash_commit_id = repo
-
        .last_commit(Path::new(r"special/faux\\path"), oid)
+
        .last_commit(&r"special/faux\\path", oid)
        .expect("Failed to get last commit")
        .map(|commit| commit.id);
    assert_eq!(backslash_commit_id, Some(expected_commit_id));

    let ogre_commit_id = repo
-
        .last_commit(Path::new("special/👹👹👹"), oid)
+
        .last_commit(&"special/👹👹👹", oid)
        .expect("Failed to get last commit")
        .map(|commit| commit.id);
    assert_eq!(ogre_commit_id, Some(expected_commit_id));
@@ -103,7 +97,7 @@ fn root() {
        .expect("Could not retrieve ./data/git-platinum as git repository");
    let rev = Branch::local(refname!("master"));
    let root_last_commit_id = repo
-
        .last_commit(PathBuf::new(), rev)
+
        .last_commit(&PathBuf::new(), rev)
        .expect("Failed to get last commit")
        .map(|commit| commit.id);

@@ -120,7 +114,7 @@ fn binary_file() {
    let repo = Repository::open(GIT_PLATINUM)
        .expect("Could not retrieve ./data/git-platinum as git repository");
    let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
-
    let file_commit = history.by_path(Path::new("bin/cat")).next();
+
    let file_commit = history.by_path(&"bin/cat").next();
    assert!(file_commit.is_some());
    println!("file commit: {:?}", &file_commit);
}
modified radicle-surf/t/src/source.rs
@@ -1,7 +1,5 @@
-
use std::path::Path;
-

use git_ref_format::refname;
-
use radicle_surf::{git::Repository, source};
+
use radicle_surf::git::Repository;
use serde_json::json;

const GIT_PLATINUM: &str = "../data/git-platinum";
@@ -9,31 +7,135 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
#[test]
fn tree_serialization() {
    let repo = Repository::open(GIT_PLATINUM).unwrap();
-
    let tree = source::Tree::new(
-
        &repo,
-
        &refname!("refs/heads/master"),
-
        Some(&Path::new("src")),
-
    )
-
    .unwrap();
+
    let tree = repo.tree(refname!("refs/heads/master"), &"src").unwrap();

    let expected = json!({
      "entries": [
        {
          "kind": "blob",
-
          "lastCommit": null,
+
          "lastCommit": {
+
            "author": {
+
              "email": "fintan.halpenny@gmail.com",
+
              "name": "Fintan Halpenny"
+
            },
+
            "committer": {
+
              "email": "noreply@github.com",
+
              "name": "GitHub"
+
            },
+
            "committerTime": 1578309972,
+
            "description": "I want to have files under src that have separate commits.\r\nThat way src's latest commit isn't the same as all its files, instead it's the file that was touched last.",
+
            "sha1": "3873745c8f6ffb45c990eb23b491d4b4b6182f95",
+
            "summary": "Extend the docs (#2)"
+
          },
          "name": "Eval.hs",
-
          "path": "src/Eval.hs"
+
          "oid": "7d6240123a8d8ea8a8376610168a0a4bcb96afd0"
        },
        {
          "kind": "blob",
-
          "lastCommit": null,
+
          "lastCommit": {
+
            "author": {
+
              "email": "rudolfs@osins.org",
+
              "name": "Rūdolfs Ošiņš"
+
            },
+
            "committer": {
+
              "email": "rudolfs@osins.org",
+
              "name": "Rūdolfs Ošiņš"
+
            },
+
            "committerTime": 1575283266,
+
            "description": "",
+
            "sha1": "e24124b7538658220b5aaf3b6ef53758f0a106dc",
+
            "summary": "Move examples to \"src\""
+
          },
          "name": "memory.rs",
-
          "path": "src/memory.rs"
+
          "oid": "b84992d24be67536837f5ab45a943f1b3f501878"
        }
      ],
-
      "lastCommit": null,
-
      "name": "src",
-
      "path": "src"
+
      "lastCommit": {
+
        "author": {
+
          "email": "rudolfs@osins.org",
+
          "name": "Rūdolfs Ošiņš"
+
        },
+
        "committer": {
+
          "email": "noreply@github.com",
+
          "name": "GitHub"
+
        },
+
        "committerTime": 1582198877,
+
        "description": "It was a bad idea to have an actual source file which is used by\r\nradicle-upstream in the fixtures repository. It gets in the way of\r\nlinting and editors pick it up as a regular source file by accident.",
+
        "sha1": "a57846bbc8ced6587bf8329fc4bce970eb7b757e",
+
        "summary": "Remove src/Folder.svelte (#3)"
+
      },
+
      "oid": "ed52e9f8dfe1d8b374b2a118c25235349a743dd2"
    });
    assert_eq!(serde_json::to_value(tree).unwrap(), expected)
}
+

+
#[test]
+
fn repo_tree() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+
    let tree = repo
+
        .tree("27acd68c7504755aa11023300890bb85bbd69d45", &"src")
+
        .unwrap();
+
    assert_eq!(tree.entries().len(), 3);
+

+
    let commit_header = tree.commit();
+
    assert_eq!(
+
        commit_header.sha1.to_string(),
+
        "e24124b7538658220b5aaf3b6ef53758f0a106dc"
+
    );
+

+
    let tree_oid = tree.object_id();
+
    assert_eq!(
+
        tree_oid.to_string(),
+
        "dbd5d80c64a00969f521b96401a315e9481e9561"
+
    );
+

+
    let entries = tree.entries();
+
    assert_eq!(entries.len(), 3);
+
    let entry = &entries[0];
+
    assert!(!entry.is_tree());
+
    assert_eq!(entry.name(), "Eval.hs");
+
    assert_eq!(
+
        entry.object_id().to_string(),
+
        "8c7447d13b907aa994ac3a38317c1e9633bf0732"
+
    );
+
    let commit = entry.commit();
+
    assert_eq!(
+
        commit.sha1.to_string(),
+
        "e24124b7538658220b5aaf3b6ef53758f0a106dc"
+
    );
+

+
    // Verify that an empty path works for getting the root tree.
+
    let root_tree = repo
+
        .tree("27acd68c7504755aa11023300890bb85bbd69d45", &"")
+
        .unwrap();
+
    assert_eq!(root_tree.entries().len(), 8);
+
}
+

+
#[test]
+
fn repo_blob() {
+
    let repo = Repository::open(GIT_PLATINUM).unwrap();
+
    let blob = repo
+
        .blob("27acd68c7504755aa11023300890bb85bbd69d45", &"src/memory.rs")
+
        .unwrap();
+

+
    let blob_oid = blob.object_id();
+
    assert_eq!(
+
        blob_oid.to_string(),
+
        "b84992d24be67536837f5ab45a943f1b3f501878"
+
    );
+

+
    let commit_header = blob.commit();
+
    assert_eq!(
+
        commit_header.sha1.to_string(),
+
        "e24124b7538658220b5aaf3b6ef53758f0a106dc"
+
    );
+

+
    assert!(!blob.is_binary());
+

+
    // Verify the blob content size matches with the file size of "memory.rs"
+
    let content = blob.content();
+
    assert_eq!(content.size(), 6253);
+

+
    // Verify as_bytes.
+
    assert_eq!(content.as_bytes().len(), content.size());
+
}