diff --git a/radicle-surf/Cargo.toml b/radicle-surf/Cargo.toml index e39c8fc..fc8eece 100644 --- a/radicle-surf/Cargo.toml +++ b/radicle-surf/Cargo.toml @@ -48,6 +48,10 @@ version = "0.2.0" path = "../radicle-git-ext" features = ["serde"] +[dependencies.radicle-std-ext] +version = "0.1.0" +path = "../radicle-std-ext" + [build-dependencies] anyhow = "1.0" flate2 = "1" diff --git a/radicle-surf/examples/browsing.rs b/radicle-surf/examples/browsing.rs index f2a00d0..91c9c19 100644 --- a/radicle-surf/examples/browsing.rs +++ b/radicle-surf/examples/browsing.rs @@ -25,7 +25,7 @@ //! the directories in a tree-like structure. use radicle_surf::{ - file_system::{Directory, DirectoryEntry}, + file_system::{directory, Directory}, git::Repository, }; use std::{env, time::Instant}; @@ -51,10 +51,10 @@ fn main() { fn print_directory(d: &Directory, repo: &Repository, indent_level: usize) { let indent = " ".repeat(indent_level * 4); println!("{}{}/", &indent, d.name()); - for entry in d.contents(repo).unwrap().iter() { + for entry in d.entries(repo).unwrap().entries() { match entry { - DirectoryEntry::File(f) => println!(" {}{}", &indent, f.name()), - DirectoryEntry::Directory(d) => print_directory(d, repo, indent_level + 1), + directory::Entry::File(f) => println!(" {}{}", &indent, f.name()), + directory::Entry::Directory(d) => print_directory(d, repo, indent_level + 1), } } } diff --git a/radicle-surf/src/file_system.rs b/radicle-surf/src/file_system.rs index f492d78..5943f94 100644 --- a/radicle-surf/src/file_system.rs +++ b/radicle-surf/src/file_system.rs @@ -111,8 +111,9 @@ //! ``` pub mod directory; +pub use directory::{Directory, Entries, Entry, File, FileContent}; mod error; pub use error::Error; mod path; -pub use self::{directory::*, path::*}; +pub use self::path::*; diff --git a/radicle-surf/src/file_system/directory.rs b/radicle-surf/src/file_system/directory.rs index 572494d..8fbf11f 100644 --- a/radicle-surf/src/file_system/directory.rs +++ b/radicle-surf/src/file_system/directory.rs @@ -20,55 +20,114 @@ //! A `Directory` is expected to be a non-empty tree of directories and files. //! See [`Directory`] for more information. -use crate::{ - file_system::{error::LabelError, path::*, Error}, - git::{self, Repository, Revision}, -}; -use git2::Blob; -use radicle_git_ext::Oid; use std::{ collections::BTreeMap, - convert::{Infallible, TryFrom}, + convert::{Infallible, Into}, path, }; -/// Represents a `file` in a git repo. +use git2::Blob; +use radicle_git_ext::{is_not_found_err, Oid}; +use radicle_std_ext::result::ResultExt as _; + +use crate::{ + file_system::{path::*, Error}, + git::{Repository, Revision}, +}; + +pub mod error { + use thiserror::Error; + + #[derive(Debug, Error, PartialEq)] + pub enum Directory { + #[error(transparent)] + Git(#[from] git2::Error), + #[error(transparent)] + Entry(#[from] Entry), + #[error(transparent)] + File(#[from] File), + } + + #[derive(Debug, Error, PartialEq, Eq)] + pub enum Entry { + #[error("the entry name was not valid UTF-8")] + Utf8Error, + #[error(transparent)] + Label(#[from] super::Error), + } + + #[derive(Debug, Error, PartialEq)] + pub enum File { + #[error(transparent)] + Git(#[from] git2::Error), + } +} + +/// A `File` in a git repository. +/// +/// The representation is lightweight and contains the [`Oid`] that +/// points to the git blob which is this file. +/// +/// The name of a file can be retrieved via [`File::name`]. +/// +/// The [`FileContent`] of a file can be retrieved via +/// [`File::content`]. #[derive(Clone, PartialEq, Eq, Debug)] pub struct File { pub(crate) name: Label, - pub(crate) oid: Oid, + pub(crate) id: Oid, } impl File { - /// Create a new `File`. - pub fn new(name: String, oid: Oid) -> Self { - File { + /// The name of this `File`. + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// The object identifier of this `File`. + pub fn id(&self) -> Oid { + self.id + } + + /// Create a new `File` with the `name` and `id` provided. + /// + /// The `id` must point to a `git` blob. + pub fn new(name: String, id: Oid) -> Self { + Self { name: Label { label: name, hidden: false, }, - oid, + id, } } - /// Returns the file name reference. - pub fn name(&self) -> &str { - self.name.as_str() + /// Get the [`FileContent`] for this `File`. + /// + /// # Errors + /// + /// This function will fail if it could not find the `git` blob + /// for the `Oid` of this `File`. + pub fn content<'a>(&self, repo: &'a Repository) -> Result, error::File> { + let blob = repo.git2_repo().find_blob(self.id.into())?; + Ok(FileContent { blob }) } } -/// Represents the actual content of a file. +/// The contents of a [`File`]. +/// +/// To construct a `FileContent` use [`File::content`]. pub struct FileContent<'a> { blob: Blob<'a>, } impl<'a> FileContent<'a> { - /// Returns the file content as a byte slice. + /// Return the file contents as a byte slice. pub fn as_bytes(&self) -> &[u8] { self.blob.content() } - /// Returns the size of file + /// Return the size of the file contents. pub fn size(&self) -> usize { self.blob.size() } @@ -79,190 +138,176 @@ impl<'a> FileContent<'a> { } } -/// Represents the listing of a directory. -pub struct DirectoryContent { - listing: BTreeMap, +/// A representations of a [`Directory`]'s entries. +pub struct Entries { + listing: BTreeMap, } -impl DirectoryContent { - /// Returns an iterator for the listing of a directory. - pub fn iter(&self) -> impl Iterator { +impl Entries { + /// Return the [`Label`]s of each [`Entry`]. + pub fn names(&self) -> impl Iterator { + self.listing.keys() + } + + /// Return each [`Entry`]. + pub fn entries(&self) -> impl Iterator { self.listing.values() } + + /// Return each [`Label`] and [`Entry`]. + pub fn iter(&self) -> impl Iterator { + self.listing.iter() + } } -/// Represents an entry in a directory. +/// An `Entry` is either a [`File`] entry or a [`Directory`] entry. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum DirectoryEntry { - /// When the entry is a file. +pub enum Entry { + /// A file entry within a [`Directory`]. File(File), - /// When the entry is a directory. + /// A sub-directory of a [`Directory`]. Directory(Directory), } -impl DirectoryEntry { - /// Returns the label (short name, without the parent path) of the entry. +impl Entry { + /// Get a label for the `Entriess`, either the name of the [`File`] + /// or the name of the [`Directory`]. pub fn label(&self) -> &Label { match self { - DirectoryEntry::File(file) => &file.name, - DirectoryEntry::Directory(directory) => directory.name(), + Entry::File(file) => &file.name, + Entry::Directory(directory) => directory.name(), } } + + pub(crate) fn from_entry(entry: &git2::TreeEntry) -> Result, error::Entry> { + let name = Label { + label: entry.name().ok_or(error::Entry::Utf8Error)?.to_string(), + hidden: false, + }; + let id = entry.id().into(); + Ok(entry.kind().and_then(|kind| match kind { + git2::ObjectType::Tree => Some(Self::Directory(Directory { name, id })), + git2::ObjectType::Blob => Some(Self::File(File { name, id })), + _ => None, + })) + } } -/// A `Directory` is a set of entries of sub-directories and files, ordered -/// by their unique names in the alphabetical order. +/// A `Directory` is the representation of a file system directory, for a given +/// [`git` tree][git-tree]. +/// +/// The name of a directory can be retrieved via [`File::name`]. +/// +/// The [`Entries`] of a directory can be retrieved via +/// [`Directory::entries`]. +/// +/// [git-tree]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects #[derive(Debug, Clone, PartialEq, Eq)] pub struct Directory { pub(crate) name: Label, - pub(crate) oid: Oid, + pub(crate) id: Oid, } impl Directory { - /// Creates a directory given `name` and `oid`, with empty contents. - pub fn new(name: Label, oid: Oid) -> Self { - Directory { name, oid } - } - /// Get the name of the current `Directory`. pub fn name(&self) -> &Label { &self.name } - /// Returns a `DirectoryContent` for the current directory. - pub fn contents(&self, repo: &Repository) -> Result { - let listing = repo.directory_get(self)?; - Ok(DirectoryContent { listing }) + /// Creates a directory given its `name` and `id`. + /// + /// The `id` must point to a `git` tree. + pub fn new(name: Label, id: Oid) -> Self { + Self { name, id } } - /// Retrieves a `DirectoryEntry` for `path` in `repo`. - pub fn get_path( + /// 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 { + let tree = repo.git2_repo().find_tree(self.id.into())?; + + let mut entries = BTreeMap::new(); + let mut error = None; + + // Walks only the first level of entries. + tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| { + match Entry::from_entry(entry) { + Ok(Some(entry)) => match entry { + Entry::File(_) => { + entries.insert(entry.label().clone(), entry); + git2::TreeWalkResult::Ok + }, + Entry::Directory(_) => { + entries.insert(entry.label().clone(), entry); + // Skip nested directories + git2::TreeWalkResult::Skip + }, + }, + Ok(None) => git2::TreeWalkResult::Skip, + Err(err) => { + error = Some(err); + git2::TreeWalkResult::Abort + }, + } + })?; + + match error { + Some(err) => Err(err.into()), + None => Ok(Entries { listing: entries }), + } + } + + /// Find the [`Entry`] found at `path`, if it exists. + pub fn find_entry( &self, path: &path::Path, repo: &Repository, - ) -> Result { + ) -> Result, crate::git::Error> { // Search the path in git2 tree. - let git2_tree = repo.git2_repo().find_tree(self.oid.into())?; - let entry = git2_tree.get_path(path)?; + let git2_tree = repo.git2_repo().find_tree(self.id.into())?; + let entry = git2_tree + .get_path(path) + .map(Some) + .or_matches::(is_not_found_err, || Ok(None))?; - // Construct the DirectoryEntry. - let name = entry.name().ok_or_else(|| { - Error::Label(LabelError::InvalidUTF8 { - label: String::from_utf8_lossy(entry.name_bytes()).into(), - }) - })?; - let label = Label { - label: name.to_string(), - hidden: false, - }; - let oid: Oid = entry.id().into(); - match entry.kind() { - Some(git2::ObjectType::Tree) => { - let dir = Directory::new(label, oid); - Ok(DirectoryEntry::Directory(dir)) - }, - Some(git2::ObjectType::Blob) => { - let f = File { name: label, oid }; - Ok(DirectoryEntry::File(f)) - }, - _ => { - let file_path = Path::try_from(path.to_path_buf())?; - Err(git::Error::PathNotFound(file_path)) - }, - } + Ok(entry + .and_then(|entry| Entry::from_entry(&entry).transpose()) + .transpose() + .unwrap()) } - /// Find a [`File`] in the directory given the [`Path`] to the [`File`]. - /// - /// # Failures - /// - /// This operation fails if the path does not lead to a [`File`]. - /// If the search is for a `Directory` then use `find_directory`. - /// - /// # Examples - /// - /// Search for a file in the path: - /// * `foo/bar/baz.hs` - /// * `foo` - /// * `foo/bar/qux.rs` - /// - /// ``` - /// use radicle_surf::file_system::{Directory, File}; - /// use radicle_surf::file_system::unsound; - /// - /// let file = File::new(b"module Banana ..."); - /// - /// let mut directory = Directory::root(); - /// directory.insert_file(unsound::path::new("foo/bar/baz.rs"), file.clone()); - /// - /// // The file is succesfully found - /// assert_eq!(directory.find_file(unsound::path::new("foo/bar/baz.rs")), Some(file)); - /// - /// // We shouldn't be able to find a directory - /// assert_eq!(directory.find_file(unsound::path::new("foo")), None); - /// - /// // We shouldn't be able to find a file that doesn't exist - /// assert_eq!(directory.find_file(unsound::path::new("foo/bar/qux.rs")), None); - /// ``` - pub fn find_file(&self, path: Path, repo: &Repository) -> Option { + /// Find the `Oid`, for a [`File`], found at `path`, if it exists. + pub fn find_file( + &self, + path: Path, + repo: &Repository, + ) -> Result, crate::git::Error> { let path_buf: std::path::PathBuf = (&path).into(); - let entry = match self.get_path(path_buf.as_path(), repo) { - Ok(entry) => entry, - Err(_) => return None, - }; - - match entry { - DirectoryEntry::File(f) => Some(f.oid), - DirectoryEntry::Directory(_) => None, - } + Ok(match self.find_entry(path_buf.as_path(), repo)? { + Some(Entry::File(f)) => Some(f.id), + _ => None, + }) } - /// Find a `Directory` in the directory given the [`Path`] to the - /// `Directory`. - /// - /// # Failures - /// - /// This operation fails if the path does not lead to the `Directory`. - /// - /// # Examples - /// - /// Search for directories in the path: - /// * `foo` - /// * `foo/bar` - /// * `foo/baz` - /// - /// ``` - /// use radicle_surf::file_system::{Directory, File}; - /// use radicle_surf::file_system::unsound; - /// - /// let file = File::new(b"module Banana ..."); - /// - /// let mut directory = Directory::root(); - /// directory.insert_file(unsound::path::new("foo/bar/baz.rs"), file.clone()); - /// - /// // Can find the first level - /// assert!(directory.find_directory(unsound::path::new("foo")).is_some()); - /// - /// // Can find the second level - /// assert!(directory.find_directory(unsound::path::new("foo/bar")).is_some()); - /// - /// // Cannot find 'baz' since it does not exist - /// assert!(directory.find_directory(unsound::path::new("foo/baz")).is_none()); - /// - /// // 'baz.rs' is a file and not a directory - /// assert!(directory.find_directory(unsound::path::new("foo/bar/baz.rs")).is_none()); - /// ``` - pub fn find_directory(&self, path: Path, repo: &Repository) -> Option { + /// Find the `Directory` found at `path`, if it exists. + pub fn find_directory( + &self, + path: Path, + repo: &Repository, + ) -> Result, crate::git::Error> { let path_buf: std::path::PathBuf = (&path).into(); - let entry = match self.get_path(path_buf.as_path(), repo) { - Ok(entry) => entry, - Err(_) => return None, - }; - - match entry { - DirectoryEntry::File(_) => None, - DirectoryEntry::Directory(d) => Some(d), - } + Ok(match self.find_entry(path_buf.as_path(), repo)? { + Some(Entry::Directory(d)) => Some(d), + _ => None, + }) } // TODO(fintan): This is going to be a bit trickier so going to leave it out for @@ -274,21 +319,43 @@ impl Directory { /// Get the total size, in bytes, of a `Directory`. The size is /// the sum of all files that can be reached from this `Directory`. - pub fn size(&self, repo: &Repository) -> Result { - let mut size = 0; - let contents = self.contents(repo)?; - for item in contents.iter() { - match item { - DirectoryEntry::File(f) => { - let sz = repo.file_size(f.oid)?; - size += sz; + pub fn size(&self, repo: &Repository) -> Result { + 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( + &self, + repo: &Repository, + initial: B, + f: &mut F, + ) -> Result + where + Error: From, + F: FnMut(B, &Entry) -> Result, + { + 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) }, - DirectoryEntry::Directory(d) => { - size += d.size(repo)?; - }, - } - } - Ok(size) + }) } } @@ -296,6 +363,6 @@ impl Revision for Directory { type Error = Infallible; fn object_id(&self, _repo: &Repository) -> Result { - Ok(self.oid) + Ok(self.id) } } diff --git a/radicle-surf/src/git/repo.rs b/radicle-surf/src/git/repo.rs index 5216453..faa97c7 100644 --- a/radicle-surf/src/git/repo.rs +++ b/radicle-surf/src/git/repo.rs @@ -15,14 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{ - collections::{BTreeMap, BTreeSet}, - convert::TryFrom, - path::PathBuf, - str, -}; +use std::{collections::BTreeSet, convert::TryFrom, path::PathBuf, str}; -use directory::{Directory, FileContent}; use git_ref_format::{refspec::QualifiedPattern, Qualified}; use radicle_git_ext::Oid; use thiserror::Error; @@ -30,7 +24,7 @@ use thiserror::Error; use crate::{ diff::{self, *}, file_system, - file_system::{directory, DirectoryEntry, Label}, + file_system::{directory::FileContent, Directory, Label}, git::{ commit, glob, @@ -195,60 +189,10 @@ impl Repository { let tree = git2_commit.as_object().peel_to_tree()?; Ok(Directory { name: Label::root(), - oid: tree.id().into(), + id: tree.id().into(), }) } - /// Retrieves the content of a directory. - pub(crate) fn directory_get( - &self, - d: &Directory, - ) -> Result, Error> { - let git2_tree = self.inner.find_tree(d.oid.into())?; - let map = self.tree_first_level(git2_tree)?; - Ok(map) - } - - /// Returns a map of the first level entries in `tree`. - fn tree_first_level(&self, tree: git2::Tree) -> Result, Error> { - let mut map = BTreeMap::new(); - - // Walks only the first level of entries. - tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| { - let oid = entry.id().into(); - let label = match entry.name() { - Some(name) => match name.parse::