diff --git a/radicle-surf/examples/browsing.rs b/radicle-surf/examples/browsing.rs new file mode 100644 index 0000000..d1e72a0 --- /dev/null +++ b/radicle-surf/examples/browsing.rs @@ -0,0 +1,66 @@ +// This file is part of radicle-surf +// +// +// Copyright (C) 2019-2020 The Radicle Team +// +// 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 . + +//! An example of browsing a git repo using `radicle-surf`. +//! +//! How to run: +//! +//! cargo run --example browsing +//! +//! This program browses the given repo and prints out the files and +//! the directories in a tree-like structure. + +use radicle_surf::{ + file_system::{Directory, DirectoryEntry}, + git::{Repository, RepositoryRef}, +}; +use std::{env, time::Instant}; + +fn main() { + let repo_path = match env::args().nth(1) { + Some(path) => path, + None => { + print_usage(); + return; + }, + }; + let repo = Repository::discover(&repo_path).unwrap(); + let repo = repo.as_ref(); + let now = Instant::now(); + let head = repo.head_oid().unwrap(); + let root = repo.root_dir(head).unwrap(); + print_directory(&root, &repo, 0); + + let elapsed_millis = now.elapsed().as_millis(); + println!("browse with print: {} ms", elapsed_millis); +} + +fn print_directory(d: &Directory, repo: &RepositoryRef, indent_level: usize) { + let indent = " ".repeat(indent_level * 4); + println!("{}{}/", &indent, d.name()); + for entry in d.contents(repo).unwrap().iter() { + match entry { + DirectoryEntry::File(f) => println!(" {}{}", &indent, f.name()), + DirectoryEntry::Directory(d) => print_directory(d, repo, indent_level + 1), + } + } +} + +fn print_usage() { + println!("Usage:"); + println!("cargo run --example browsing "); +} diff --git a/radicle-surf/src/diff.rs b/radicle-surf/src/diff.rs index 55418ee..1f7a402 100644 --- a/radicle-surf/src/diff.rs +++ b/radicle-surf/src/diff.rs @@ -17,12 +17,15 @@ #![allow(dead_code, unused_variables, missing_docs)] -use std::{cell::RefCell, cmp::Ordering, convert::TryFrom, ops::Deref, rc::Rc, slice}; +use std::{convert::TryFrom, slice}; #[cfg(feature = "serialize")] use serde::{ser, Serialize, Serializer}; -use crate::file_system::{Directory, DirectoryContents, Path}; +use crate::{ + file_system::{Directory, Path}, + vcs::git::{Error, RepositoryRef}, +}; pub mod git; @@ -293,14 +296,7 @@ impl Diff { // TODO: Direction of comparison is not obvious with this signature. // For now using conventional approach with the right being "newer". #[allow(clippy::self_named_constructors)] - pub fn diff(left: Directory, right: Directory) -> Self { - let mut diff = Diff::new(); - let path = Rc::new(RefCell::new(Path::from_labels( - right.current().clone(), - &[], - ))); - Diff::collect_diff(&left, &right, &path, &mut diff); - + pub fn diff(left: Directory, right: Directory, repo: RepositoryRef) -> Result { // TODO: Some of the deleted files may actually be moved (renamed) to one of the // created files. Finding out which of the deleted files were deleted // and which were moved will probably require performing some variant of @@ -308,180 +304,7 @@ impl Diff { // decision can be based on heuristics, e.g. the file can be considered // moved, if len(LCS) > 0,25 * min(size(d), size(c)), and // deleted otherwise. - - diff - } - - fn collect_diff( - old: &Directory, - new: &Directory, - parent_path: &Rc>, - diff: &mut Diff, - ) { - let mut old_iter = old.contents(); - let mut new_iter = new.contents(); - let mut old_entry_opt = old_iter.next(); - let mut new_entry_opt = new_iter.next(); - - while old_entry_opt.is_some() || new_entry_opt.is_some() { - match (&old_entry_opt, &new_entry_opt) { - (Some(ref old_entry), Some(ref new_entry)) => { - match new_entry.label().cmp(old_entry.label()) { - Ordering::Greater => { - diff.add_deleted_files(old_entry, parent_path); - old_entry_opt = old_iter.next(); - }, - Ordering::Less => { - diff.add_created_files(new_entry, parent_path); - new_entry_opt = new_iter.next(); - }, - Ordering::Equal => match (new_entry, old_entry) { - ( - DirectoryContents::File { - name: new_file_name, - file: new_file, - }, - DirectoryContents::File { - name: old_file_name, - file: old_file, - }, - ) => { - if old_file.size != new_file.size - || old_file.checksum() != new_file.checksum() - { - let mut path = parent_path.borrow().clone(); - path.push(new_file_name.clone()); - - diff.add_modified_file(path, vec![], None); - } - old_entry_opt = old_iter.next(); - new_entry_opt = new_iter.next(); - }, - ( - DirectoryContents::File { - name: new_file_name, - file: new_file, - }, - DirectoryContents::Directory(old_dir), - ) => { - let mut path = parent_path.borrow().clone(); - path.push(new_file_name.clone()); - - diff.add_created_file( - path, - FileDiff::Plain { - hunks: Hunks::default(), - }, - ); - diff.add_deleted_files(old_entry, parent_path); - - old_entry_opt = old_iter.next(); - new_entry_opt = new_iter.next(); - }, - ( - DirectoryContents::Directory(new_dir), - DirectoryContents::File { - name: old_file_name, - file: old_file, - }, - ) => { - let mut path = parent_path.borrow().clone(); - path.push(old_file_name.clone()); - - diff.add_created_files(new_entry, parent_path); - diff.add_deleted_file( - path, - FileDiff::Plain { - hunks: Hunks::default(), - }, - ); - - old_entry_opt = old_iter.next(); - new_entry_opt = new_iter.next(); - }, - ( - DirectoryContents::Directory(new_dir), - DirectoryContents::Directory(old_dir), - ) => { - parent_path.borrow_mut().push(new_dir.current().clone()); - Diff::collect_diff( - old_dir.deref(), - new_dir.deref(), - parent_path, - diff, - ); - parent_path.borrow_mut().pop(); - old_entry_opt = old_iter.next(); - new_entry_opt = new_iter.next(); - }, - }, - } - }, - (Some(old_entry), None) => { - diff.add_deleted_files(old_entry, parent_path); - old_entry_opt = old_iter.next(); - }, - (None, Some(new_entry)) => { - diff.add_created_files(new_entry, parent_path); - new_entry_opt = new_iter.next(); - }, - (None, None) => break, - } - } - } - - // if entry is a file, then return this file, - // or a list of files in the directory tree otherwise - fn collect_files_from_entry( - entry: &DirectoryContents, - parent_path: &Rc>, - mapper: F, - ) -> Vec - where - F: Fn(Path) -> T + Copy, - { - match entry { - DirectoryContents::Directory(dir) => Diff::collect_files(dir, parent_path, mapper), - DirectoryContents::File { name, .. } => { - let mut path = parent_path.borrow().clone(); - path.push(name.clone()); - - vec![mapper(path)] - }, - } - } - - fn collect_files(dir: &Directory, parent_path: &Rc>, mapper: F) -> Vec - where - F: Fn(Path) -> T + Copy, - { - let mut files: Vec = Vec::new(); - Diff::collect_files_inner(dir, parent_path, mapper, &mut files); - files - } - - fn collect_files_inner<'a, F, T>( - dir: &'a Directory, - parent_path: &Rc>, - mapper: F, - files: &mut Vec, - ) where - F: Fn(Path) -> T + Copy, - { - parent_path.borrow_mut().push(dir.current().clone()); - for entry in dir.contents() { - match entry { - DirectoryContents::Directory(subdir) => { - Diff::collect_files_inner(subdir, parent_path, mapper, files); - }, - DirectoryContents::File { name, .. } => { - let mut path = parent_path.borrow().clone(); - path.push(name.clone()); - files.push(mapper(path)); - }, - } - } - parent_path.borrow_mut().pop(); + repo.diff(left, right) } pub(crate) fn add_modified_file( @@ -522,32 +345,10 @@ impl Diff { self.created.push(CreateFile { path, diff }); } - fn add_created_files(&mut self, dc: &DirectoryContents, parent_path: &Rc>) { - let mut new_files: Vec = - Diff::collect_files_from_entry(dc, parent_path, |path| CreateFile { - path, - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - }); - self.created.append(&mut new_files); - } - pub(crate) fn add_deleted_file(&mut self, path: Path, diff: FileDiff) { self.deleted.push(DeleteFile { path, diff }); } - fn add_deleted_files(&mut self, dc: &DirectoryContents, parent_path: &Rc>) { - let mut new_files: Vec = - Diff::collect_files_from_entry(dc, parent_path, |path| DeleteFile { - path, - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - }); - self.deleted.append(&mut new_files); - } - pub fn stats(&self) -> Stats { let mut deletions = 0; let mut insertions = 0; diff --git a/radicle-surf/src/file_system/directory.rs b/radicle-surf/src/file_system/directory.rs index e332a52..042a439 100644 --- a/radicle-surf/src/file_system/directory.rs +++ b/radicle-surf/src/file_system/directory.rs @@ -19,83 +19,90 @@ //! //! A `Directory` is expected to be a non-empty tree of directories and files. //! See [`Directory`] for more information. -//! -//! As well as this, this module contains [`DirectoryContents`] which is the -//! output of iterating over a [`Directory`]. -use crate::file_system::path::*; -use nonempty::NonEmpty; -use std::{ - collections::{hash_map::DefaultHasher, BTreeMap}, - hash::{Hash, Hasher}, +use crate::{ + file_system::{error::LabelError, path::*, Error}, + vcs::git::{self, RepositoryRef, Revision}, }; +use git2::Blob; +use radicle_git_ext::Oid; +use std::{collections::BTreeMap, convert::TryFrom, path}; -/// A `File` consists of its file contents (a [`Vec`] of bytes). -/// -/// The `Debug` instance of `File` will show the first few bytes of the file and -/// its [`size`](#method.size). -#[derive(Clone, PartialEq, Eq)] +/// Represents a `file` in a git repo. +#[derive(Clone, PartialEq, Eq, Debug)] pub struct File { - /// The contents of a `File` as a vector of bytes. - pub contents: Vec, - pub(crate) size: usize, -} - -impl std::fmt::Debug for File { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut contents = self.contents.clone(); - contents.truncate(10); - write!( - f, - "File {{ contents: {:?}, size: {} }}", - contents, self.size - ) - } + pub(crate) name: Label, + pub(crate) oid: Oid, } impl File { - /// Create a new `File` with the contents provided. - pub fn new(contents: &[u8]) -> Self { - let size = contents.len(); + /// Create a new `File`. + pub fn new(name: String, oid: Oid) -> Self { File { - contents: contents.to_vec(), - size, + name: Label { + label: name, + hidden: false, + }, + oid, } } - /// Get the size of the `File` corresponding to the number of bytes in the - /// file contents. - /// - /// # Examples - /// - /// ``` - /// use radicle_surf::file_system::File; - /// - /// let file = File::new(b"pub mod diff;\npub mod file_system;\npub mod vcs;\npub use crate::vcs::git;\n"); - /// - /// assert_eq!(file.size(), 73); - /// ``` - pub fn size(&self) -> usize { - self.size + /// Returns the file name reference. + pub fn name(&self) -> &str { + self.name.as_str() + } +} + +/// Represents the actual content of a file. +pub struct FileContent<'a> { + blob: Blob<'a>, +} + +impl<'a> FileContent<'a> { + /// Returns the file content as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + self.blob.content() } - /// Get the hash of the `File` corresponding to the contents of the file. - /// - /// # Examples - /// - /// ``` - /// use radicle_surf::file_system::File; - /// - /// let file = File::new( - /// b"pub mod diff;\npub mod file_system;\npub mod vcs;\npub use crate::vcs::git;\n", - /// ); - /// - /// assert_eq!(file.checksum(), 8457766712413557403); - /// ``` - pub fn checksum(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.contents.hash(&mut hasher); - hasher.finish() + /// Returns the size of file + pub fn size(&self) -> usize { + self.blob.size() + } + + /// Creates a `FileContent` using a blob. + pub(crate) fn new(blob: Blob<'a>) -> Self { + Self { blob } + } +} + +/// Represents the listing of a directory. +pub struct DirectoryContent { + listing: BTreeMap, +} + +impl DirectoryContent { + /// Returns an iterator for the listing of a directory. + pub fn iter(&self) -> impl Iterator { + self.listing.values() + } +} + +/// Represents an entry in a directory. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DirectoryEntry { + /// When the entry is a file. + File(File), + /// When the entry is a directory. + Directory(Directory), +} + +impl DirectoryEntry { + /// Returns the label (short name, without the parent path) of the entry. + pub fn label(&self) -> &Label { + match self { + DirectoryEntry::File(file) => &file.name, + DirectoryEntry::Directory(directory) => directory.name(), + } } } @@ -103,55 +110,14 @@ impl File { /// by their unique names in the alphabetical order. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Directory { - name: Label, - contents: BTreeMap, -} - -/// `DirectoryContents` is an enumeration of what a [`Directory`] can contain -/// and is used for when we are iterating through a [`Directory`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DirectoryContents { - /// The `File` variant contains the file's name and the [`File`] itself. - File { - /// The name of the file. - name: Label, - /// The file data. - file: File, - }, - /// The `Directory` variant contains a sub-directory to the current one. - Directory(Directory), -} - -impl DirectoryContents { - /// Get a label for the `DirectoryContents`, either the name of the [`File`] - /// or the name of the [`Directory`]. - pub fn label(&self) -> &Label { - match self { - DirectoryContents::File { name, .. } => name, - DirectoryContents::Directory(directory) => directory.current(), - } - } + pub(crate) name: Label, + pub(crate) oid: Oid, } impl Directory { - /// Create a root directory. - /// - /// This function is usually used for testing and demonstation purposes. - pub fn root() -> Self { - Directory { - name: Label::root(), - contents: BTreeMap::new(), - } - } - - /// Create a directory, similar to `root`, except with a given name. - /// - /// This function is usually used for testing and demonstation purposes. - pub fn new(name: Label) -> Self { - Directory { - name, - contents: BTreeMap::new(), - } + /// Creates a directory given `name` and `oid`, with empty contents. + pub fn new(name: Label, oid: Oid) -> Self { + Directory { name, oid } } /// Get the name of the current `Directory`. @@ -159,19 +125,47 @@ impl Directory { &self.name } - /// Add the `content` under `name` to the current `Directory`. - /// If `name` already exists in this directory, then the previous contents - /// are replaced. - pub fn insert(&mut self, name: Label, content: DirectoryContents) { - self.contents.insert(name, content); + /// Returns a `DirectoryContent` for the current directory. + pub fn contents(&self, repo: &RepositoryRef) -> Result { + let listing = repo.directory_get(self)?; + Ok(DirectoryContent { listing }) } - /// Returns an iterator for the contents of the current directory. - /// - /// Note that the returned iterator only iterates the current level, - /// without going recursively into sub-directories. - pub fn contents(&self) -> impl Iterator { - self.contents.values() + /// Retrieves a `DirectoryEntry` for `path` in `repo`. + pub fn get_path( + &self, + path: &path::Path, + repo: &RepositoryRef, + ) -> Result { + // Search the path in git2 tree. + let git2_tree = repo.repo_ref.find_tree(self.oid.into())?; + let entry = git2_tree.get_path(path)?; + + // Construct the DirectoryEntry. + let name = entry.name().ok_or_else(|| { + Error::Label(LabelError::InvalidUTF8 { + label: String::from_utf8_lossy(entry.name_bytes()).into(), + }) + })?; + let label = Label { + label: name.to_string(), + hidden: false, + }; + let oid: Oid = entry.id().into(); + match entry.kind() { + Some(git2::ObjectType::Tree) => { + let dir = Directory::new(label, oid); + Ok(DirectoryEntry::Directory(dir)) + }, + Some(git2::ObjectType::Blob) => { + let f = File { name: label, oid }; + Ok(DirectoryEntry::File(f)) + }, + _ => { + let file_path = Path::try_from(path.to_path_buf())?; + Err(git::Error::PathNotFound(file_path)) + }, + } } /// Find a [`File`] in the directory given the [`Path`] to the [`File`]. @@ -206,23 +200,17 @@ impl Directory { /// // We shouldn't be able to find a file that doesn't exist /// assert_eq!(directory.find_file(unsound::path::new("foo/bar/qux.rs")), None); /// ``` - pub fn find_file(&self, path: Path) -> Option<&File> { - let mut contents = &self.contents; - let path_depth = path.0.len(); - for (idx, label) in path.iter().enumerate() { - match contents.get(label) { - Some(DirectoryContents::Directory(d)) => contents = &d.contents, - Some(DirectoryContents::File { name: _, file }) => { - if idx + 1 == path_depth { - return Some(file); - } else { - break; // Abort: finding a file before the last label. - } - }, - None => break, // Abort: a label not found. - } + pub fn find_file(&self, path: Path, repo: &RepositoryRef) -> Option { + let path_buf: std::path::PathBuf = (&path).into(); + let entry = match self.get_path(path_buf.as_path(), repo) { + Ok(entry) => entry, + Err(_) => return None, + }; + + match entry { + DirectoryEntry::File(f) => Some(f.oid), + DirectoryEntry::Directory(_) => None, } - None } /// Find a `Directory` in the directory given the [`Path`] to the @@ -260,46 +248,17 @@ impl Directory { /// // 'baz.rs' is a file and not a directory /// assert!(directory.find_directory(unsound::path::new("foo/bar/baz.rs")).is_none()); /// ``` - pub fn find_directory(&self, path: Path) -> Option<&Self> { - let mut found = None; - let mut contents = &self.contents; + pub fn find_directory(&self, path: Path, repo: &RepositoryRef) -> Option { + let path_buf: std::path::PathBuf = (&path).into(); + let entry = match self.get_path(path_buf.as_path(), repo) { + Ok(entry) => entry, + Err(_) => return None, + }; - for label in path.iter() { - match contents.get(label) { - Some(DirectoryContents::Directory(d)) => { - found = Some(d); - contents = &d.contents; - }, - Some(DirectoryContents::File { .. }) => break, // Abort: should not be a file. - None => break, // Abort: a label not found. - } + match entry { + DirectoryEntry::File(_) => None, + DirectoryEntry::Directory(d) => Some(d), } - - found - } - - /// Get the [`Label`] of the current directory. - /// - /// # Examples - /// - /// ``` - /// use radicle_surf::file_system::{Directory, File, Label}; - /// use radicle_surf::file_system::unsound; - /// - /// let mut root = Directory::root(); - /// root.insert_file(unsound::path::new("main.rs"), File::new(b"println!(\"Hello, world!\")")); - /// root.insert_file(unsound::path::new("lib.rs"), File::new(b"struct Hello(String)")); - /// root.insert_file(unsound::path::new("test/mod.rs"), File::new(b"assert_eq!(1 + 1, 2);")); - /// - /// assert_eq!(root.current(), Label::root()); - /// - /// let test = root.find_directory( - /// unsound::path::new("test") - /// ).expect("Missing test directory"); - /// assert_eq!(test.current(), unsound::label::new("test")); - /// ``` - pub fn current(&self) -> &Label { - &self.name } // TODO(fintan): This is going to be a bit trickier so going to leave it out for @@ -311,94 +270,26 @@ impl Directory { /// Get the total size, in bytes, of a `Directory`. The size is /// the sum of all files that can be reached from this `Directory`. - /// - /// # Examples - /// - /// ``` - /// use radicle_surf::file_system::{Directory, File}; - /// use radicle_surf::file_system::unsound; - /// - /// let mut root = Directory::root(); - /// root.insert_file(unsound::path::new("main.rs"), File::new(b"println!(\"Hello, world!\")")); - /// root.insert_file(unsound::path::new("lib.rs"), File::new(b"struct Hello(String)")); - /// root.insert_file(unsound::path::new("test/mod.rs"), File::new(b"assert_eq!(1 + 1, 2);")); - /// - /// assert_eq!(root.size(), 66); - /// ``` - pub fn size(&self) -> usize { - self.contents().fold(0, |size, item| { - if let DirectoryContents::File { name: _, file } = item { - size + file.size() - } else { - size - } - }) - } - - /// Insert a file into a directory, given the full path to file (file name - /// inclusive) and the `File` itself. - /// - /// This function is usually used for testing and demonstation purposes. - pub fn insert_file(&mut self, path: Path, file: File) { - let name = path.0.last().clone(); - let f = DirectoryContents::File { - name: name.clone(), - file, - }; - - let mut contents = &mut self.contents; - let path_depth = path.0.len() - 1; // exclude the last label: file name - - if path_depth == 0 { - contents.insert(name, f); - } else { - for (idx, label) in path.iter().enumerate() { - // if label does not exist, create a sub directory. - if contents.get(label).is_none() { - let new_dir = Directory::new(label.clone()); - contents.insert(label.clone(), DirectoryContents::Directory(new_dir)); - } - - match contents.get_mut(label) { - Some(DirectoryContents::Directory(d)) => { - contents = &mut d.contents; - if idx + 1 == path_depth { - // We are in the last directory level, insert the file. - contents.insert(name, f); - return; - } - }, - Some(DirectoryContents::File { .. }) => return, // Abort: should not be a file. - None => return, - } + pub fn size(&self, repo: &RepositoryRef) -> Result { + let mut size = 0; + let contents = self.contents(repo)?; + for item in contents.iter() { + match item { + DirectoryEntry::File(f) => { + let sz = repo.file_size(f.oid)?; + size += sz; + }, + DirectoryEntry::Directory(d) => { + size += d.size(repo)?; + }, } } + Ok(size) } +} - /// Insert files into a shared directory path. - /// - /// `directory_path` is used as the prefix to where the files should go. If - /// empty the files will be placed in the current `Directory`. - /// - /// `files` are pairs of file name and the [`File`] itself. - /// - /// This function is usually used for testing and demonstation purposes. - pub fn insert_files(&mut self, directory_path: &[Label], files: NonEmpty<(Label, File)>) { - match NonEmpty::from_slice(directory_path) { - None => { - for (file_name, file) in files.into_iter() { - self.insert_file(Path::new(file_name), file) - } - }, - Some(path) => { - for (file_name, file) in files.into_iter() { - // The clone is necessary here because we use it as a prefix. - let mut file_path = Path(path.clone()); - file_path.push(file_name); - - self.insert_file(file_path, file) - } - }, - } +impl Revision for Directory { + fn object_id(&self, _repo: &RepositoryRef) -> Result { + Ok(self.oid) } } diff --git a/radicle-surf/src/object/blob.rs b/radicle-surf/src/object/blob.rs index bf2f576..756df96 100644 --- a/radicle-surf/src/object/blob.rs +++ b/radicle-surf/src/object/blob.rs @@ -136,11 +136,11 @@ where C: FnOnce(&[u8]) -> BlobContent, { let revision = maybe_revision.unwrap(); - let root = repo.snapshot(&revision)?; + let root = repo.root_dir(&revision)?; let p = file_system::Path::from_str(path)?; let file = root - .find_file(p.clone()) + .find_file(p.clone(), repo) .ok_or_else(|| Error::PathNotFound(p.clone()))?; let mut commit_path = file_system::Path::root(); @@ -151,7 +151,8 @@ where .map(|c| commit::Header::from(&c)); let (_rest, last) = p.split_last(); - let content = content(&file.contents); + let file_content = repo.file_content(file)?; + let content = content(file_content.as_bytes()); Ok(Blob { content, diff --git a/radicle-surf/src/object/tree.rs b/radicle-surf/src/object/tree.rs index c9931cc..2077527 100644 --- a/radicle-surf/src/object/tree.rs +++ b/radicle-surf/src/object/tree.rs @@ -28,7 +28,7 @@ use serde::{ use crate::{ commit, - file_system::{self, DirectoryContents}, + file_system::{self, DirectoryEntry}, git::RepositoryRef, object::{Error, Info, ObjectType}, revision::Revision, @@ -108,17 +108,18 @@ where file_system::Path::from_str(&prefix)? }; - let root_dir = repo.snapshot(&rev)?; + let root_dir = repo.root_dir(&rev)?; let prefix_dir = if path.is_root() { - &root_dir + root_dir } else { root_dir - .find_directory(path.clone()) + .find_directory(path.clone(), repo) .ok_or_else(|| Error::PathNotFound(path.clone()))? }; let entries_results: Result, Error> = prefix_dir - .contents() + .contents(repo)? + .iter() .map(|entry| { let entry_path = if path.is_root() { file_system::Path::new(entry.label().clone()) @@ -133,8 +134,8 @@ where let info = Info { name: entry.label().to_string(), object_type: match entry { - DirectoryContents::Directory(_) => ObjectType::Tree, - DirectoryContents::File { .. } => ObjectType::Blob, + DirectoryEntry::Directory(_) => ObjectType::Tree, + DirectoryEntry::File { .. } => ObjectType::Blob, }, last_commit: None, }; diff --git a/radicle-surf/src/vcs/git/commit.rs b/radicle-surf/src/vcs/git/commit.rs index 9fded41..9f04068 100644 --- a/radicle-surf/src/vcs/git/commit.rs +++ b/radicle-surf/src/vcs/git/commit.rs @@ -121,11 +121,11 @@ impl Commit { } /// Retrieves the file with `path` in this commit. - pub fn get_file( - &self, - repo: &RepositoryRef, + pub fn get_file<'a>( + &'a self, + repo: &'a RepositoryRef, path: file_system::Path, - ) -> Result { + ) -> Result { let git2_commit = repo.get_git2_commit(self.id)?; repo.get_commit_file(&git2_commit, path) } diff --git a/radicle-surf/src/vcs/git/repo.rs b/radicle-surf/src/vcs/git/repo.rs index 931f20c..67c3b85 100644 --- a/radicle-surf/src/vcs/git/repo.rs +++ b/radicle-surf/src/vcs/git/repo.rs @@ -18,7 +18,7 @@ use crate::{ diff::*, file_system, - file_system::{directory, DirectoryContents, Label}, + file_system::{directory, DirectoryEntry, Label}, vcs::git::{ error::*, Branch, @@ -34,10 +34,10 @@ use crate::{ TagName, }, }; -use directory::Directory; +use directory::{Directory, FileContent}; use radicle_git_ext::Oid; use std::{ - collections::{btree_set, BTreeSet}, + collections::{btree_set, BTreeMap, BTreeSet}, convert::TryFrom, path::PathBuf, str, @@ -60,7 +60,7 @@ pub struct Repository(pub(super) git2::Repository); /// `RepositoryRef`. #[derive(Clone, Copy)] pub struct RepositoryRef<'a> { - pub(super) repo_ref: &'a git2::Repository, + pub(crate) repo_ref: &'a git2::Repository, } // RepositoryRef should be safe to transfer across thread boundaries since it @@ -221,11 +221,68 @@ impl<'a> RepositoryRef<'a> { Ok(self.repo_ref.revparse_single(oid)?.id().into()) } - /// Gets a snapshot of the repo as a Directory. - pub fn snapshot(&self, commit: C) -> Result { + /// Returns a top level `Directory` without nested sub-directories. + /// + /// To visit inside any nested sub-directories, call `directory.get(&repo)` + /// on the sub-directory. + pub fn root_dir(&self, commit: C) -> Result { let commit = commit.to_commit(self)?; let git2_commit = self.repo_ref.find_commit((commit.id).into())?; - self.directory_of_commit(&git2_commit) + let tree = git2_commit.as_object().peel_to_tree()?; + Ok(Directory { + name: Label::root(), + oid: tree.id().into(), + }) + } + + /// Retrieves the content of a directory. + pub(crate) fn directory_get( + &self, + d: &Directory, + ) -> Result, Error> { + let git2_tree = self.repo_ref.find_tree(d.oid.into())?; + let map = self.tree_first_level(git2_tree)?; + Ok(map) + } + + /// Returns a map of the first level entries in `tree`. + fn tree_first_level(&self, tree: git2::Tree) -> Result, 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::