Merge remote-tracking branch 'han/design-update'

* han/design-update:
  redesign Directory and File to be lightweight and lazy.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Fintan Halpenny
2022-11-18 09:49:21 +00:00
10 changed files with 423 additions and 852 deletions
+66
View File
@@ -0,0 +1,66 @@
// This file is part of radicle-surf
// <https://github.com/radicle-dev/radicle-surf>
//
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 or
// later as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! An example of browsing a git repo using `radicle-surf`.
//!
//! How to run:
//!
//! cargo run --example browsing <git_repo_path>
//!
//! This program browses the given repo and prints out the files and
//! the directories in a tree-like structure.
use radicle_surf::{
file_system::{Directory, DirectoryEntry},
git::{Repository, RepositoryRef},
};
use std::{env, time::Instant};
fn main() {
let repo_path = match env::args().nth(1) {
Some(path) => path,
None => {
print_usage();
return;
},
};
let repo = Repository::discover(&repo_path).unwrap();
let repo = repo.as_ref();
let now = Instant::now();
let head = repo.head_oid().unwrap();
let root = repo.root_dir(head).unwrap();
print_directory(&root, &repo, 0);
let elapsed_millis = now.elapsed().as_millis();
println!("browse with print: {} ms", elapsed_millis);
}
fn print_directory(d: &Directory, repo: &RepositoryRef, indent_level: usize) {
let indent = " ".repeat(indent_level * 4);
println!("{}{}/", &indent, d.name());
for entry in d.contents(repo).unwrap().iter() {
match entry {
DirectoryEntry::File(f) => println!(" {}{}", &indent, f.name()),
DirectoryEntry::Directory(d) => print_directory(d, repo, indent_level + 1),
}
}
}
fn print_usage() {
println!("Usage:");
println!("cargo run --example browsing <repo_path>");
}
+7 -206
View File
@@ -17,12 +17,15 @@
#![allow(dead_code, unused_variables, missing_docs)]
use std::{cell::RefCell, cmp::Ordering, convert::TryFrom, ops::Deref, rc::Rc, slice};
use std::{convert::TryFrom, slice};
#[cfg(feature = "serialize")]
use serde::{ser, Serialize, Serializer};
use crate::file_system::{Directory, DirectoryContents, Path};
use crate::{
file_system::{Directory, Path},
vcs::git::{Error, RepositoryRef},
};
pub mod git;
@@ -293,14 +296,7 @@ impl Diff {
// TODO: Direction of comparison is not obvious with this signature.
// For now using conventional approach with the right being "newer".
#[allow(clippy::self_named_constructors)]
pub fn diff(left: Directory, right: Directory) -> Self {
let mut diff = Diff::new();
let path = Rc::new(RefCell::new(Path::from_labels(
right.current().clone(),
&[],
)));
Diff::collect_diff(&left, &right, &path, &mut diff);
pub fn diff(left: Directory, right: Directory, repo: RepositoryRef) -> Result<Self, Error> {
// TODO: Some of the deleted files may actually be moved (renamed) to one of the
// created files. Finding out which of the deleted files were deleted
// and which were moved will probably require performing some variant of
@@ -308,180 +304,7 @@ impl Diff {
// decision can be based on heuristics, e.g. the file can be considered
// moved, if len(LCS) > 0,25 * min(size(d), size(c)), and
// deleted otherwise.
diff
}
fn collect_diff(
old: &Directory,
new: &Directory,
parent_path: &Rc<RefCell<Path>>,
diff: &mut Diff,
) {
let mut old_iter = old.contents();
let mut new_iter = new.contents();
let mut old_entry_opt = old_iter.next();
let mut new_entry_opt = new_iter.next();
while old_entry_opt.is_some() || new_entry_opt.is_some() {
match (&old_entry_opt, &new_entry_opt) {
(Some(ref old_entry), Some(ref new_entry)) => {
match new_entry.label().cmp(old_entry.label()) {
Ordering::Greater => {
diff.add_deleted_files(old_entry, parent_path);
old_entry_opt = old_iter.next();
},
Ordering::Less => {
diff.add_created_files(new_entry, parent_path);
new_entry_opt = new_iter.next();
},
Ordering::Equal => match (new_entry, old_entry) {
(
DirectoryContents::File {
name: new_file_name,
file: new_file,
},
DirectoryContents::File {
name: old_file_name,
file: old_file,
},
) => {
if old_file.size != new_file.size
|| old_file.checksum() != new_file.checksum()
{
let mut path = parent_path.borrow().clone();
path.push(new_file_name.clone());
diff.add_modified_file(path, vec![], None);
}
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
(
DirectoryContents::File {
name: new_file_name,
file: new_file,
},
DirectoryContents::Directory(old_dir),
) => {
let mut path = parent_path.borrow().clone();
path.push(new_file_name.clone());
diff.add_created_file(
path,
FileDiff::Plain {
hunks: Hunks::default(),
},
);
diff.add_deleted_files(old_entry, parent_path);
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
(
DirectoryContents::Directory(new_dir),
DirectoryContents::File {
name: old_file_name,
file: old_file,
},
) => {
let mut path = parent_path.borrow().clone();
path.push(old_file_name.clone());
diff.add_created_files(new_entry, parent_path);
diff.add_deleted_file(
path,
FileDiff::Plain {
hunks: Hunks::default(),
},
);
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
(
DirectoryContents::Directory(new_dir),
DirectoryContents::Directory(old_dir),
) => {
parent_path.borrow_mut().push(new_dir.current().clone());
Diff::collect_diff(
old_dir.deref(),
new_dir.deref(),
parent_path,
diff,
);
parent_path.borrow_mut().pop();
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
},
}
},
(Some(old_entry), None) => {
diff.add_deleted_files(old_entry, parent_path);
old_entry_opt = old_iter.next();
},
(None, Some(new_entry)) => {
diff.add_created_files(new_entry, parent_path);
new_entry_opt = new_iter.next();
},
(None, None) => break,
}
}
}
// if entry is a file, then return this file,
// or a list of files in the directory tree otherwise
fn collect_files_from_entry<F, T>(
entry: &DirectoryContents,
parent_path: &Rc<RefCell<Path>>,
mapper: F,
) -> Vec<T>
where
F: Fn(Path) -> T + Copy,
{
match entry {
DirectoryContents::Directory(dir) => Diff::collect_files(dir, parent_path, mapper),
DirectoryContents::File { name, .. } => {
let mut path = parent_path.borrow().clone();
path.push(name.clone());
vec![mapper(path)]
},
}
}
fn collect_files<F, T>(dir: &Directory, parent_path: &Rc<RefCell<Path>>, mapper: F) -> Vec<T>
where
F: Fn(Path) -> T + Copy,
{
let mut files: Vec<T> = Vec::new();
Diff::collect_files_inner(dir, parent_path, mapper, &mut files);
files
}
fn collect_files_inner<'a, F, T>(
dir: &'a Directory,
parent_path: &Rc<RefCell<Path>>,
mapper: F,
files: &mut Vec<T>,
) where
F: Fn(Path) -> T + Copy,
{
parent_path.borrow_mut().push(dir.current().clone());
for entry in dir.contents() {
match entry {
DirectoryContents::Directory(subdir) => {
Diff::collect_files_inner(subdir, parent_path, mapper, files);
},
DirectoryContents::File { name, .. } => {
let mut path = parent_path.borrow().clone();
path.push(name.clone());
files.push(mapper(path));
},
}
}
parent_path.borrow_mut().pop();
repo.diff(left, right)
}
pub(crate) fn add_modified_file(
@@ -522,32 +345,10 @@ impl Diff {
self.created.push(CreateFile { path, diff });
}
fn add_created_files(&mut self, dc: &DirectoryContents, parent_path: &Rc<RefCell<Path>>) {
let mut new_files: Vec<CreateFile> =
Diff::collect_files_from_entry(dc, parent_path, |path| CreateFile {
path,
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
});
self.created.append(&mut new_files);
}
pub(crate) fn add_deleted_file(&mut self, path: Path, diff: FileDiff) {
self.deleted.push(DeleteFile { path, diff });
}
fn add_deleted_files(&mut self, dc: &DirectoryContents, parent_path: &Rc<RefCell<Path>>) {
let mut new_files: Vec<DeleteFile> =
Diff::collect_files_from_entry(dc, parent_path, |path| DeleteFile {
path,
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
});
self.deleted.append(&mut new_files);
}
pub fn stats(&self) -> Stats {
let mut deletions = 0;
let mut insertions = 0;
+151 -260
View File
@@ -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<u8>,
pub(crate) size: usize,
}
impl std::fmt::Debug for File {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut contents = self.contents.clone();
contents.truncate(10);
write!(
f,
"File {{ contents: {:?}, size: {} }}",
contents, self.size
)
}
pub(crate) name: Label,
pub(crate) oid: Oid,
}
impl File {
/// Create a new `File` with the contents provided.
pub fn new(contents: &[u8]) -> Self {
let size = contents.len();
/// Create a new `File`.
pub fn new(name: String, oid: Oid) -> Self {
File {
contents: contents.to_vec(),
size,
name: Label {
label: name,
hidden: false,
},
oid,
}
}
/// Get the size of the `File` corresponding to the number of bytes in the
/// file contents.
///
/// # Examples
///
/// ```
/// use radicle_surf::file_system::File;
///
/// let file = File::new(b"pub mod diff;\npub mod file_system;\npub mod vcs;\npub use crate::vcs::git;\n");
///
/// assert_eq!(file.size(), 73);
/// ```
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<Label, DirectoryEntry>,
}
impl DirectoryContent {
/// Returns an iterator for the listing of a directory.
pub fn iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
self.listing.values()
}
}
/// Represents an entry in a directory.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum 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<Label, DirectoryContents>,
}
/// `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<DirectoryContent, git::Error> {
let listing = repo.directory_get(self)?;
Ok(DirectoryContent { listing })
}
/// Returns an iterator for the contents of the current directory.
///
/// Note that the returned iterator only iterates the current level,
/// without going recursively into sub-directories.
pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents> {
self.contents.values()
/// Retrieves a `DirectoryEntry` for `path` in `repo`.
pub fn get_path(
&self,
path: &path::Path,
repo: &RepositoryRef,
) -> Result<DirectoryEntry, git::Error> {
// Search the path in git2 tree.
let git2_tree = repo.repo_ref.find_tree(self.oid.into())?;
let entry = git2_tree.get_path(path)?;
// Construct the DirectoryEntry.
let name = entry.name().ok_or_else(|| {
Error::Label(LabelError::InvalidUTF8 {
label: String::from_utf8_lossy(entry.name_bytes()).into(),
})
})?;
let label = Label {
label: name.to_string(),
hidden: false,
};
let oid: Oid = entry.id().into();
match entry.kind() {
Some(git2::ObjectType::Tree) => {
let dir = Directory::new(label, oid);
Ok(DirectoryEntry::Directory(dir))
},
Some(git2::ObjectType::Blob) => {
let f = File { name: label, oid };
Ok(DirectoryEntry::File(f))
},
_ => {
let file_path = Path::try_from(path.to_path_buf())?;
Err(git::Error::PathNotFound(file_path))
},
}
}
/// Find a [`File`] in the directory given the [`Path`] to the [`File`].
@@ -206,23 +200,17 @@ impl Directory {
/// // We shouldn't be able to find a file that doesn't exist
/// assert_eq!(directory.find_file(unsound::path::new("foo/bar/qux.rs")), None);
/// ```
pub fn find_file(&self, path: Path) -> Option<&File> {
let mut contents = &self.contents;
let path_depth = path.0.len();
for (idx, label) in path.iter().enumerate() {
match contents.get(label) {
Some(DirectoryContents::Directory(d)) => contents = &d.contents,
Some(DirectoryContents::File { name: _, file }) => {
if idx + 1 == path_depth {
return Some(file);
} else {
break; // Abort: finding a file before the last label.
}
},
None => break, // Abort: a label not found.
}
pub fn find_file(&self, path: Path, repo: &RepositoryRef) -> Option<Oid> {
let path_buf: std::path::PathBuf = (&path).into();
let entry = match self.get_path(path_buf.as_path(), repo) {
Ok(entry) => entry,
Err(_) => return None,
};
match entry {
DirectoryEntry::File(f) => Some(f.oid),
DirectoryEntry::Directory(_) => None,
}
None
}
/// Find a `Directory` in the directory given the [`Path`] to the
@@ -260,46 +248,17 @@ impl Directory {
/// // 'baz.rs' is a file and not a directory
/// assert!(directory.find_directory(unsound::path::new("foo/bar/baz.rs")).is_none());
/// ```
pub fn find_directory(&self, path: Path) -> Option<&Self> {
let mut found = None;
let mut contents = &self.contents;
pub fn find_directory(&self, path: Path, repo: &RepositoryRef) -> Option<Self> {
let path_buf: std::path::PathBuf = (&path).into();
let entry = match self.get_path(path_buf.as_path(), repo) {
Ok(entry) => entry,
Err(_) => return None,
};
for label in path.iter() {
match contents.get(label) {
Some(DirectoryContents::Directory(d)) => {
found = Some(d);
contents = &d.contents;
},
Some(DirectoryContents::File { .. }) => break, // Abort: should not be a file.
None => break, // Abort: a label not found.
}
match entry {
DirectoryEntry::File(_) => None,
DirectoryEntry::Directory(d) => Some(d),
}
found
}
/// Get the [`Label`] of the current directory.
///
/// # Examples
///
/// ```
/// use radicle_surf::file_system::{Directory, File, Label};
/// use radicle_surf::file_system::unsound;
///
/// let mut root = Directory::root();
/// root.insert_file(unsound::path::new("main.rs"), File::new(b"println!(\"Hello, world!\")"));
/// root.insert_file(unsound::path::new("lib.rs"), File::new(b"struct Hello(String)"));
/// root.insert_file(unsound::path::new("test/mod.rs"), File::new(b"assert_eq!(1 + 1, 2);"));
///
/// assert_eq!(root.current(), Label::root());
///
/// let test = root.find_directory(
/// unsound::path::new("test")
/// ).expect("Missing test directory");
/// assert_eq!(test.current(), unsound::label::new("test"));
/// ```
pub fn current(&self) -> &Label {
&self.name
}
// TODO(fintan): This is going to be a bit trickier so going to leave it out for
@@ -311,94 +270,26 @@ impl Directory {
/// Get the total size, in bytes, of a `Directory`. The size is
/// the sum of all files that can be reached from this `Directory`.
///
/// # Examples
///
/// ```
/// use radicle_surf::file_system::{Directory, File};
/// use radicle_surf::file_system::unsound;
///
/// let mut root = Directory::root();
/// root.insert_file(unsound::path::new("main.rs"), File::new(b"println!(\"Hello, world!\")"));
/// root.insert_file(unsound::path::new("lib.rs"), File::new(b"struct Hello(String)"));
/// root.insert_file(unsound::path::new("test/mod.rs"), File::new(b"assert_eq!(1 + 1, 2);"));
///
/// assert_eq!(root.size(), 66);
/// ```
pub fn size(&self) -> usize {
self.contents().fold(0, |size, item| {
if let DirectoryContents::File { name: _, file } = item {
size + file.size()
} else {
size
}
})
}
/// Insert a file into a directory, given the full path to file (file name
/// inclusive) and the `File` itself.
///
/// This function is usually used for testing and demonstation purposes.
pub fn insert_file(&mut self, path: Path, file: File) {
let name = path.0.last().clone();
let f = DirectoryContents::File {
name: name.clone(),
file,
};
let mut contents = &mut self.contents;
let path_depth = path.0.len() - 1; // exclude the last label: file name
if path_depth == 0 {
contents.insert(name, f);
} else {
for (idx, label) in path.iter().enumerate() {
// if label does not exist, create a sub directory.
if contents.get(label).is_none() {
let new_dir = Directory::new(label.clone());
contents.insert(label.clone(), DirectoryContents::Directory(new_dir));
}
match contents.get_mut(label) {
Some(DirectoryContents::Directory(d)) => {
contents = &mut d.contents;
if idx + 1 == path_depth {
// We are in the last directory level, insert the file.
contents.insert(name, f);
return;
}
},
Some(DirectoryContents::File { .. }) => return, // Abort: should not be a file.
None => return,
}
pub fn size(&self, repo: &RepositoryRef) -> Result<usize, git::Error> {
let mut size = 0;
let contents = self.contents(repo)?;
for item in contents.iter() {
match item {
DirectoryEntry::File(f) => {
let sz = repo.file_size(f.oid)?;
size += sz;
},
DirectoryEntry::Directory(d) => {
size += d.size(repo)?;
},
}
}
Ok(size)
}
}
/// Insert files into a shared directory path.
///
/// `directory_path` is used as the prefix to where the files should go. If
/// empty the files will be placed in the current `Directory`.
///
/// `files` are pairs of file name and the [`File`] itself.
///
/// This function is usually used for testing and demonstation purposes.
pub fn insert_files(&mut self, directory_path: &[Label], files: NonEmpty<(Label, File)>) {
match NonEmpty::from_slice(directory_path) {
None => {
for (file_name, file) in files.into_iter() {
self.insert_file(Path::new(file_name), file)
}
},
Some(path) => {
for (file_name, file) in files.into_iter() {
// The clone is necessary here because we use it as a prefix.
let mut file_path = Path(path.clone());
file_path.push(file_name);
self.insert_file(file_path, file)
}
},
}
impl Revision for Directory {
fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, git::Error> {
Ok(self.oid)
}
}
+4 -3
View File
@@ -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,
+8 -7
View File
@@ -28,7 +28,7 @@ use serde::{
use crate::{
commit,
file_system::{self, DirectoryContents},
file_system::{self, DirectoryEntry},
git::RepositoryRef,
object::{Error, Info, ObjectType},
revision::Revision,
@@ -108,17 +108,18 @@ where
file_system::Path::from_str(&prefix)?
};
let root_dir = repo.snapshot(&rev)?;
let root_dir = repo.root_dir(&rev)?;
let prefix_dir = if path.is_root() {
&root_dir
root_dir
} else {
root_dir
.find_directory(path.clone())
.find_directory(path.clone(), repo)
.ok_or_else(|| Error::PathNotFound(path.clone()))?
};
let entries_results: Result<Vec<TreeEntry>, Error> = prefix_dir
.contents()
.contents(repo)?
.iter()
.map(|entry| {
let entry_path = if path.is_root() {
file_system::Path::new(entry.label().clone())
@@ -133,8 +134,8 @@ where
let info = Info {
name: entry.label().to_string(),
object_type: match entry {
DirectoryContents::Directory(_) => ObjectType::Tree,
DirectoryContents::File { .. } => ObjectType::Blob,
DirectoryEntry::Directory(_) => ObjectType::Tree,
DirectoryEntry::File { .. } => ObjectType::Blob,
},
last_commit: None,
};
+4 -4
View File
@@ -121,11 +121,11 @@ impl Commit {
}
/// Retrieves the file with `path` in this commit.
pub fn get_file(
&self,
repo: &RepositoryRef,
pub fn get_file<'a>(
&'a self,
repo: &'a RepositoryRef,
path: file_system::Path,
) -> Result<directory::File, Error> {
) -> Result<directory::FileContent, Error> {
let git2_commit = repo.get_git2_commit(self.id)?;
repo.get_commit_file(&git2_commit, path)
}
+79 -104
View File
@@ -18,7 +18,7 @@
use crate::{
diff::*,
file_system,
file_system::{directory, DirectoryContents, Label},
file_system::{directory, DirectoryEntry, Label},
vcs::git::{
error::*,
Branch,
@@ -34,10 +34,10 @@ use crate::{
TagName,
},
};
use directory::Directory;
use directory::{Directory, FileContent};
use radicle_git_ext::Oid;
use std::{
collections::{btree_set, BTreeSet},
collections::{btree_set, BTreeMap, BTreeSet},
convert::TryFrom,
path::PathBuf,
str,
@@ -60,7 +60,7 @@ pub struct Repository(pub(super) git2::Repository);
/// `RepositoryRef`.
#[derive(Clone, Copy)]
pub struct RepositoryRef<'a> {
pub(super) repo_ref: &'a git2::Repository,
pub(crate) repo_ref: &'a git2::Repository,
}
// RepositoryRef should be safe to transfer across thread boundaries since it
@@ -221,11 +221,68 @@ impl<'a> RepositoryRef<'a> {
Ok(self.repo_ref.revparse_single(oid)?.id().into())
}
/// Gets a snapshot of the repo as a Directory.
pub fn snapshot<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
/// Returns a top level `Directory` without nested sub-directories.
///
/// To visit inside any nested sub-directories, call `directory.get(&repo)`
/// on the sub-directory.
pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
let commit = commit.to_commit(self)?;
let git2_commit = self.repo_ref.find_commit((commit.id).into())?;
self.directory_of_commit(&git2_commit)
let tree = git2_commit.as_object().peel_to_tree()?;
Ok(Directory {
name: Label::root(),
oid: tree.id().into(),
})
}
/// Retrieves the content of a directory.
pub(crate) fn directory_get(
&self,
d: &Directory,
) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
let git2_tree = self.repo_ref.find_tree(d.oid.into())?;
let map = self.tree_first_level(git2_tree)?;
Ok(map)
}
/// Returns a map of the first level entries in `tree`.
fn tree_first_level(&self, tree: git2::Tree) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
let mut map = BTreeMap::new();
// Walks only the first level of entries.
tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| {
let oid = entry.id().into();
let label = match entry.name() {
Some(name) => match name.parse::<Label>() {
Ok(label) => label,
Err(_) => return git2::TreeWalkResult::Abort,
},
None => return git2::TreeWalkResult::Abort,
};
match entry.kind() {
Some(git2::ObjectType::Tree) => {
let dir = Directory::new(label.clone(), oid);
map.insert(label, DirectoryEntry::Directory(dir));
return git2::TreeWalkResult::Skip; // Not go into nested
// directories.
},
Some(git2::ObjectType::Blob) => {
let f = directory::File {
name: label.clone(),
oid,
};
map.insert(label, DirectoryEntry::File(f));
},
_ => {
return git2::TreeWalkResult::Skip;
},
}
git2::TreeWalkResult::Ok
})?;
Ok(map)
}
/// Returns the last commit, if exists, for a `path` in the history of
@@ -271,6 +328,18 @@ impl<'a> RepositoryRef<'a> {
})
}
/// Obtain the file content
pub(crate) fn file_content(&self, object_id: Oid) -> Result<FileContent, Error> {
let blob = self.repo_ref.find_blob(object_id.into())?;
Ok(FileContent::new(blob))
}
/// Return the size of a file
pub(crate) fn file_size(&self, oid: Oid) -> Result<usize, Error> {
let blob = self.repo_ref.find_blob(oid.into())?;
Ok(blob.size())
}
/// Lists branch names with `filter`.
pub fn branch_names(&self, filter: &Glob<Branch>) -> Result<Vec<BranchName>, Error> {
let branches: Result<Vec<BranchName>, Error> =
@@ -378,12 +447,12 @@ impl<'a> RepositoryRef<'a> {
&self,
git2_commit: &git2::Commit,
path: file_system::Path,
) -> Result<directory::File, Error> {
) -> Result<FileContent, Error> {
let git2_tree = git2_commit.tree()?;
let entry = git2_tree.get_path(PathBuf::from(&path).as_ref())?;
let object = entry.to_object(self.repo_ref)?;
let blob = object.as_blob().ok_or(Error::PathNotFound(path))?;
Ok(directory::File::new(blob.content()))
let blob = object.into_blob().map_err(|_| Error::PathNotFound(path))?;
Ok(FileContent::new(blob))
}
pub(crate) fn diff_commit_and_parents(
@@ -429,100 +498,6 @@ impl<'a> RepositoryRef<'a> {
Ok(diff)
}
/// Generates a Directory for the commit.
fn directory_of_commit(&self, commit: &git2::Commit) -> Result<Directory, Error> {
let mut parent_dirs = vec![Directory::root()];
let tree = commit.as_object().peel_to_tree()?;
tree.walk(git2::TreeWalkMode::PreOrder, |s, entry| {
let tree_level = s.split('/').count();
if tree_level < parent_dirs.len() {
// As it is PreOrder, the last directory A was visited
// completely and we are back to the level. Now insert A
// into its parent directory.
if let Some(last_dir) = parent_dirs.pop() {
if let Some(parent) = parent_dirs.last_mut() {
let name = last_dir.name().clone();
let content = DirectoryContents::Directory(last_dir);
parent.insert(name, content);
}
}
}
match entry.kind() {
Some(git2::ObjectType::Tree) => {
if let Some(name) = entry.name() {
// Add a new level of directory.
match name.parse() {
Ok(label) => parent_dirs.push(Directory::new(label)),
Err(_) => {
return git2::TreeWalkResult::Abort;
},
}
}
},
Some(git2::ObjectType::Blob) => {
// Construct a File to insert into its parent directory.
let object = match entry.to_object(self.repo_ref) {
Ok(obj) => obj,
Err(_) => {
return git2::TreeWalkResult::Abort;
},
};
let blob = match object.as_blob() {
Some(b) => b,
None => return git2::TreeWalkResult::Abort,
};
let f = directory::File::new(blob.content());
let label = match entry.name() {
Some(name) => match name.parse::<Label>() {
Ok(label) => label,
Err(_) => {
return git2::TreeWalkResult::Abort;
},
},
None => return git2::TreeWalkResult::Abort,
};
let content = DirectoryContents::File {
name: label.clone(),
file: f,
};
let parent = match parent_dirs.last_mut() {
Some(parent_dir) => parent_dir,
None => return git2::TreeWalkResult::Abort,
};
parent.insert(label, content);
},
_ => {
return git2::TreeWalkResult::Skip;
},
}
git2::TreeWalkResult::Ok
})?;
// Tree walk is complete but there are some levels of dirs
// that are not popped up from `parent_dirs` yet. Note that
// the root dir is `parent_dirs[0]`.
//
// We need to pop up `parent_dirs` fully and update the directory
// content at each level.
while let Some(curr_dir) = parent_dirs.pop() {
match parent_dirs.last_mut() {
Some(parent) => {
let name = curr_dir.name().clone();
let content = DirectoryContents::Directory(curr_dir);
parent.insert(name, content);
},
None => return Ok(curr_dir), // No more parent, we're at the root.
}
}
Err(Error::RevParseFailure {
rev: commit.id().to_string(),
})
}
/// Returns the history with the `head` commit.
pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
History::new(*self, head)
+1 -170
View File
@@ -4,60 +4,7 @@
//! Unit tests for radicle_surf::diff
use pretty_assertions::assert_eq;
use radicle_surf::{
diff::*,
file_system::{unsound, *},
};
#[test]
fn test_create_file() {
let directory = Directory::root();
let mut new_directory = Directory::root();
new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
let diff = Diff::diff(directory, new_directory);
let expected_diff = Diff {
created: vec![CreateFile {
path: Path::with_root(&[unsound::label::new("banana.rs")]),
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
}],
deleted: vec![],
copied: vec![],
moved: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_delete_file() {
let mut directory = Directory::root();
directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
let new_directory = Directory::root();
let diff = Diff::diff(directory, new_directory);
let expected_diff = Diff {
created: vec![],
deleted: vec![DeleteFile {
path: Path::with_root(&[unsound::label::new("banana.rs")]),
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
}],
moved: vec![],
copied: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
use radicle_surf::diff::*;
/* TODO(fintan): Move is not detected yet
#[test]
@@ -74,122 +21,6 @@ fn test_moved_file() {
}
*/
#[test]
fn test_modify_file() {
let mut directory = Directory::root();
directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
let mut new_directory = Directory::root();
new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana;"));
let diff = Diff::diff(directory, new_directory);
let expected_diff = Diff {
created: vec![],
deleted: vec![],
moved: vec![],
copied: vec![],
modified: vec![ModifiedFile {
path: Path::with_root(&[unsound::label::new("banana.rs")]),
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
eof: None,
}],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_create_directory() {
let directory = Directory::root();
let mut new_directory = Directory::root();
new_directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana"),
);
let diff = Diff::diff(directory, new_directory);
let expected_diff = Diff {
created: vec![CreateFile {
path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]),
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
}],
deleted: vec![],
moved: vec![],
copied: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_delete_directory() {
let mut directory = Directory::root();
directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana"),
);
let new_directory = Directory::root();
let diff = Diff::diff(directory, new_directory);
let expected_diff = Diff {
created: vec![],
deleted: vec![DeleteFile {
path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]),
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
}],
moved: vec![],
copied: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_modify_file_directory() {
let mut directory = Directory::root();
directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana"),
);
let mut new_directory = Directory::root();
new_directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana;"),
);
let diff = Diff::diff(directory, new_directory);
let expected_diff = Diff {
created: vec![],
deleted: vec![],
moved: vec![],
copied: vec![],
modified: vec![ModifiedFile {
path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]),
diff: FileDiff::Plain {
hunks: Hunks::default(),
},
eof: None,
}],
};
assert_eq!(diff, expected_diff)
}
/* TODO(fintan): Tricky stuff
#[test]
fn test_disjoint_directories() {
+70 -87
View File
@@ -3,93 +3,6 @@
//! Unit tests for radicle_surf::file_system
#[cfg(test)]
mod list_directory {
use radicle_surf::file_system::{unsound, Directory, File, Label};
#[test]
fn root_files() {
let mut directory = Directory::root();
directory.insert_file(
unsound::path::new("foo.hs"),
File::new(b"module BananaFoo ..."),
);
directory.insert_file(
unsound::path::new("bar.hs"),
File::new(b"module BananaBar ..."),
);
directory.insert_file(
unsound::path::new("baz.hs"),
File::new(b"module BananaBaz ..."),
);
let files: Vec<Label> = directory.contents().map(|c| c.label().clone()).collect();
assert_eq!(
files,
vec![
"bar.hs".parse::<Label>().unwrap(),
"baz.hs".parse::<Label>().unwrap(),
"foo.hs".parse::<Label>().unwrap(),
]
);
}
}
#[cfg(test)]
mod find_file {
use radicle_surf::file_system::{unsound, *};
#[test]
fn in_root() {
let file = File::new(b"module Banana ...");
let mut directory = Directory::root();
directory.insert_file(unsound::path::new("bar/foo.hs"), file.clone());
assert_eq!(
directory.find_file(unsound::path::new("bar/foo.hs")),
Some(&file)
);
}
#[test]
fn file_does_not_exist() {
let file_path = unsound::path::new("bar.hs");
let file = File::new(b"module Banana ...");
let mut directory = Directory::root();
directory.insert_file(unsound::path::new("foo.hs"), file);
assert_eq!(directory.find_file(file_path), None)
}
}
#[cfg(test)]
mod directory_size {
use nonempty::NonEmpty;
use radicle_surf::file_system::{unsound, Directory, File};
#[test]
fn root_directory_files() {
let mut root = Directory::root();
root.insert_files(
&[],
NonEmpty::from((
(
unsound::label::new("main.rs"),
File::new(b"println!(\"Hello, world!\")"),
),
vec![(
unsound::label::new("lib.rs"),
File::new(b"struct Hello(String)"),
)],
)),
);
assert_eq!(root.size(), 45);
}
}
#[cfg(test)]
mod path {
use radicle_surf::file_system::unsound;
@@ -114,3 +27,73 @@ mod path {
);
}
}
#[cfg(test)]
mod directory {
use radicle_surf::{
file_system::DirectoryEntry,
git::{Branch, Repository},
};
use std::path::Path;
const GIT_PLATINUM: &str = "../data/git-platinum";
#[test]
fn directory_get_path() {
let repo = Repository::open(GIT_PLATINUM).unwrap();
let repo = repo.as_ref();
let root = repo.root_dir(&Branch::local("master")).unwrap();
// get_path for a file.
let path = Path::new("src/memory.rs");
let entry = root.get_path(path, &repo).unwrap();
assert!(matches!(entry, DirectoryEntry::File(_)));
// get_path for a directory.
let path = Path::new("this/is/a/really/deeply/nested/directory/tree");
let entry = root.get_path(path, &repo).unwrap();
assert!(matches!(entry, DirectoryEntry::Directory(_)));
// get_path for a non-leaf directory and its relative path.
let path = Path::new("text");
let entry = root.get_path(path, &repo).unwrap();
assert!(matches!(entry, DirectoryEntry::Directory(_)));
if let DirectoryEntry::Directory(sub_dir) = entry {
let inner_path = Path::new("garden.txt");
let inner_entry = sub_dir.get_path(inner_path, &repo).unwrap();
assert!(matches!(inner_entry, DirectoryEntry::File(_)));
}
// get_path for non-existing file
let path = Path::new("this/is/a/really/missing_file");
let result = root.get_path(path, &repo);
assert!(result.is_err());
// get_path for absolute path: fail.
let path = Path::new("/src/memory.rs");
let result = root.get_path(path, &repo);
assert!(result.is_err());
}
#[test]
fn directory_size() {
let repo = Repository::open(GIT_PLATINUM).unwrap();
let repo = repo.as_ref();
let root = repo.root_dir(&Branch::local("master")).unwrap();
/*
git-platinum (master) $ ls -l src
-rw-r--r-- 1 pi pi 10044 Oct 31 11:32 Eval.hs
-rw-r--r-- 1 pi pi 6253 Oct 31 11:27 memory.rs
10044 + 6253 = 16297
*/
let path = Path::new("src");
let entry = root.get_path(path, &repo).unwrap();
assert!(matches!(entry, DirectoryEntry::Directory(_)));
if let DirectoryEntry::Directory(d) = entry {
assert_eq!(16297, d.size(&repo).unwrap());
}
}
}
+33 -11
View File
@@ -7,7 +7,7 @@
use radicle_surf::git::{Author, BranchType, Commit};
use radicle_surf::{
diff::*,
file_system::{unsound, DirectoryContents, Path},
file_system::{unsound, DirectoryEntry, Path},
git::{error::Error, Branch, Glob, Namespace, Oid, Repository, TagName},
};
@@ -18,7 +18,7 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
fn test_submodule_failure() {
let repo = Repository::discover(".").unwrap();
repo.as_ref().snapshot(&Branch::local("main")).unwrap();
repo.as_ref().root_dir(&Branch::local("main")).unwrap();
}
#[cfg(test)]
@@ -744,8 +744,8 @@ mod reference {
.collect::<Result<Vec<Tag>, Error>>()
.unwrap();
assert_eq!(tags.len(), 6);
let root_dir = repo_ref.snapshot(&tags[0]).unwrap();
assert_eq!(root_dir.contents().count(), 1);
let root_dir = repo_ref.root_dir(&tags[0]).unwrap();
assert_eq!(root_dir.contents(&repo_ref).unwrap().iter().count(), 1);
}
#[test]
@@ -767,27 +767,49 @@ mod reference {
mod code_browsing {
use super::*;
use radicle_surf::file_system::Directory;
use radicle_surf::{file_system::Directory, git::RepositoryRef};
#[test]
fn iterate_root_dir_recursive() {
let repo = Repository::open(GIT_PLATINUM).unwrap();
let repo = repo.as_ref();
let root_dir = repo.snapshot(&Branch::local("master")).unwrap();
let count = println_dir(&root_dir, 0);
let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
let count = println_dir(&root_dir, &repo, 0);
assert_eq!(count, 36); // Check total file count.
/// Prints items in `dir` with `indent_level`.
/// For sub-directories, will do Depth-First-Search and print
/// recursively.
/// Returns the number of items visited (i.e. printed)
fn println_dir(dir: &Directory, indent_level: usize) -> i32 {
fn println_dir(dir: &Directory, repo: &RepositoryRef, indent_level: usize) -> i32 {
let mut count = 0;
for item in dir.contents() {
for item in dir.contents(repo).unwrap().iter() {
println!("> {}{}", " ".repeat(indent_level * 4), &item.label());
count += 1;
if let DirectoryContents::Directory(sub_dir) = item {
count += println_dir(sub_dir, indent_level + 1);
if let DirectoryEntry::Directory(sub_dir) = item {
count += println_dir(sub_dir, repo, indent_level + 1);
}
}
count
}
}
#[test]
fn browse_repo_lazily() {
let repo = Repository::open(GIT_PLATINUM).unwrap();
let repo = repo.as_ref();
let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
let count = root_dir.contents(&repo).unwrap().iter().count();
assert_eq!(count, 8);
let count = traverse(&root_dir, &repo);
assert_eq!(count, 36);
fn traverse(dir: &Directory, repo: &RepositoryRef) -> i32 {
let mut count = 0;
for item in dir.contents(repo).unwrap().iter() {
count += 1;
if let DirectoryEntry::Directory(sub_dir) = item {
count += traverse(sub_dir, repo)
}
}
count