mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-26 23:00:38 +08:00
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:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user