mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-18 19:10:31 +08:00
Merge remote-tracking branch 'han/dogfood'
* han/dogfood: refactored Directory, File, Tree and Blob. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -31,7 +31,7 @@ use git2::Blob;
|
||||
use radicle_git_ext::{is_not_found_err, Oid};
|
||||
use radicle_std_ext::result::ResultExt as _;
|
||||
|
||||
use crate::git::{Repository, Revision};
|
||||
use crate::git::{Commit, Repository, Revision};
|
||||
|
||||
pub mod error {
|
||||
use thiserror::Error;
|
||||
@@ -79,6 +79,8 @@ pub struct File {
|
||||
prefix: PathBuf,
|
||||
/// The object identifier of the git blob of this file.
|
||||
id: Oid,
|
||||
/// The commit that created this file version.
|
||||
last_commit: Commit,
|
||||
}
|
||||
|
||||
impl File {
|
||||
@@ -88,14 +90,19 @@ impl File {
|
||||
/// so should not end in `name`.
|
||||
///
|
||||
/// The `id` must point to a git blob.
|
||||
pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
|
||||
pub(crate) fn new(name: String, prefix: PathBuf, id: Oid, last_commit: Commit) -> Self {
|
||||
debug_assert!(
|
||||
!prefix.ends_with(&name),
|
||||
"prefix = {:?}, name = {}",
|
||||
prefix,
|
||||
name
|
||||
);
|
||||
Self { name, prefix, id }
|
||||
Self {
|
||||
name,
|
||||
prefix,
|
||||
id,
|
||||
last_commit,
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of this `File`.
|
||||
@@ -256,12 +263,26 @@ impl Entry {
|
||||
pub(crate) fn from_entry(
|
||||
entry: &git2::TreeEntry,
|
||||
path: PathBuf,
|
||||
repo: &Repository,
|
||||
parent_commit: Oid,
|
||||
) -> Result<Option<Self>, error::Entry> {
|
||||
let name = entry.name().ok_or(error::Entry::Utf8Error)?.to_string();
|
||||
let id = entry.id().into();
|
||||
let escaped_name = name.replace('\\', r"\\");
|
||||
let entry_path = path.join(escaped_name);
|
||||
// FIXME: I don't like to use FIXME, but here it is. I would
|
||||
// like to simplify the error definitions and then fix these
|
||||
// unwrap(s).
|
||||
let last_commit = repo
|
||||
.last_commit(&entry_path, parent_commit)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
Ok(entry.kind().and_then(|kind| match kind {
|
||||
git2::ObjectType::Tree => Some(Self::Directory(Directory::new(name, path, id))),
|
||||
git2::ObjectType::Blob => Some(Self::File(File::new(name, path, id))),
|
||||
git2::ObjectType::Tree => {
|
||||
Some(Self::Directory(Directory::new(name, path, id, last_commit)))
|
||||
},
|
||||
git2::ObjectType::Blob => Some(Self::File(File::new(name, path, id, last_commit))),
|
||||
_ => None,
|
||||
}))
|
||||
}
|
||||
@@ -285,16 +306,18 @@ pub struct Directory {
|
||||
prefix: PathBuf,
|
||||
/// The object identifier of the git tree of this directory.
|
||||
id: Oid,
|
||||
/// The commit that created this directory version.
|
||||
last_commit: Commit,
|
||||
}
|
||||
|
||||
const ROOT_DIR: &str = "";
|
||||
|
||||
impl Directory {
|
||||
/// Creates a directory given its `id`.
|
||||
/// Creates a directory given its `tree_id`.
|
||||
///
|
||||
/// The `name` and `prefix` are both set to be empty.
|
||||
///
|
||||
/// The `id` must point to a `git` tree.
|
||||
pub(crate) fn root(id: Oid) -> Self {
|
||||
Self::new("".to_string(), PathBuf::new(), id)
|
||||
pub(crate) fn root(tree_id: Oid, repo_commit: Commit) -> Self {
|
||||
Self::new(ROOT_DIR.to_string(), PathBuf::new(), tree_id, repo_commit)
|
||||
}
|
||||
|
||||
/// Creates a directory given its `name` and `id`.
|
||||
@@ -303,14 +326,19 @@ impl Directory {
|
||||
/// so should not end in `name`.
|
||||
///
|
||||
/// The `id` must point to a `git` tree.
|
||||
pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
|
||||
pub(crate) fn new(name: String, prefix: PathBuf, id: Oid, last_commit: Commit) -> Self {
|
||||
debug_assert!(
|
||||
name.is_empty() || !prefix.ends_with(&name),
|
||||
"prefix = {:?}, name = {}",
|
||||
prefix,
|
||||
name
|
||||
);
|
||||
Self { name, prefix, id }
|
||||
Self {
|
||||
name,
|
||||
prefix,
|
||||
id,
|
||||
last_commit,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of the current `Directory`.
|
||||
@@ -354,9 +382,10 @@ impl Directory {
|
||||
let mut error = None;
|
||||
let path = self.path();
|
||||
|
||||
// Walks only the first level of entries.
|
||||
tree.walk(git2::TreeWalkMode::PreOrder, |_, entry| {
|
||||
match Entry::from_entry(entry, path.clone()) {
|
||||
// Walks only the first level of entries. And `_entry_path` is always
|
||||
// empty for the first level.
|
||||
tree.walk(git2::TreeWalkMode::PreOrder, |_entry_path, entry| {
|
||||
match Entry::from_entry(entry, path.clone(), repo, self.last_commit.id) {
|
||||
Ok(Some(entry)) => match entry {
|
||||
Entry::File(_) => {
|
||||
entries.insert(entry.name().clone(), entry);
|
||||
@@ -382,6 +411,11 @@ impl Directory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the last commit that created or modified this directory.
|
||||
pub fn last_commit(&self) -> &Commit {
|
||||
&self.last_commit
|
||||
}
|
||||
|
||||
/// Find the [`Entry`] found at `path`, if it exists.
|
||||
pub fn find_entry<P>(
|
||||
&self,
|
||||
@@ -401,9 +435,13 @@ impl Directory {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| error::Directory::InvalidPath(path.to_string_lossy().to_string()))?;
|
||||
let root_path = self.path().join(parent);
|
||||
|
||||
Ok(entry
|
||||
.and_then(|entry| Entry::from_entry(&entry, parent.to_path_buf()).transpose())
|
||||
.and_then(|entry| {
|
||||
Entry::from_entry(&entry, root_path.to_path_buf(), repo, self.last_commit.id)
|
||||
.transpose()
|
||||
})
|
||||
.transpose()
|
||||
.unwrap())
|
||||
}
|
||||
@@ -424,6 +462,8 @@ impl Directory {
|
||||
}
|
||||
|
||||
/// Find the `Directory` found at `path`, if it exists.
|
||||
///
|
||||
/// If `path` is `ROOT_DIR` (i.e. an empty path), returns self.
|
||||
pub fn find_directory<P>(
|
||||
&self,
|
||||
path: &P,
|
||||
@@ -432,6 +472,10 @@ impl Directory {
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
if path.as_ref() == Path::new(ROOT_DIR) {
|
||||
return Ok(Some(self.clone()));
|
||||
}
|
||||
|
||||
Ok(match self.find_entry(path, repo)? {
|
||||
Some(Entry::Directory(d)) => Some(d),
|
||||
_ => None,
|
||||
|
||||
@@ -62,7 +62,7 @@ impl<'a> History<'a> {
|
||||
///
|
||||
/// Note that it is possible that a filtered History becomes empty,
|
||||
/// even though calling `.head()` still returns the original head.
|
||||
pub fn by_path<P>(mut self, path: P) -> Self
|
||||
pub fn by_path<P>(mut self, path: &P) -> Self
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
|
||||
@@ -28,7 +28,10 @@ use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
diff::{self, *},
|
||||
file_system::{directory::FileContent, Directory},
|
||||
file_system::{
|
||||
directory::{self, File, FileContent},
|
||||
Directory,
|
||||
},
|
||||
git::{
|
||||
commit,
|
||||
glob,
|
||||
@@ -44,6 +47,7 @@ use crate::{
|
||||
Tag,
|
||||
ToCommit,
|
||||
},
|
||||
source::{self, commit::Header, Blob, Tree, TreeEntry},
|
||||
};
|
||||
|
||||
pub mod iter;
|
||||
@@ -66,6 +70,10 @@ pub enum Error {
|
||||
Diff(#[from] diff::git::error::Diff),
|
||||
/// A wrapper around the generic [`git2::Error`].
|
||||
#[error(transparent)]
|
||||
Directory(#[from] directory::error::Directory),
|
||||
#[error(transparent)]
|
||||
File(#[from] directory::error::File),
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Glob(#[from] glob::Error),
|
||||
@@ -249,12 +257,72 @@ impl Repository {
|
||||
.map_err(|err| Error::ToCommit(err.into()))?;
|
||||
let git2_commit = self.inner.find_commit((commit.id).into())?;
|
||||
let tree = git2_commit.as_object().peel_to_tree()?;
|
||||
Ok(Directory::root(tree.id().into()))
|
||||
Ok(Directory::root(tree.id().into(), commit))
|
||||
}
|
||||
|
||||
/// Returns a [`Directory`] for `path` in `commit`.
|
||||
pub fn directory<C: ToCommit, P: AsRef<Path>>(
|
||||
&self,
|
||||
commit: C,
|
||||
path: &P,
|
||||
) -> Result<Directory, Error> {
|
||||
let root = self.root_dir(commit)?;
|
||||
root.find_directory(path, self)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
|
||||
}
|
||||
|
||||
/// Returns a [`File`] for `path` in `commit`.
|
||||
pub fn file<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<File, Error> {
|
||||
let root = self.root_dir(commit)?;
|
||||
root.find_file(path, self)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))
|
||||
}
|
||||
|
||||
/// Returns a [`Tree`] for `path` in `commit`.
|
||||
pub fn tree<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Tree, Error> {
|
||||
let commit = commit
|
||||
.to_commit(self)
|
||||
.map_err(|e| Error::ToCommit(e.into()))?;
|
||||
let dir = self.directory(commit.id, path)?;
|
||||
let mut entries = dir
|
||||
.entries(self)?
|
||||
.map(|en| {
|
||||
let name = en.name().to_string();
|
||||
let path = en.path();
|
||||
let commit = self
|
||||
.last_commit(&path, commit.id)?
|
||||
.ok_or(Error::PathNotFound(path))?;
|
||||
let commit_header = Header::from(commit);
|
||||
Ok(TreeEntry::new(name, en.into(), commit_header))
|
||||
})
|
||||
.collect::<Result<Vec<TreeEntry>, Error>>()?;
|
||||
entries.sort();
|
||||
|
||||
let last_commit = self
|
||||
.last_commit(path, commit)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
|
||||
let header = source::commit::Header::from(last_commit);
|
||||
Ok(Tree::new(dir.id(), entries, header))
|
||||
}
|
||||
|
||||
/// Returns a [`Blob`] for `path` in `commit`.
|
||||
pub fn blob<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Blob, Error> {
|
||||
let commit = commit
|
||||
.to_commit(self)
|
||||
.map_err(|e| Error::ToCommit(e.into()))?;
|
||||
let file = self.file(commit.id, path)?;
|
||||
let last_commit = self
|
||||
.last_commit(path, commit)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.as_ref().to_path_buf()))?;
|
||||
let header = source::commit::Header::from(last_commit);
|
||||
|
||||
let content = file.content(self)?;
|
||||
Ok(Blob::new(file.id(), content.as_bytes(), header))
|
||||
}
|
||||
|
||||
/// Returns the last commit, if exists, for a `path` in the history of
|
||||
/// `rev`.
|
||||
pub fn last_commit<P, C>(&self, path: P, rev: C) -> Result<Option<Commit>, Error>
|
||||
pub fn last_commit<P, C>(&self, path: &P, rev: C) -> Result<Option<Commit>, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
C: ToCommit,
|
||||
@@ -444,7 +512,7 @@ impl Repository {
|
||||
|
||||
let mut opts = git2::DiffOptions::new();
|
||||
if let Some(path) = path {
|
||||
opts.pathspec(path);
|
||||
opts.pathspec(path.to_string_lossy().to_string());
|
||||
// We're skipping the binary pass because we won't be inspecting deltas.
|
||||
opts.skip_binary_check(true);
|
||||
}
|
||||
|
||||
@@ -18,70 +18,33 @@
|
||||
//! Represents git object type 'blob', i.e. actual file contents.
|
||||
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
|
||||
|
||||
use std::{path::Path, str};
|
||||
use std::str;
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
file_system::{File, FileContent},
|
||||
git::{self, Repository},
|
||||
source::{commit, object::Error},
|
||||
};
|
||||
use crate::source::commit;
|
||||
|
||||
/// File data abstraction.
|
||||
/// Represents a git blob object.
|
||||
pub struct Blob {
|
||||
pub file: File,
|
||||
pub content: BlobContent,
|
||||
pub commit: Option<commit::Header>,
|
||||
id: Oid,
|
||||
content: BlobContent,
|
||||
commit: commit::Header,
|
||||
}
|
||||
|
||||
impl Blob {
|
||||
/// Returns the [`Blob`] for a file at `revision` under `path`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return [`Error`] if the project doesn't exist or a surf interaction
|
||||
/// fails.
|
||||
pub fn new<P, R>(repo: &Repository, revision: &R, path: &P) -> Result<Blob, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
R: git::Revision,
|
||||
{
|
||||
Self::make_blob(repo, revision, path, |c| BlobContent::from(c))
|
||||
}
|
||||
|
||||
fn make_blob<P, R>(
|
||||
repo: &Repository,
|
||||
revision: &R,
|
||||
path: &P,
|
||||
content: impl FnOnce(FileContent) -> BlobContent,
|
||||
) -> Result<Blob, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
R: git::Revision,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let root = repo.root_dir(revision)?;
|
||||
|
||||
let file = root
|
||||
.find_file(&path, repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?;
|
||||
|
||||
let last_commit = repo
|
||||
.last_commit(path, revision)?
|
||||
.map(|c| commit::Header::from(&c));
|
||||
|
||||
let content = content(file.content(repo)?);
|
||||
|
||||
Ok(Blob {
|
||||
file,
|
||||
pub(crate) fn new(id: Oid, content: &[u8], commit: commit::Header) -> Self {
|
||||
let content = BlobContent::from(content);
|
||||
Self {
|
||||
id,
|
||||
content,
|
||||
commit: last_commit,
|
||||
})
|
||||
commit,
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates if the content of the [`Blob`] is binary.
|
||||
@@ -89,6 +52,19 @@ impl Blob {
|
||||
pub fn is_binary(&self) -> bool {
|
||||
matches!(self.content, BlobContent::Binary(_))
|
||||
}
|
||||
|
||||
pub fn object_id(&self) -> Oid {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &BlobContent {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the commit that created this blob.
|
||||
pub fn commit(&self) -> &commit::Header {
|
||||
&self.commit
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -102,8 +78,6 @@ impl Serialize for Blob {
|
||||
state.serialize_field("binary", &self.is_binary())?;
|
||||
state.serialize_field("content", &self.content)?;
|
||||
state.serialize_field("lastCommit", &self.commit)?;
|
||||
state.serialize_field("name", &self.file.name())?;
|
||||
state.serialize_field("path", &self.file.path())?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
@@ -117,6 +91,24 @@ pub enum BlobContent {
|
||||
Binary(Vec<u8>),
|
||||
}
|
||||
|
||||
impl BlobContent {
|
||||
/// Returns the size of this `BlobContent`.
|
||||
pub fn size(&self) -> usize {
|
||||
match self {
|
||||
Self::Plain(content) => content.len(),
|
||||
Self::Binary(bytes) => bytes.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the content as bytes.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Plain(content) => content.as_bytes(),
|
||||
Self::Binary(bytes) => &bytes[..],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for BlobContent {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
@@ -133,12 +125,11 @@ impl Serialize for BlobContent {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileContent<'_>> for BlobContent {
|
||||
fn from(content: FileContent) -> Self {
|
||||
let content = content.as_bytes();
|
||||
match str::from_utf8(content) {
|
||||
impl From<&[u8]> for BlobContent {
|
||||
fn from(bytes: &[u8]) -> Self {
|
||||
match str::from_utf8(bytes) {
|
||||
Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
|
||||
Err(_) => BlobContent::Binary(content.to_owned()),
|
||||
Err(_) => BlobContent::Binary(bytes.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,125 +18,213 @@
|
||||
//! Represents git object type 'tree', i.e. like directory entries in Unix.
|
||||
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
|
||||
|
||||
use std::path::Path;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
file_system::{directory, Directory},
|
||||
git::{self, Repository},
|
||||
source::{commit, object::Error},
|
||||
};
|
||||
use crate::{file_system::directory, source::commit};
|
||||
|
||||
/// Result of a directory listing, carries other trees and blobs.
|
||||
/// Represents a tree object as in git. It is essentially the content of
|
||||
/// one directory. Note that multiple directories can have the same content,
|
||||
/// i.e. have the same tree object. Hence this struct does not embed its path.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tree {
|
||||
pub directory: Directory,
|
||||
pub commit: Option<commit::Header>,
|
||||
/// Entries listed in that tree result.
|
||||
pub entries: Vec<TreeEntry>,
|
||||
/// The object id of this tree.
|
||||
id: Oid,
|
||||
entries: Vec<TreeEntry>,
|
||||
/// The commit object that created this tree object.
|
||||
commit: commit::Header,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
/// Retrieve the [`Tree`] for the given `revision` and directory `prefix`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return [`Error`] if any of the surf interactions fail.
|
||||
pub fn new<P, R>(repo: &Repository, revision: &R, prefix: Option<&P>) -> Result<Tree, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
R: git::Revision,
|
||||
{
|
||||
let prefix = prefix.map(|p| p.as_ref());
|
||||
|
||||
let prefix_dir = match prefix {
|
||||
None => repo.root_dir(revision)?,
|
||||
Some(path) => repo
|
||||
.root_dir(revision)?
|
||||
.find_directory(&path, repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
|
||||
};
|
||||
|
||||
let mut entries = prefix_dir
|
||||
.entries(repo)?
|
||||
.entries()
|
||||
.cloned()
|
||||
.map(TreeEntry::from)
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort();
|
||||
|
||||
let last_commit = if prefix.is_none() {
|
||||
let history = repo.history(revision)?;
|
||||
Some(commit::Header::from(history.head()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Tree {
|
||||
/// Creates a new tree.
|
||||
pub(crate) fn new(id: Oid, entries: Vec<TreeEntry>, commit: commit::Header) -> Self {
|
||||
Self {
|
||||
id,
|
||||
entries,
|
||||
directory: prefix_dir,
|
||||
commit: last_commit,
|
||||
})
|
||||
commit,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_id(&self) -> Oid {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns the commit that created this tree.
|
||||
pub fn commit(&self) -> &commit::Header {
|
||||
&self.commit
|
||||
}
|
||||
|
||||
/// Returns the entries of the tree.
|
||||
pub fn entries(&self) -> &Vec<TreeEntry> {
|
||||
&self.entries
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Tree {
|
||||
/// Sample output:
|
||||
/// (for `<entry_1>` and `<entry_2>` sample output, see [`TreeEntry`])
|
||||
/// ```
|
||||
/// {
|
||||
/// "entries": [
|
||||
/// { <entry_1> },
|
||||
/// { <entry_2> },
|
||||
/// ],
|
||||
/// "lastCommit": {
|
||||
/// "author": {
|
||||
/// "email": "foobar@gmail.com",
|
||||
/// "name": "Foo Bar"
|
||||
/// },
|
||||
/// "committer": {
|
||||
/// "email": "noreply@github.com",
|
||||
/// "name": "GitHub"
|
||||
/// },
|
||||
/// "committerTime": 1582198877,
|
||||
/// "description": "A sample commit.",
|
||||
/// "sha1": "b57846bbc8ced6587bf8329fc4bce970eb7b757e",
|
||||
/// "summary": "Add a new sample"
|
||||
/// },
|
||||
/// "oid": "dd52e9f8dfe1d8b374b2a118c25235349a743dd2"
|
||||
/// }
|
||||
/// ```
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
const FIELDS: usize = 4;
|
||||
let mut state = serializer.serialize_struct("Tree", FIELDS)?;
|
||||
state.serialize_field("oid", &self.id)?;
|
||||
state.serialize_field("entries", &self.entries)?;
|
||||
state.serialize_field("lastCommit", &self.commit)?;
|
||||
state.serialize_field("name", &self.directory.name())?;
|
||||
state.serialize_field("path", &self.directory.path())?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry in a Tree result.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct TreeEntry {
|
||||
pub entry: directory::Entry,
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Entry {
|
||||
Tree(Oid),
|
||||
Blob(Oid),
|
||||
}
|
||||
|
||||
impl From<directory::Entry> for TreeEntry {
|
||||
fn from(entry: directory::Entry) -> Self {
|
||||
Self { entry }
|
||||
/// Entry in a Tree result.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TreeEntry {
|
||||
name: String,
|
||||
entry: Entry,
|
||||
|
||||
/// The commit object that created this entry object.
|
||||
commit: commit::Header,
|
||||
}
|
||||
|
||||
impl TreeEntry {
|
||||
pub(crate) fn new(name: String, entry: Entry, commit: commit::Header) -> Self {
|
||||
Self {
|
||||
name,
|
||||
entry,
|
||||
commit,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn entry(&self) -> &Entry {
|
||||
&self.entry
|
||||
}
|
||||
|
||||
pub fn is_tree(&self) -> bool {
|
||||
matches!(self.entry, Entry::Tree(_))
|
||||
}
|
||||
|
||||
pub fn commit(&self) -> &commit::Header {
|
||||
&self.commit
|
||||
}
|
||||
|
||||
pub fn object_id(&self) -> Oid {
|
||||
match self.entry {
|
||||
Entry::Blob(id) => id,
|
||||
Entry::Tree(id) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TreeEntry> for directory::Entry {
|
||||
fn from(TreeEntry { entry }: TreeEntry) -> Self {
|
||||
entry
|
||||
// To support `sort`.
|
||||
impl Ord for TreeEntry {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.name.cmp(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for TreeEntry {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TreeEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TreeEntry {}
|
||||
|
||||
impl From<directory::Entry> for Entry {
|
||||
fn from(entry: directory::Entry) -> Self {
|
||||
match entry {
|
||||
directory::Entry::File(f) => Entry::Blob(f.id()),
|
||||
directory::Entry::Directory(d) => Entry::Tree(d.id()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for TreeEntry {
|
||||
/// Sample output:
|
||||
/// ```
|
||||
/// {
|
||||
/// "kind": "blob",
|
||||
/// "lastCommit": {
|
||||
/// "author": {
|
||||
/// "email": "foobar@gmail.com",
|
||||
/// "name": "Foo Bar"
|
||||
/// },
|
||||
/// "committer": {
|
||||
/// "email": "noreply@github.com",
|
||||
/// "name": "GitHub"
|
||||
/// },
|
||||
/// "committerTime": 1578309972,
|
||||
/// "description": "This is a sample file",
|
||||
/// "sha1": "2873745c8f6ffb45c990eb23b491d4b4b6182f95",
|
||||
/// "summary": "Add a new sample"
|
||||
/// },
|
||||
/// "name": "Sample.rs",
|
||||
/// "oid": "6d6240123a8d8ea8a8376610168a0a4bcb96afd0"
|
||||
/// },
|
||||
/// ```
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
const FIELDS: usize = 4;
|
||||
let mut state = serializer.serialize_struct("TreeEntry", FIELDS)?;
|
||||
state.serialize_field("path", &self.entry.path())?;
|
||||
state.serialize_field("name", &self.entry.name())?;
|
||||
state.serialize_field("lastCommit", &None::<commit::Header>)?;
|
||||
state.serialize_field("name", &self.name)?;
|
||||
state.serialize_field(
|
||||
"kind",
|
||||
match self.entry {
|
||||
directory::Entry::File(_) => "blob",
|
||||
directory::Entry::Directory(_) => "tree",
|
||||
Entry::Blob(_) => "blob",
|
||||
Entry::Tree(_) => "tree",
|
||||
},
|
||||
)?;
|
||||
state.serialize_field("oid", &self.object_id())?;
|
||||
state.serialize_field("lastCommit", &self.commit)?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ mod directory {
|
||||
.find_directory(&Path::new("src"), &repo)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(src.path(), Path::new("src").to_path_buf());
|
||||
let src_contents: Vec<Entry> = src.entries(&repo).unwrap().collect();
|
||||
assert_eq!(src_contents.len(), 3);
|
||||
assert_eq!(src_contents[0].name(), "Eval.hs");
|
||||
@@ -112,4 +113,16 @@ mod directory {
|
||||
assert_eq!(16297, d.size(&repo).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_last_commit() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let root = repo.root_dir(Branch::local(refname!("dev"))).unwrap();
|
||||
let dir = root.find_directory(&"this/is", &repo).unwrap().unwrap();
|
||||
let last_commit = dir.last_commit();
|
||||
assert_eq!(
|
||||
last_commit.id.to_string(),
|
||||
"2429f097664f9af0c5b7b389ab998b2199ffa977"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +59,9 @@ fn test_file_history() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
|
||||
let path = Path::new("README.md");
|
||||
let mut file_history = history.by_path(path);
|
||||
let mut file_history = history.by_path(&path);
|
||||
let commit = file_history.next().unwrap().unwrap();
|
||||
let file = repo
|
||||
.get_commit_file(&commit.id, &Path::new("README.md"))
|
||||
.unwrap();
|
||||
let file = repo.get_commit_file(&commit.id, &path).unwrap();
|
||||
assert_eq!(file.size(), 67);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use git_ref_format::refname;
|
||||
use radicle_git_ext::Oid;
|
||||
@@ -18,7 +15,7 @@ fn readme_missing_and_memory() {
|
||||
|
||||
// memory.rs is commited later so it should not exist here.
|
||||
let memory_last_commit_oid = repo
|
||||
.last_commit(Path::new("src/memory.rs"), oid)
|
||||
.last_commit(&"src/memory.rs", oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -26,7 +23,7 @@ fn readme_missing_and_memory() {
|
||||
|
||||
// README.md exists in this commit.
|
||||
let readme_last_commit = repo
|
||||
.last_commit(Path::new("README.md"), oid)
|
||||
.last_commit(&"README.md", oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -44,7 +41,7 @@ fn folder_svelte() {
|
||||
let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();
|
||||
|
||||
let folder_svelte = repo
|
||||
.last_commit(Path::new("examples/Folder.svelte"), oid)
|
||||
.last_commit(&"examples/Folder.svelte", oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -62,10 +59,7 @@ fn nest_directory() {
|
||||
let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap();
|
||||
|
||||
let nested_directory_tree_commit_id = repo
|
||||
.last_commit(
|
||||
Path::new("this/is/a/really/deeply/nested/directory/tree"),
|
||||
oid,
|
||||
)
|
||||
.last_commit(&"this/is/a/really/deeply/nested/directory/tree", oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -85,13 +79,13 @@ fn can_get_last_commit_for_special_filenames() {
|
||||
let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();
|
||||
|
||||
let backslash_commit_id = repo
|
||||
.last_commit(Path::new(r"special/faux\\path"), oid)
|
||||
.last_commit(&r"special/faux\\path", oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
assert_eq!(backslash_commit_id, Some(expected_commit_id));
|
||||
|
||||
let ogre_commit_id = repo
|
||||
.last_commit(Path::new("special/👹👹👹"), oid)
|
||||
.last_commit(&"special/👹👹👹", oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
assert_eq!(ogre_commit_id, Some(expected_commit_id));
|
||||
@@ -103,7 +97,7 @@ fn root() {
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let rev = Branch::local(refname!("master"));
|
||||
let root_last_commit_id = repo
|
||||
.last_commit(PathBuf::new(), rev)
|
||||
.last_commit(&PathBuf::new(), rev)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -120,7 +114,7 @@ fn binary_file() {
|
||||
let repo = Repository::open(GIT_PLATINUM)
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
|
||||
let file_commit = history.by_path(Path::new("bin/cat")).next();
|
||||
let file_commit = history.by_path(&"bin/cat").next();
|
||||
assert!(file_commit.is_some());
|
||||
println!("file commit: {:?}", &file_commit);
|
||||
}
|
||||
|
||||
+118
-16
@@ -1,7 +1,5 @@
|
||||
use std::path::Path;
|
||||
|
||||
use git_ref_format::refname;
|
||||
use radicle_surf::{git::Repository, source};
|
||||
use radicle_surf::git::Repository;
|
||||
use serde_json::json;
|
||||
|
||||
const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
@@ -9,31 +7,135 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
#[test]
|
||||
fn tree_serialization() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let tree = source::Tree::new(
|
||||
&repo,
|
||||
&refname!("refs/heads/master"),
|
||||
Some(&Path::new("src")),
|
||||
)
|
||||
.unwrap();
|
||||
let tree = repo.tree(refname!("refs/heads/master"), &"src").unwrap();
|
||||
|
||||
let expected = json!({
|
||||
"entries": [
|
||||
{
|
||||
"kind": "blob",
|
||||
"lastCommit": null,
|
||||
"lastCommit": {
|
||||
"author": {
|
||||
"email": "fintan.halpenny@gmail.com",
|
||||
"name": "Fintan Halpenny"
|
||||
},
|
||||
"committer": {
|
||||
"email": "noreply@github.com",
|
||||
"name": "GitHub"
|
||||
},
|
||||
"committerTime": 1578309972,
|
||||
"description": "I want to have files under src that have separate commits.\r\nThat way src's latest commit isn't the same as all its files, instead it's the file that was touched last.",
|
||||
"sha1": "3873745c8f6ffb45c990eb23b491d4b4b6182f95",
|
||||
"summary": "Extend the docs (#2)"
|
||||
},
|
||||
"name": "Eval.hs",
|
||||
"path": "src/Eval.hs"
|
||||
"oid": "7d6240123a8d8ea8a8376610168a0a4bcb96afd0"
|
||||
},
|
||||
{
|
||||
"kind": "blob",
|
||||
"lastCommit": null,
|
||||
"lastCommit": {
|
||||
"author": {
|
||||
"email": "rudolfs@osins.org",
|
||||
"name": "Rūdolfs Ošiņš"
|
||||
},
|
||||
"committer": {
|
||||
"email": "rudolfs@osins.org",
|
||||
"name": "Rūdolfs Ošiņš"
|
||||
},
|
||||
"committerTime": 1575283266,
|
||||
"description": "",
|
||||
"sha1": "e24124b7538658220b5aaf3b6ef53758f0a106dc",
|
||||
"summary": "Move examples to \"src\""
|
||||
},
|
||||
"name": "memory.rs",
|
||||
"path": "src/memory.rs"
|
||||
"oid": "b84992d24be67536837f5ab45a943f1b3f501878"
|
||||
}
|
||||
],
|
||||
"lastCommit": null,
|
||||
"name": "src",
|
||||
"path": "src"
|
||||
"lastCommit": {
|
||||
"author": {
|
||||
"email": "rudolfs@osins.org",
|
||||
"name": "Rūdolfs Ošiņš"
|
||||
},
|
||||
"committer": {
|
||||
"email": "noreply@github.com",
|
||||
"name": "GitHub"
|
||||
},
|
||||
"committerTime": 1582198877,
|
||||
"description": "It was a bad idea to have an actual source file which is used by\r\nradicle-upstream in the fixtures repository. It gets in the way of\r\nlinting and editors pick it up as a regular source file by accident.",
|
||||
"sha1": "a57846bbc8ced6587bf8329fc4bce970eb7b757e",
|
||||
"summary": "Remove src/Folder.svelte (#3)"
|
||||
},
|
||||
"oid": "ed52e9f8dfe1d8b374b2a118c25235349a743dd2"
|
||||
});
|
||||
assert_eq!(serde_json::to_value(tree).unwrap(), expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_tree() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let tree = repo
|
||||
.tree("27acd68c7504755aa11023300890bb85bbd69d45", &"src")
|
||||
.unwrap();
|
||||
assert_eq!(tree.entries().len(), 3);
|
||||
|
||||
let commit_header = tree.commit();
|
||||
assert_eq!(
|
||||
commit_header.sha1.to_string(),
|
||||
"e24124b7538658220b5aaf3b6ef53758f0a106dc"
|
||||
);
|
||||
|
||||
let tree_oid = tree.object_id();
|
||||
assert_eq!(
|
||||
tree_oid.to_string(),
|
||||
"dbd5d80c64a00969f521b96401a315e9481e9561"
|
||||
);
|
||||
|
||||
let entries = tree.entries();
|
||||
assert_eq!(entries.len(), 3);
|
||||
let entry = &entries[0];
|
||||
assert!(!entry.is_tree());
|
||||
assert_eq!(entry.name(), "Eval.hs");
|
||||
assert_eq!(
|
||||
entry.object_id().to_string(),
|
||||
"8c7447d13b907aa994ac3a38317c1e9633bf0732"
|
||||
);
|
||||
let commit = entry.commit();
|
||||
assert_eq!(
|
||||
commit.sha1.to_string(),
|
||||
"e24124b7538658220b5aaf3b6ef53758f0a106dc"
|
||||
);
|
||||
|
||||
// Verify that an empty path works for getting the root tree.
|
||||
let root_tree = repo
|
||||
.tree("27acd68c7504755aa11023300890bb85bbd69d45", &"")
|
||||
.unwrap();
|
||||
assert_eq!(root_tree.entries().len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_blob() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let blob = repo
|
||||
.blob("27acd68c7504755aa11023300890bb85bbd69d45", &"src/memory.rs")
|
||||
.unwrap();
|
||||
|
||||
let blob_oid = blob.object_id();
|
||||
assert_eq!(
|
||||
blob_oid.to_string(),
|
||||
"b84992d24be67536837f5ab45a943f1b3f501878"
|
||||
);
|
||||
|
||||
let commit_header = blob.commit();
|
||||
assert_eq!(
|
||||
commit_header.sha1.to_string(),
|
||||
"e24124b7538658220b5aaf3b6ef53758f0a106dc"
|
||||
);
|
||||
|
||||
assert!(!blob.is_binary());
|
||||
|
||||
// Verify the blob content size matches with the file size of "memory.rs"
|
||||
let content = blob.content();
|
||||
assert_eq!(content.size(), 6253);
|
||||
|
||||
// Verify as_bytes.
|
||||
assert_eq!(content.as_bytes().len(), content.size());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user