mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-25 22:30:35 +08:00
Merge remote-tracking branch 'origin/directory-generic'
* origin/directory-generic: radicle-surf: refactor directory Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -48,6 +48,10 @@ version = "0.2.0"
|
||||
path = "../radicle-git-ext"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.radicle-std-ext]
|
||||
version = "0.1.0"
|
||||
path = "../radicle-std-ext"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0"
|
||||
flate2 = "1"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
//! the directories in a tree-like structure.
|
||||
|
||||
use radicle_surf::{
|
||||
file_system::{Directory, DirectoryEntry},
|
||||
file_system::{directory, Directory},
|
||||
git::Repository,
|
||||
};
|
||||
use std::{env, time::Instant};
|
||||
@@ -51,10 +51,10 @@ fn main() {
|
||||
fn print_directory(d: &Directory, repo: &Repository, indent_level: usize) {
|
||||
let indent = " ".repeat(indent_level * 4);
|
||||
println!("{}{}/", &indent, d.name());
|
||||
for entry in d.contents(repo).unwrap().iter() {
|
||||
for entry in d.entries(repo).unwrap().entries() {
|
||||
match entry {
|
||||
DirectoryEntry::File(f) => println!(" {}{}", &indent, f.name()),
|
||||
DirectoryEntry::Directory(d) => print_directory(d, repo, indent_level + 1),
|
||||
directory::Entry::File(f) => println!(" {}{}", &indent, f.name()),
|
||||
directory::Entry::Directory(d) => print_directory(d, repo, indent_level + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +111,9 @@
|
||||
//! ```
|
||||
|
||||
pub mod directory;
|
||||
pub use directory::{Directory, Entries, Entry, File, FileContent};
|
||||
mod error;
|
||||
pub use error::Error;
|
||||
mod path;
|
||||
|
||||
pub use self::{directory::*, path::*};
|
||||
pub use self::path::*;
|
||||
|
||||
@@ -20,55 +20,114 @@
|
||||
//! A `Directory` is expected to be a non-empty tree of directories and files.
|
||||
//! See [`Directory`] for more information.
|
||||
|
||||
use crate::{
|
||||
file_system::{error::LabelError, path::*, Error},
|
||||
git::{self, Repository, Revision},
|
||||
};
|
||||
use git2::Blob;
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
convert::{Infallible, TryFrom},
|
||||
convert::{Infallible, Into},
|
||||
path,
|
||||
};
|
||||
|
||||
/// Represents a `file` in a git repo.
|
||||
use git2::Blob;
|
||||
use radicle_git_ext::{is_not_found_err, Oid};
|
||||
use radicle_std_ext::result::ResultExt as _;
|
||||
|
||||
use crate::{
|
||||
file_system::{path::*, Error},
|
||||
git::{Repository, Revision},
|
||||
};
|
||||
|
||||
pub mod error {
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum Directory {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Entry(#[from] Entry),
|
||||
#[error(transparent)]
|
||||
File(#[from] File),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum Entry {
|
||||
#[error("the entry name was not valid UTF-8")]
|
||||
Utf8Error,
|
||||
#[error(transparent)]
|
||||
Label(#[from] super::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum File {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
}
|
||||
}
|
||||
|
||||
/// A `File` in a git repository.
|
||||
///
|
||||
/// The representation is lightweight and contains the [`Oid`] that
|
||||
/// points to the git blob which is this file.
|
||||
///
|
||||
/// The name of a file can be retrieved via [`File::name`].
|
||||
///
|
||||
/// The [`FileContent`] of a file can be retrieved via
|
||||
/// [`File::content`].
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct File {
|
||||
pub(crate) name: Label,
|
||||
pub(crate) oid: Oid,
|
||||
pub(crate) id: Oid,
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// Create a new `File`.
|
||||
pub fn new(name: String, oid: Oid) -> Self {
|
||||
File {
|
||||
/// The name of this `File`.
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
/// The object identifier of this `File`.
|
||||
pub fn id(&self) -> Oid {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Create a new `File` with the `name` and `id` provided.
|
||||
///
|
||||
/// The `id` must point to a `git` blob.
|
||||
pub fn new(name: String, id: Oid) -> Self {
|
||||
Self {
|
||||
name: Label {
|
||||
label: name,
|
||||
hidden: false,
|
||||
},
|
||||
oid,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the file name reference.
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
/// Get the [`FileContent`] for this `File`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will fail if it could not find the `git` blob
|
||||
/// for the `Oid` of this `File`.
|
||||
pub fn content<'a>(&self, repo: &'a Repository) -> Result<FileContent<'a>, error::File> {
|
||||
let blob = repo.git2_repo().find_blob(self.id.into())?;
|
||||
Ok(FileContent { blob })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the actual content of a file.
|
||||
/// The contents of a [`File`].
|
||||
///
|
||||
/// To construct a `FileContent` use [`File::content`].
|
||||
pub struct FileContent<'a> {
|
||||
blob: Blob<'a>,
|
||||
}
|
||||
|
||||
impl<'a> FileContent<'a> {
|
||||
/// Returns the file content as a byte slice.
|
||||
/// Return the file contents as a byte slice.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.blob.content()
|
||||
}
|
||||
|
||||
/// Returns the size of file
|
||||
/// Return the size of the file contents.
|
||||
pub fn size(&self) -> usize {
|
||||
self.blob.size()
|
||||
}
|
||||
@@ -79,190 +138,176 @@ impl<'a> FileContent<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the listing of a directory.
|
||||
pub struct DirectoryContent {
|
||||
listing: BTreeMap<Label, DirectoryEntry>,
|
||||
/// A representations of a [`Directory`]'s entries.
|
||||
pub struct Entries {
|
||||
listing: BTreeMap<Label, Entry>,
|
||||
}
|
||||
|
||||
impl DirectoryContent {
|
||||
/// Returns an iterator for the listing of a directory.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &DirectoryEntry> {
|
||||
impl Entries {
|
||||
/// Return the [`Label`]s of each [`Entry`].
|
||||
pub fn names(&self) -> impl Iterator<Item = &Label> {
|
||||
self.listing.keys()
|
||||
}
|
||||
|
||||
/// Return each [`Entry`].
|
||||
pub fn entries(&self) -> impl Iterator<Item = &Entry> {
|
||||
self.listing.values()
|
||||
}
|
||||
|
||||
/// Return each [`Label`] and [`Entry`].
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Label, &Entry)> {
|
||||
self.listing.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an entry in a directory.
|
||||
/// An `Entry` is either a [`File`] entry or a [`Directory`] entry.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DirectoryEntry {
|
||||
/// When the entry is a file.
|
||||
pub enum Entry {
|
||||
/// A file entry within a [`Directory`].
|
||||
File(File),
|
||||
/// When the entry is a directory.
|
||||
/// A sub-directory of a [`Directory`].
|
||||
Directory(Directory),
|
||||
}
|
||||
|
||||
impl DirectoryEntry {
|
||||
/// Returns the label (short name, without the parent path) of the entry.
|
||||
impl Entry {
|
||||
/// Get a label for the `Entriess`, either the name of the [`File`]
|
||||
/// or the name of the [`Directory`].
|
||||
pub fn label(&self) -> &Label {
|
||||
match self {
|
||||
DirectoryEntry::File(file) => &file.name,
|
||||
DirectoryEntry::Directory(directory) => directory.name(),
|
||||
Entry::File(file) => &file.name,
|
||||
Entry::Directory(directory) => directory.name(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_entry(entry: &git2::TreeEntry) -> Result<Option<Self>, error::Entry> {
|
||||
let name = Label {
|
||||
label: entry.name().ok_or(error::Entry::Utf8Error)?.to_string(),
|
||||
hidden: false,
|
||||
};
|
||||
let id = entry.id().into();
|
||||
Ok(entry.kind().and_then(|kind| match kind {
|
||||
git2::ObjectType::Tree => Some(Self::Directory(Directory { name, id })),
|
||||
git2::ObjectType::Blob => Some(Self::File(File { name, id })),
|
||||
_ => None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Directory` is a set of entries of sub-directories and files, ordered
|
||||
/// by their unique names in the alphabetical order.
|
||||
/// A `Directory` is the representation of a file system directory, for a given
|
||||
/// [`git` tree][git-tree].
|
||||
///
|
||||
/// The name of a directory can be retrieved via [`File::name`].
|
||||
///
|
||||
/// The [`Entries`] of a directory can be retrieved via
|
||||
/// [`Directory::entries`].
|
||||
///
|
||||
/// [git-tree]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Directory {
|
||||
pub(crate) name: Label,
|
||||
pub(crate) oid: Oid,
|
||||
pub(crate) id: Oid,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
/// Creates a directory given `name` and `oid`, with empty contents.
|
||||
pub fn new(name: Label, oid: Oid) -> Self {
|
||||
Directory { name, oid }
|
||||
}
|
||||
|
||||
/// Get the name of the current `Directory`.
|
||||
pub fn name(&self) -> &Label {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns a `DirectoryContent` for the current directory.
|
||||
pub fn contents(&self, repo: &Repository) -> Result<DirectoryContent, git::Error> {
|
||||
let listing = repo.directory_get(self)?;
|
||||
Ok(DirectoryContent { listing })
|
||||
/// Creates a directory given its `name` and `id`.
|
||||
///
|
||||
/// The `id` must point to a `git` tree.
|
||||
pub fn new(name: Label, id: Oid) -> Self {
|
||||
Self { name, id }
|
||||
}
|
||||
|
||||
/// Retrieves a `DirectoryEntry` for `path` in `repo`.
|
||||
pub fn get_path(
|
||||
/// Return the [`Entries`] for this `Directory`'s `Oid`.
|
||||
///
|
||||
/// The resulting `Entries` will only resolve to this
|
||||
/// `Directory`'s entries. Any sub-directories will need to be
|
||||
/// resolved independently.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will fail if it could not find the `git` tree
|
||||
/// for the `Oid`.
|
||||
pub fn entries(&self, repo: &Repository) -> Result<Entries, error::Directory> {
|
||||
let tree = repo.git2_repo().find_tree(self.id.into())?;
|
||||
|
||||
let mut entries = BTreeMap::new();
|
||||
let mut error = None;
|
||||
|
||||
// Walks only the first level of entries.
|
||||
tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| {
|
||||
match Entry::from_entry(entry) {
|
||||
Ok(Some(entry)) => match entry {
|
||||
Entry::File(_) => {
|
||||
entries.insert(entry.label().clone(), entry);
|
||||
git2::TreeWalkResult::Ok
|
||||
},
|
||||
Entry::Directory(_) => {
|
||||
entries.insert(entry.label().clone(), entry);
|
||||
// Skip nested directories
|
||||
git2::TreeWalkResult::Skip
|
||||
},
|
||||
},
|
||||
Ok(None) => git2::TreeWalkResult::Skip,
|
||||
Err(err) => {
|
||||
error = Some(err);
|
||||
git2::TreeWalkResult::Abort
|
||||
},
|
||||
}
|
||||
})?;
|
||||
|
||||
match error {
|
||||
Some(err) => Err(err.into()),
|
||||
None => Ok(Entries { listing: entries }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the [`Entry`] found at `path`, if it exists.
|
||||
pub fn find_entry(
|
||||
&self,
|
||||
path: &path::Path,
|
||||
repo: &Repository,
|
||||
) -> Result<DirectoryEntry, git::Error> {
|
||||
) -> Result<Option<Entry>, crate::git::Error> {
|
||||
// Search the path in git2 tree.
|
||||
let git2_tree = repo.git2_repo().find_tree(self.oid.into())?;
|
||||
let entry = git2_tree.get_path(path)?;
|
||||
let git2_tree = repo.git2_repo().find_tree(self.id.into())?;
|
||||
let entry = git2_tree
|
||||
.get_path(path)
|
||||
.map(Some)
|
||||
.or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))?;
|
||||
|
||||
// Construct the DirectoryEntry.
|
||||
let name = entry.name().ok_or_else(|| {
|
||||
Error::Label(LabelError::InvalidUTF8 {
|
||||
label: String::from_utf8_lossy(entry.name_bytes()).into(),
|
||||
})
|
||||
})?;
|
||||
let label = Label {
|
||||
label: name.to_string(),
|
||||
hidden: false,
|
||||
};
|
||||
let oid: Oid = entry.id().into();
|
||||
match entry.kind() {
|
||||
Some(git2::ObjectType::Tree) => {
|
||||
let dir = Directory::new(label, oid);
|
||||
Ok(DirectoryEntry::Directory(dir))
|
||||
},
|
||||
Some(git2::ObjectType::Blob) => {
|
||||
let f = File { name: label, oid };
|
||||
Ok(DirectoryEntry::File(f))
|
||||
},
|
||||
_ => {
|
||||
let file_path = Path::try_from(path.to_path_buf())?;
|
||||
Err(git::Error::PathNotFound(file_path))
|
||||
},
|
||||
}
|
||||
Ok(entry
|
||||
.and_then(|entry| Entry::from_entry(&entry).transpose())
|
||||
.transpose()
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
/// Find a [`File`] in the directory given the [`Path`] to the [`File`].
|
||||
///
|
||||
/// # Failures
|
||||
///
|
||||
/// This operation fails if the path does not lead to a [`File`].
|
||||
/// If the search is for a `Directory` then use `find_directory`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Search for a file in the path:
|
||||
/// * `foo/bar/baz.hs`
|
||||
/// * `foo`
|
||||
/// * `foo/bar/qux.rs`
|
||||
///
|
||||
/// ```
|
||||
/// use radicle_surf::file_system::{Directory, File};
|
||||
/// use radicle_surf::file_system::unsound;
|
||||
///
|
||||
/// let file = File::new(b"module Banana ...");
|
||||
///
|
||||
/// let mut directory = Directory::root();
|
||||
/// directory.insert_file(unsound::path::new("foo/bar/baz.rs"), file.clone());
|
||||
///
|
||||
/// // The file is succesfully found
|
||||
/// assert_eq!(directory.find_file(unsound::path::new("foo/bar/baz.rs")), Some(file));
|
||||
///
|
||||
/// // We shouldn't be able to find a directory
|
||||
/// assert_eq!(directory.find_file(unsound::path::new("foo")), None);
|
||||
///
|
||||
/// // We shouldn't be able to find a file that doesn't exist
|
||||
/// assert_eq!(directory.find_file(unsound::path::new("foo/bar/qux.rs")), None);
|
||||
/// ```
|
||||
pub fn find_file(&self, path: Path, repo: &Repository) -> Option<Oid> {
|
||||
/// Find the `Oid`, for a [`File`], found at `path`, if it exists.
|
||||
pub fn find_file(
|
||||
&self,
|
||||
path: Path,
|
||||
repo: &Repository,
|
||||
) -> Result<Option<Oid>, crate::git::Error> {
|
||||
let path_buf: std::path::PathBuf = (&path).into();
|
||||
let entry = match self.get_path(path_buf.as_path(), repo) {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match entry {
|
||||
DirectoryEntry::File(f) => Some(f.oid),
|
||||
DirectoryEntry::Directory(_) => None,
|
||||
}
|
||||
Ok(match self.find_entry(path_buf.as_path(), repo)? {
|
||||
Some(Entry::File(f)) => Some(f.id),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Find a `Directory` in the directory given the [`Path`] to the
|
||||
/// `Directory`.
|
||||
///
|
||||
/// # Failures
|
||||
///
|
||||
/// This operation fails if the path does not lead to the `Directory`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Search for directories in the path:
|
||||
/// * `foo`
|
||||
/// * `foo/bar`
|
||||
/// * `foo/baz`
|
||||
///
|
||||
/// ```
|
||||
/// use radicle_surf::file_system::{Directory, File};
|
||||
/// use radicle_surf::file_system::unsound;
|
||||
///
|
||||
/// let file = File::new(b"module Banana ...");
|
||||
///
|
||||
/// let mut directory = Directory::root();
|
||||
/// directory.insert_file(unsound::path::new("foo/bar/baz.rs"), file.clone());
|
||||
///
|
||||
/// // Can find the first level
|
||||
/// assert!(directory.find_directory(unsound::path::new("foo")).is_some());
|
||||
///
|
||||
/// // Can find the second level
|
||||
/// assert!(directory.find_directory(unsound::path::new("foo/bar")).is_some());
|
||||
///
|
||||
/// // Cannot find 'baz' since it does not exist
|
||||
/// assert!(directory.find_directory(unsound::path::new("foo/baz")).is_none());
|
||||
///
|
||||
/// // 'baz.rs' is a file and not a directory
|
||||
/// assert!(directory.find_directory(unsound::path::new("foo/bar/baz.rs")).is_none());
|
||||
/// ```
|
||||
pub fn find_directory(&self, path: Path, repo: &Repository) -> Option<Self> {
|
||||
/// Find the `Directory` found at `path`, if it exists.
|
||||
pub fn find_directory(
|
||||
&self,
|
||||
path: Path,
|
||||
repo: &Repository,
|
||||
) -> Result<Option<Self>, crate::git::Error> {
|
||||
let path_buf: std::path::PathBuf = (&path).into();
|
||||
let entry = match self.get_path(path_buf.as_path(), repo) {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match entry {
|
||||
DirectoryEntry::File(_) => None,
|
||||
DirectoryEntry::Directory(d) => Some(d),
|
||||
}
|
||||
Ok(match self.find_entry(path_buf.as_path(), repo)? {
|
||||
Some(Entry::Directory(d)) => Some(d),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(fintan): This is going to be a bit trickier so going to leave it out for
|
||||
@@ -274,21 +319,43 @@ impl Directory {
|
||||
|
||||
/// Get the total size, in bytes, of a `Directory`. The size is
|
||||
/// the sum of all files that can be reached from this `Directory`.
|
||||
pub fn size(&self, repo: &Repository) -> Result<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;
|
||||
pub fn size(&self, repo: &Repository) -> Result<usize, error::Directory> {
|
||||
self.traverse(repo, 0, &mut |size, entry| match entry {
|
||||
Entry::File(file) => Ok(size + file.content(repo)?.size()),
|
||||
Entry::Directory(dir) => Ok(size + dir.size(repo)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Traverse the entire `Directory` using the `initial`
|
||||
/// accumulator and the function `f`.
|
||||
///
|
||||
/// For each [`Entry::Directory`] this will recursively call
|
||||
/// [`Directory::traverse`] and obtain its [`Entries`].
|
||||
///
|
||||
/// `Error` is the error type of the fallible function.
|
||||
/// `B` is the type of the accumulator.
|
||||
/// `F` is the fallible function that takes the accumulator and
|
||||
/// the next [`Entry`], possibly providing the next accumulator
|
||||
/// value.
|
||||
pub fn traverse<Error, B, F>(
|
||||
&self,
|
||||
repo: &Repository,
|
||||
initial: B,
|
||||
f: &mut F,
|
||||
) -> Result<B, Error>
|
||||
where
|
||||
Error: From<error::Directory>,
|
||||
F: FnMut(B, &Entry) -> Result<B, Error>,
|
||||
{
|
||||
self.entries(repo)?
|
||||
.entries()
|
||||
.try_fold(initial, |acc, entry| match entry {
|
||||
Entry::File(_) => f(acc, entry),
|
||||
Entry::Directory(directory) => {
|
||||
let acc = directory.traverse(repo, acc, f)?;
|
||||
f(acc, entry)
|
||||
},
|
||||
DirectoryEntry::Directory(d) => {
|
||||
size += d.size(repo)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(size)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +363,6 @@ impl Revision for Directory {
|
||||
type Error = Infallible;
|
||||
|
||||
fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
|
||||
Ok(self.oid)
|
||||
Ok(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
convert::TryFrom,
|
||||
path::PathBuf,
|
||||
str,
|
||||
};
|
||||
use std::{collections::BTreeSet, convert::TryFrom, path::PathBuf, str};
|
||||
|
||||
use directory::{Directory, FileContent};
|
||||
use git_ref_format::{refspec::QualifiedPattern, Qualified};
|
||||
use radicle_git_ext::Oid;
|
||||
use thiserror::Error;
|
||||
@@ -30,7 +24,7 @@ use thiserror::Error;
|
||||
use crate::{
|
||||
diff::{self, *},
|
||||
file_system,
|
||||
file_system::{directory, DirectoryEntry, Label},
|
||||
file_system::{directory::FileContent, Directory, Label},
|
||||
git::{
|
||||
commit,
|
||||
glob,
|
||||
@@ -195,60 +189,10 @@ impl Repository {
|
||||
let tree = git2_commit.as_object().peel_to_tree()?;
|
||||
Ok(Directory {
|
||||
name: Label::root(),
|
||||
oid: tree.id().into(),
|
||||
id: tree.id().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves the content of a directory.
|
||||
pub(crate) fn directory_get(
|
||||
&self,
|
||||
d: &Directory,
|
||||
) -> Result<BTreeMap<Label, DirectoryEntry>, Error> {
|
||||
let git2_tree = self.inner.find_tree(d.oid.into())?;
|
||||
let map = self.tree_first_level(git2_tree)?;
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Returns a map of the first level entries in `tree`.
|
||||
fn tree_first_level(&self, tree: git2::Tree) -> Result<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
|
||||
/// `rev`.
|
||||
pub fn last_commit<C: ToCommit>(
|
||||
@@ -294,12 +238,6 @@ impl Repository {
|
||||
Ok(FileContent::new(blob))
|
||||
}
|
||||
|
||||
/// Return the size of a file
|
||||
pub(crate) fn file_size(&self, oid: Oid) -> Result<usize, Error> {
|
||||
let blob = self.inner.find_blob(oid.into())?;
|
||||
Ok(blob.size())
|
||||
}
|
||||
|
||||
/// Retrieves the file with `path` in this commit.
|
||||
pub fn get_commit_file<R: Revision>(
|
||||
&self,
|
||||
|
||||
@@ -30,7 +30,11 @@ pub use blob::{Blob, BlobContent};
|
||||
pub mod tree;
|
||||
pub use tree::{tree, Tree, TreeEntry};
|
||||
|
||||
use crate::{commit, file_system, git};
|
||||
use crate::{
|
||||
commit,
|
||||
file_system::{self, directory},
|
||||
git,
|
||||
};
|
||||
|
||||
/// Git object types.
|
||||
///
|
||||
@@ -84,6 +88,9 @@ impl Serialize for Info {
|
||||
/// An error reported by object types.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Directory(#[from] directory::error::Directory),
|
||||
|
||||
/// An error occurred during a file system operation.
|
||||
#[error(transparent)]
|
||||
FileSystem(#[from] file_system::Error),
|
||||
|
||||
@@ -136,7 +136,7 @@ where
|
||||
let p = file_system::Path::from_str(path)?;
|
||||
|
||||
let file = root
|
||||
.find_file(p.clone(), repo)
|
||||
.find_file(p.clone(), repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(p.clone()))?;
|
||||
|
||||
let mut commit_path = file_system::Path::root();
|
||||
|
||||
@@ -29,7 +29,7 @@ use serde::{
|
||||
|
||||
use crate::{
|
||||
commit,
|
||||
file_system::{self, DirectoryEntry},
|
||||
file_system::{self, directory},
|
||||
git::Repository,
|
||||
object::{Error, Info, ObjectType},
|
||||
revision::Revision,
|
||||
@@ -111,41 +111,40 @@ pub fn tree(
|
||||
root_dir
|
||||
} else {
|
||||
root_dir
|
||||
.find_directory(path.clone(), repo)
|
||||
.find_directory(path.clone(), repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.clone()))?
|
||||
};
|
||||
|
||||
let entries_results: Result<Vec<TreeEntry>, Error> = prefix_dir
|
||||
.contents(repo)?
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let entry_path = if path.is_root() {
|
||||
file_system::Path::new(entry.label().clone())
|
||||
} else {
|
||||
let mut p = path.clone();
|
||||
p.push(entry.label().clone());
|
||||
p
|
||||
};
|
||||
let mut commit_path = file_system::Path::root();
|
||||
commit_path.append(entry_path.clone());
|
||||
let mut entries =
|
||||
prefix_dir
|
||||
.entries(repo)?
|
||||
.iter()
|
||||
.fold(Vec::new(), |mut entries, (label, entry)| {
|
||||
let entry_path = if path.is_root() {
|
||||
file_system::Path::new(label.clone())
|
||||
} else {
|
||||
let mut p = path.clone();
|
||||
p.push(label.clone());
|
||||
p
|
||||
};
|
||||
let mut commit_path = file_system::Path::root();
|
||||
commit_path.append(entry_path.clone());
|
||||
|
||||
let info = Info {
|
||||
name: entry.label().to_string(),
|
||||
object_type: match entry {
|
||||
DirectoryEntry::Directory(_) => ObjectType::Tree,
|
||||
DirectoryEntry::File { .. } => ObjectType::Blob,
|
||||
},
|
||||
last_commit: None,
|
||||
};
|
||||
let info = Info {
|
||||
name: label.to_string(),
|
||||
object_type: match entry {
|
||||
directory::Entry::Directory(_) => ObjectType::Tree,
|
||||
directory::Entry::File { .. } => ObjectType::Blob,
|
||||
},
|
||||
last_commit: None,
|
||||
};
|
||||
|
||||
Ok(TreeEntry {
|
||||
info,
|
||||
path: entry_path.to_string(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut entries = entries_results?;
|
||||
entries.push(TreeEntry {
|
||||
info,
|
||||
path: entry_path.to_string(),
|
||||
});
|
||||
entries
|
||||
});
|
||||
|
||||
// We want to ensure that in the response Tree entries come first. `Ord` being
|
||||
// derived on the enum ensures Variant declaration order.
|
||||
|
||||
@@ -32,7 +32,7 @@ mod path {
|
||||
mod directory {
|
||||
use git_ref_format::refname;
|
||||
use radicle_surf::{
|
||||
file_system::DirectoryEntry,
|
||||
file_system::directory,
|
||||
git::{Branch, Repository},
|
||||
};
|
||||
use std::path::Path;
|
||||
@@ -40,38 +40,38 @@ mod directory {
|
||||
const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
|
||||
#[test]
|
||||
fn directory_get_path() {
|
||||
fn directory_find_entry() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let root = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
|
||||
|
||||
// get_path for a file.
|
||||
// find_entry for a file.
|
||||
let path = Path::new("src/memory.rs");
|
||||
let entry = root.get_path(path, &repo).unwrap();
|
||||
assert!(matches!(entry, DirectoryEntry::File(_)));
|
||||
let entry = root.find_entry(path, &repo).unwrap();
|
||||
assert!(matches!(entry, Some(directory::Entry::File(_))));
|
||||
|
||||
// get_path for a directory.
|
||||
// find_entry 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(_)));
|
||||
let entry = root.find_entry(path, &repo).unwrap();
|
||||
assert!(matches!(entry, Some(directory::Entry::Directory(_))));
|
||||
|
||||
// get_path for a non-leaf directory and its relative path.
|
||||
// find_entry 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 entry = root.find_entry(path, &repo).unwrap();
|
||||
assert!(matches!(entry, Some(directory::Entry::Directory(_))));
|
||||
if let Some(directory::Entry::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(_)));
|
||||
let inner_entry = sub_dir.find_entry(inner_path, &repo).unwrap();
|
||||
assert!(matches!(inner_entry, Some(directory::Entry::File(_))));
|
||||
}
|
||||
|
||||
// get_path for non-existing file
|
||||
// find_entry 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());
|
||||
let result = root.find_entry(path, &repo).unwrap();
|
||||
assert_eq!(result, None);
|
||||
|
||||
// get_path for absolute path: fail.
|
||||
// find_entry for absolute path: fail.
|
||||
let path = Path::new("/src/memory.rs");
|
||||
let result = root.get_path(path, &repo);
|
||||
let result = root.find_entry(path, &repo);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ mod directory {
|
||||
*/
|
||||
|
||||
let path = Path::new("src");
|
||||
let entry = root.get_path(path, &repo).unwrap();
|
||||
assert!(matches!(entry, DirectoryEntry::Directory(_)));
|
||||
if let DirectoryEntry::Directory(d) = entry {
|
||||
let entry = root.find_entry(path, &repo).unwrap();
|
||||
assert!(matches!(entry, Some(directory::Entry::Directory(_))));
|
||||
if let Some(directory::Entry::Directory(d)) = entry {
|
||||
assert_eq!(16297, d.size(&repo).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
+24
-22
@@ -7,7 +7,7 @@
|
||||
use radicle_surf::git::{Author, Commit};
|
||||
use radicle_surf::{
|
||||
diff::*,
|
||||
file_system::{unsound, DirectoryEntry, Path},
|
||||
file_system::{directory, unsound, Path},
|
||||
git::{Branch, Error, Glob, Oid, Repository},
|
||||
};
|
||||
|
||||
@@ -697,7 +697,7 @@ mod reference {
|
||||
.unwrap();
|
||||
assert_eq!(tags.len(), 6);
|
||||
let root_dir = repo.root_dir(&tags[0]).unwrap();
|
||||
assert_eq!(root_dir.contents(&repo).unwrap().iter().count(), 1);
|
||||
assert_eq!(root_dir.entries(&repo).unwrap().entries().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -727,7 +727,7 @@ mod code_browsing {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
|
||||
let root_dir = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
|
||||
let count = println_dir(&root_dir, &repo, 0);
|
||||
let count = println_dir(&root_dir, &repo);
|
||||
|
||||
assert_eq!(count, 36); // Check total file count.
|
||||
|
||||
@@ -735,16 +735,20 @@ mod code_browsing {
|
||||
/// 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, repo: &Repository, indent_level: usize) -> i32 {
|
||||
let mut count = 0;
|
||||
for item in dir.contents(repo).unwrap().iter() {
|
||||
println!("> {}{}", " ".repeat(indent_level * 4), &item.label());
|
||||
count += 1;
|
||||
if let DirectoryEntry::Directory(sub_dir) = item {
|
||||
count += println_dir(sub_dir, repo, indent_level + 1);
|
||||
}
|
||||
}
|
||||
count
|
||||
fn println_dir(dir: &Directory, repo: &Repository) -> i32 {
|
||||
dir.traverse::<directory::error::Directory, _, _>(
|
||||
repo,
|
||||
(0, 0),
|
||||
&mut |(count, indent_level), entry| {
|
||||
println!("> {}{}", " ".repeat(indent_level * 4), entry.label());
|
||||
match entry {
|
||||
directory::Entry::File(_) => Ok((count + 1, indent_level)),
|
||||
directory::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,20 +756,18 @@ mod code_browsing {
|
||||
fn browse_repo_lazily() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let root_dir = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
|
||||
let count = root_dir.contents(&repo).unwrap().iter().count();
|
||||
let count = root_dir.entries(&repo).unwrap().entries().count();
|
||||
assert_eq!(count, 8);
|
||||
let count = traverse(&root_dir, &repo);
|
||||
assert_eq!(count, 36);
|
||||
|
||||
fn traverse(dir: &Directory, repo: &Repository) -> 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
|
||||
dir.traverse::<directory::error::Directory, _, _>(
|
||||
repo,
|
||||
0,
|
||||
&mut |count, _| Ok(count + 1),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user