radicle-surf: an updated design

- A new design document.
- Uses examples/diff.rs to test out some ideas.
- Removes the type Browser.
- Removes the type Snapshot.
This commit is contained in:
Han Xu
2022-10-04 22:01:12 -07:00
parent 93d92ede2d
commit cd9132c377
13 changed files with 414 additions and 1387 deletions
+3 -4
View File
@@ -18,14 +18,13 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use radicle_surf::{
file_system::{unsound, Path},
vcs::git::{Branch, Browser, Repository},
vcs::git::{Branch, Repository, Rev},
};
fn last_commit_comparison(c: &mut Criterion) {
let repo = Repository::new("./data/git-platinum")
.expect("Could not retrieve ./data/git-platinum as git repository");
let browser =
Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
let rev: Rev = Branch::local("master").into();
let mut group = c.benchmark_group("Last Commit");
for path in [
@@ -36,7 +35,7 @@ fn last_commit_comparison(c: &mut Criterion) {
.iter()
{
group.bench_with_input(BenchmarkId::new("", path), path, |b, path| {
b.iter(|| browser.last_commit(path.clone()))
b.iter(|| repo.as_ref().last_commit(path.clone(), &rev))
});
}
}
+47
View File
@@ -0,0 +1,47 @@
# An updated design for radicle-surf
Now we have ported the `radicle-surf` crate from its own github repo to be part of the `radicle-git` repo. We are taking this opportunity to refactor its design as well. Intuitively, `radicle-surf` provides an API so that one can use it to create a github-like UI for a git repo:
- Given a commit (or other types of ref), list the content: i.e. files and directories.
- Generate a diff between two commits.
- List the history of the commits.
- List refs: Branches, Tags.
The main goals of the changes are:
- make API simpler whenever possible.
- address open issues in the original `radicle-surf` repo as much as possible.
- not to shy away from being `git` specific. (i.e. not to consider supporting other VCS systems)
## Make API simpler
The current API has quite a bit accidental complexity that is not inherent with the requirements, especially when we can be git specific and don't care about other VCS systems.
### Remove the `Browser`
The type `Browser` is awkward as of today:
- it is not a source of truth of any information. For example, `list_branches` method is just a wrapper of `Repository::list_branches`.
- it takes in `History`, but really works at the `Snapshot` level.
- it is mutable but its state does not help much.
Can we just remove `Browser` and implement its functionalities using other types?
- For iteratoring the history, use `History`.
- For generating `Directory`, use `Repository` directly given a `Rev`.
- For accessing `Branch`, `Tag` or `Commit`, use `Repository`.
## Remove the `Snapshot`
A `Snapshot` should be really just a tree (or `Directory`) of a `Commit` in git. Currently it is a function that returns a `Directory`. Because it is OK to be git specific, we don't need to have this generic function to create a snapshot across different VCS systems.
The only `snapshot` function defined currently:
```Rust
let snapshot = Box::new(|repository: &RepositoryRef<'a>, history: &History| {
let tree = Self::get_tree(repository.repo_ref, history.0.first())?;
Ok(directory::Directory::from_hash_map(tree))
});
```
The above function can be easily implement as a method of `Repository`.
+8 -65
View File
@@ -19,37 +19,20 @@ extern crate radicle_surf;
use std::{env::Args, str::FromStr, time::Instant};
use nonempty::NonEmpty;
use radicle_git_ext::Oid;
use radicle_surf::{
diff::Diff,
file_system::Directory,
vcs::{git, History},
};
use radicle_surf::{diff::Diff, vcs::git};
fn main() {
let options = get_options_or_exit();
let repo = init_repository_or_exit(&options.path_to_repo);
let mut browser =
git::Browser::new(&repo, git::Branch::local("master")).expect("failed to create browser:");
match options.head_revision {
HeadRevision::Head => {
reset_browser_to_head_or_exit(&mut browser);
},
HeadRevision::Commit(id) => {
set_browser_history_or_exit(&mut browser, &id);
},
}
let head_directory = get_directory_or_exit(&browser);
set_browser_history_or_exit(&mut browser, &options.base_revision);
let base_directory = get_directory_or_exit(&browser);
let head_oid = match options.head_revision {
HeadRevision::Head => repo.as_ref().head_oid().unwrap(),
HeadRevision::Commit(id) => Oid::from_str(&id).unwrap(),
};
let base_oid = Oid::from_str(&options.base_revision).unwrap();
let now = Instant::now();
let elapsed_nanos = now.elapsed().as_nanos();
let diff = Diff::diff(base_directory, head_directory);
let diff = repo.as_ref().diff(base_oid, head_oid).unwrap();
print_diff_summary(&diff, elapsed_nanos);
}
@@ -73,46 +56,6 @@ fn init_repository_or_exit(path_to_repo: &str) -> git::Repository {
}
}
fn reset_browser_to_head_or_exit(browser: &mut git::Browser) {
if let Err(e) = browser.head() {
println!("Failed to set browser to HEAD: {:?}", e);
std::process::exit(1);
}
}
fn set_browser_history_or_exit(browser: &mut git::Browser, commit_id: &str) {
// TODO: Might consider to not require resetting to HEAD when history is not at
// HEAD
reset_browser_to_head_or_exit(browser);
if let Err(e) = set_browser_history(browser, commit_id) {
println!("Failed to set browser history: {:?}", e);
std::process::exit(1);
}
}
fn set_browser_history(browser: &mut git::Browser, commit_id: &str) -> Result<(), String> {
let oid = match Oid::from_str(commit_id) {
Ok(oid) => oid,
Err(e) => return Err(format!("{}", e)),
};
let commit = match browser.get().find_in_history(&oid, |artifact| artifact.id) {
Some(commit) => commit,
None => return Err(format!("Git commit not found: {}", commit_id)),
};
browser.set(History(NonEmpty::new(commit)));
Ok(())
}
fn get_directory_or_exit(browser: &git::Browser) -> Directory {
match browser.get_directory() {
Ok(dir) => dir,
Err(e) => {
println!("Failed to get directory: {:?}", e);
std::process::exit(1)
},
}
}
fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {
diff.created.iter().for_each(|created| {
println!("+++ {}", created.path);
@@ -131,7 +74,7 @@ fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {
diff.modified.len(),
diff.created.len() + diff.deleted.len() + diff.modified.len()
);
println!("diff took {} micros ", elapsed_nanos / 1000);
println!("diff took {} nanos ", elapsed_nanos);
}
struct Options {
+21 -25
View File
@@ -30,7 +30,10 @@ use crate::{
file_system,
person::Person,
revision::Revision,
vcs::git::{self, BranchName, Browser, Rev},
vcs::{
git::{self, BranchName, RepositoryRef, Rev},
Vcs,
},
};
use radicle_git_ext::Oid;
@@ -141,16 +144,12 @@ pub struct Commits {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
pub fn commit(browser: &mut Browser<'_>, sha1: Oid) -> Result<Commit, Error> {
browser.commit(sha1)?;
let history = browser.get();
let commit = history.first();
pub fn commit(repo: &RepositoryRef, sha1: Oid) -> Result<Commit, Error> {
let commit = repo.get_commit(sha1)?;
let diff = if let Some(parent) = commit.parents.first() {
browser.diff(*parent, sha1)?
repo.diff(*parent, sha1)?
} else {
browser.initial_diff(sha1)?
repo.initial_diff(sha1)?
};
let mut deletions = 0;
@@ -194,14 +193,14 @@ pub fn commit(browser: &mut Browser<'_>, sha1: Oid) -> Result<Commit, Error> {
}
}
let branches = browser
.revision_branches(sha1)?
let branches = repo
.revision_branches(&sha1)?
.into_iter()
.map(|b| b.name)
.collect();
Ok(Commit {
header: Header::from(commit),
header: Header::from(&commit),
stats: Stats {
additions,
deletions,
@@ -217,13 +216,9 @@ pub fn commit(browser: &mut Browser<'_>, sha1: Oid) -> Result<Commit, Error> {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
pub fn header(browser: &mut Browser<'_>, sha1: Oid) -> Result<Header, Error> {
browser.commit(sha1)?;
let history = browser.get();
let commit = history.first();
Ok(Header::from(commit))
pub fn header(repo: &RepositoryRef, sha1: Oid) -> Result<Header, Error> {
let commit = repo.get_commit(sha1)?;
Ok(Header::from(&commit))
}
/// Retrieves the [`Commit`] history for the given `revision`.
@@ -233,7 +228,7 @@ pub fn header(browser: &mut Browser<'_>, sha1: Oid) -> Result<Header, Error> {
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
pub fn commits<P>(
browser: &mut Browser<'_>,
repo: &RepositoryRef,
maybe_revision: Option<Revision<P>>,
) -> Result<Commits, Error>
where
@@ -241,12 +236,13 @@ where
{
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
if let Some(revision) = maybe_revision {
browser.rev(revision)?;
}
let rev: Rev = match maybe_revision {
Some(revision) => revision,
None => repo.head_oid()?.into(),
};
let headers = browser.get().iter().map(Header::from).collect();
let stats = browser.get_stats()?;
let stats = repo.get_stats(&rev)?;
let headers = repo.get_history(rev)?.iter().map(Header::from).collect();
Ok(Commits { headers, stats })
}
+1 -1
View File
@@ -25,7 +25,7 @@ use serde::{
};
pub mod blob;
pub use blob::{blob, Blob, BlobContent};
pub use blob::{Blob, BlobContent};
pub mod tree;
pub use tree::{tree, Tree, TreeEntry};
+9 -10
View File
@@ -32,9 +32,10 @@ use serde::{
use crate::{
commit,
file_system,
git::RepositoryRef,
object::{Error, Info, ObjectType},
revision::Revision,
vcs::git::{Browser, Rev},
vcs::git::Rev,
};
#[cfg(feature = "syntax")]
@@ -118,18 +119,18 @@ impl Serialize for BlobContent {
/// Will return [`Error`] if the project doesn't exist or a surf interaction
/// fails.
pub fn blob<P>(
browser: &mut Browser,
repo: &RepositoryRef,
maybe_revision: Option<Revision<P>>,
path: &str,
) -> Result<Blob, Error>
where
P: ToString,
{
make_blob(browser, maybe_revision, path, content)
make_blob(repo, maybe_revision, path, content)
}
fn make_blob<P, C>(
browser: &mut Browser,
repo: &RepositoryRef,
maybe_revision: Option<Revision<P>>,
path: &str,
content: C,
@@ -139,11 +140,9 @@ where
C: FnOnce(&[u8]) -> BlobContent,
{
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
if let Some(revision) = maybe_revision {
browser.rev(revision)?;
}
let revision = maybe_revision.unwrap();
let root = browser.get_directory()?;
let root = repo.snapshot(&revision)?;
let p = file_system::Path::from_str(path)?;
let file = root
@@ -153,8 +152,8 @@ where
let mut commit_path = file_system::Path::root();
commit_path.append(p.clone());
let last_commit = browser
.last_commit(commit_path)?
let last_commit = repo
.last_commit(commit_path, &revision)?
.map(|c| commit::Header::from(&c));
let (_rest, last) = p.split_last();
+12 -7
View File
@@ -29,9 +29,13 @@ use serde::{
use crate::{
commit,
file_system,
git::RepositoryRef,
object::{Error, Info, ObjectType},
revision::Revision,
vcs::git::{Browser, Rev},
vcs::{
git::{Branch, Rev},
Vcs,
},
};
/// Result of a directory listing, carries other trees and blobs.
@@ -86,7 +90,7 @@ impl Serialize for TreeEntry {
///
/// Will return [`Error`] if any of the surf interactions fail.
pub fn tree<P>(
browser: &mut Browser<'_>,
repo: &RepositoryRef,
maybe_revision: Option<Revision<P>>,
maybe_prefix: Option<String>,
) -> Result<Tree, Error>
@@ -96,9 +100,10 @@ where
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
let prefix = maybe_prefix.unwrap_or_default();
if let Some(revision) = maybe_revision {
browser.rev(revision)?;
}
let rev = match maybe_revision {
Some(r) => r,
None => Branch::local("main").into(),
};
let path = if prefix == "/" || prefix.is_empty() {
file_system::Path::root()
@@ -106,7 +111,7 @@ where
file_system::Path::from_str(&prefix)?
};
let root_dir = browser.get_directory()?;
let root_dir = repo.snapshot(&rev)?;
let prefix_dir = if path.is_root() {
root_dir
} else {
@@ -155,7 +160,7 @@ where
entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
let last_commit = if path.is_root() {
Some(commit::Header::from(browser.get().first()))
Some(commit::Header::from(repo.get_history(rev).unwrap().first()))
} else {
None
};
+14 -10
View File
@@ -27,8 +27,8 @@ use serde::{Deserialize, Serialize};
use radicle_git_ext::Oid;
use crate::{
git::BranchName,
vcs::git::{self, error::Error, Browser, RefScope, Rev, TagName},
git::{BranchName, RepositoryRef},
vcs::git::{self, error::Error, RefScope, Rev, TagName},
};
/// Types of a peer.
@@ -122,14 +122,14 @@ pub struct Revisions<P, U> {
///
/// * If we cannot get the branches from the `Browser`
pub fn remote<P, U>(
browser: &Browser,
repo: &RepositoryRef,
peer_id: P,
user: U,
) -> Result<Option<Revisions<P, U>>, Error>
where
P: Clone + ToString,
{
let remote_branches = browser.branch_names(Some(peer_id.clone()).into())?;
let remote_branches = repo.branch_names(Some(peer_id.clone()).into())?;
Ok(
NonEmpty::from_vec(remote_branches).map(|branches| Revisions {
peer_id,
@@ -150,12 +150,16 @@ where
/// # Errors
///
/// * If we cannot get the branches from the `Browser`
pub fn local<P, U>(browser: &Browser, peer_id: P, user: U) -> Result<Option<Revisions<P, U>>, Error>
pub fn local<P, U>(
repo: &RepositoryRef,
peer_id: P,
user: U,
) -> Result<Option<Revisions<P, U>>, Error>
where
P: Clone + ToString,
{
let local_branches = browser.branch_names(RefScope::Local)?;
let tags = browser.tag_names()?;
let local_branches = repo.branch_names(RefScope::Local)?;
let tags = repo.tag_names()?;
Ok(
NonEmpty::from_vec(local_branches).map(|branches| Revisions {
peer_id,
@@ -178,14 +182,14 @@ where
///
/// * If we cannot get the branches from the `Browser`
pub fn revisions<P, U>(
browser: &Browser,
repo: &RepositoryRef,
peer: Category<P, U>,
) -> Result<Option<Revisions<P, U>>, Error>
where
P: Clone + ToString,
{
match peer {
Category::Local { peer_id, user } => local(browser, peer_id, user),
Category::Remote { peer_id, user } => remote(browser, peer_id, user),
Category::Local { peer_id, user } => local(repo, peer_id, user),
Category::Remote { peer_id, user } => remote(repo, peer_id, user),
}
}
+3 -79
View File
@@ -15,10 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! A model of a general VCS. The components consist of a [`History`], a
//! [`Browser`], and a [`Vcs`] trait.
//! A model of a general VCS. The components consist of a [`History`]
//! and a [`Vcs`] trait.
use crate::file_system::directory::Directory;
// use crate::file_system::directory::Directory;
use nonempty::NonEmpty;
pub mod git;
@@ -135,82 +135,6 @@ impl<A> History<A> {
}
}
/// A Snapshot is a function that renders a `Directory` given
/// the `Repo` object and a `History` of artifacts.
type Snapshot<A, Repo, Error> = Box<dyn Fn(&Repo, &History<A>) -> Result<Directory, Error>>;
/// A `Browser` is a way of rendering a `History` into a
/// `Directory` snapshot, and the current `History` it is
/// viewing.
pub struct Browser<Repo, A, Error> {
snapshot: Snapshot<A, Repo, Error>,
history: History<A>,
repository: Repo,
}
impl<Repo, A, Error> Browser<Repo, A, Error> {
/// Get the current `History` the `Browser` is viewing.
pub fn get(&self) -> History<A>
where
A: Clone,
{
self.history.clone()
}
/// Get the current `History` the `Browser` is viewing as a ref.
pub fn as_history(&self) -> &History<A> {
&self.history
}
/// Set the `History` the `Browser` should view.
pub fn set(&mut self, history: History<A>) {
self.history = history;
}
/// Render the `Directory` for this `Browser`.
pub fn get_directory(&self) -> Result<Directory, Error> {
(self.snapshot)(&self.repository, &self.history)
}
/// Modify the `History` in this `Browser`.
pub fn modify<F>(&mut self, f: F)
where
F: Fn(&History<A>) -> History<A>,
{
self.history = f(&self.history)
}
/// Change the `Browser`'s view of `History` by modifying it, or
/// using the default `History` provided if the operation fails.
pub fn view_at<F>(&mut self, default_history: History<A>, f: F)
where
A: Clone,
F: Fn(&History<A>) -> Option<History<A>>,
{
self.modify(|history| f(history).unwrap_or_else(|| default_history.clone()))
}
}
impl<Repo, A, Error> Vcs<A, Error> for Browser<Repo, A, Error>
where
Repo: Vcs<A, Error>,
{
type HistoryId = Repo::HistoryId;
type ArtefactId = Repo::ArtefactId;
fn get_history(&self, identifier: Self::HistoryId) -> Result<History<A>, Error> {
self.repository.get_history(identifier)
}
fn get_histories(&self) -> Result<Vec<History<A>>, Error> {
self.repository.get_histories()
}
fn get_identifier(artifact: &A) -> Self::ArtefactId {
Repo::get_identifier(artifact)
}
}
pub(crate) trait GetVcs<Error>
where
Self: Sized,
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -21,7 +21,7 @@ pub use radicle_git_ext::Oid;
use std::{convert::TryFrom, fmt, str};
/// A `Namespace` value allows us to switch the git namespace of
/// [`super::Browser`].
/// a repo.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Namespace {
/// Since namespaces can be nested we have a vector of strings.
+185 -7
View File
@@ -18,29 +18,37 @@
use crate::{
diff::*,
file_system,
file_system::directory,
vcs,
vcs::{
git::{
error::*,
reference::{glob::RefGlob, Ref, Rev},
Branch,
BranchName,
Commit,
Namespace,
RefScope,
Signature,
Stats,
Tag,
TagName,
},
Vcs,
},
};
use nonempty::NonEmpty;
use radicle_git_ext::Oid;
use std::{collections::HashSet, convert::TryFrom, str};
use std::{
collections::{BTreeSet, HashMap, HashSet},
convert::TryFrom,
str,
};
/// This is for flagging to the `file_history` function that it should
/// stop at the first (i.e. Last) commit it finds for a file.
pub(super) enum CommitHistory {
Full,
_Full,
Last,
}
@@ -171,6 +179,81 @@ impl<'a> RepositoryRef<'a> {
Ok(self.repo_ref.revparse_single(oid)?.id().into())
}
/// Gets a snapshot of the repo as a Directory.
pub fn snapshot(&self, rev: &Rev) -> Result<directory::Directory, Error> {
let commit = self.rev_to_commit(rev)?;
let oid: Oid = commit.id().into();
let tree = self.get_tree(&oid)?;
Ok(directory::Directory::from_hash_map(tree))
}
/// Returns the last commit, if exists, for a `path` in the history of
/// `rev`.
pub fn last_commit(&self, path: file_system::Path, rev: &Rev) -> Result<Option<Commit>, Error> {
let git2_commit = self.rev_to_commit(rev)?;
let commit = Commit::try_from(git2_commit)?;
let file_history = self.file_history(&path, CommitHistory::Last, commit)?;
Ok(file_history.first().cloned())
}
/// Retrieves `Commit` identified by `oid`.
pub fn get_commit(&self, oid: Oid) -> Result<Commit, Error> {
let git2_commit = self.get_git2_commit(oid)?;
Commit::try_from(git2_commit)
}
/// Gets stats of `rev`.
pub fn get_stats(&self, rev: &Rev) -> Result<Stats, Error> {
let branches = self.list_branches(RefScope::Local)?.len();
let history = self.get_history(rev.clone())?;
let commits = history.len();
let contributors = history
.iter()
.cloned()
.map(|commit| (commit.author.name, commit.author.email))
.collect::<BTreeSet<_>>();
Ok(Stats {
branches,
commits,
contributors: contributors.len(),
})
}
/// Lists branch names with `filter`.
pub fn branch_names(&self, filter: RefScope) -> Result<Vec<BranchName>, Error> {
let mut branches = self
.list_branches(filter)?
.into_iter()
.map(|b| b.name)
.collect::<Vec<BranchName>>();
branches.sort();
Ok(branches)
}
/// Lists tag names in the local RefScope.
pub fn tag_names(&self) -> Result<Vec<TagName>, Error> {
let tag_names = self.list_tags(RefScope::Local)?;
let mut tags: Vec<TagName> = tag_names
.into_iter()
.map(|tag_name| tag_name.name())
.collect();
tags.sort();
Ok(tags)
}
/// Returns the Oid of the current HEAD
pub fn head_oid(&self) -> Result<Oid, Error> {
let head = self.repo_ref.head()?;
let head_commit = head.peel_to_commit()?;
Ok(head_commit.id().into())
}
pub(super) fn rev_to_commit(&self, rev: &Rev) -> Result<git2::Commit, Error> {
match rev {
Rev::Oid(oid) => Ok(self.repo_ref.find_commit((*oid).into())?),
@@ -178,7 +261,8 @@ impl<'a> RepositoryRef<'a> {
}
}
pub(super) fn switch_namespace(&self, namespace: &str) -> Result<(), Error> {
/// Switch to a `namespace`
pub fn switch_namespace(&self, namespace: &str) -> Result<(), Error> {
Ok(self.repo_ref.set_namespace(namespace)?)
}
@@ -189,7 +273,7 @@ impl<'a> RepositoryRef<'a> {
}
/// Build a [`History`] using the `head` reference.
pub(super) fn head(&self) -> Result<History, Error> {
pub fn head_history(&self) -> Result<History, Error> {
let head = self.repo_ref.head()?;
self.to_history(&head)
}
@@ -235,7 +319,7 @@ impl<'a> RepositoryRef<'a> {
/// `commit_oid` - The object ID of the commit
/// `field` - the name of the header field containing the signature block;
/// pass `None` to extract the default 'gpgsig'
pub(super) fn extract_signature(
pub fn extract_signature(
&self,
commit_oid: &Oid,
field: Option<&str>,
@@ -256,7 +340,8 @@ impl<'a> RepositoryRef<'a> {
}
}
pub(crate) fn revision_branches(&self, oid: &Oid) -> Result<Vec<Branch>, Error> {
/// Lists branches that are reachable from `oid`.
pub fn revision_branches(&self, oid: &Oid) -> Result<Vec<Branch>, Error> {
let local = RefGlob::LocalBranch.references(self)?;
let remote = RefGlob::RemoteBranch { remote: None }.references(self)?;
let mut references = local.iter().chain(remote.iter());
@@ -307,7 +392,7 @@ impl<'a> RepositoryRef<'a> {
commits.push(Commit::try_from(parent)?);
match &commit_history {
CommitHistory::Last => break,
CommitHistory::Full => {},
CommitHistory::_Full => {},
}
}
}
@@ -355,6 +440,99 @@ impl<'a> RepositoryRef<'a> {
Ok(diff)
}
fn update_file_map(
path: file_system::Path,
name: file_system::Label,
file: directory::File,
files: &mut HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
) {
files
.entry(path)
.and_modify(|entries| entries.push((name.clone(), file.clone())))
.or_insert_with(|| NonEmpty::new((name, file)));
}
/// Do a pre-order TreeWalk of the given commit. This turns a Tree
/// into a HashMap of Paths and a list of Files. We can then turn that
/// into a Directory.
fn get_tree(
&self,
oid: &Oid,
) -> Result<HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>, Error>
{
let mut file_paths_or_error: Result<
HashMap<file_system::Path, NonEmpty<(file_system::Label, directory::File)>>,
Error,
> = Ok(HashMap::new());
let commit = self.repo_ref.find_commit((*oid).into())?;
let tree = commit.as_object().peel_to_tree()?;
tree.walk(git2::TreeWalkMode::PreOrder, |s, entry| {
match self.tree_entry_to_file_and_path(s, entry) {
Ok((path, name, file)) => {
match file_paths_or_error.as_mut() {
Ok(files) => Self::update_file_map(path, name, file, files),
// We don't need to update, we want to keep the error.
Err(_err) => {},
}
git2::TreeWalkResult::Ok
},
Err(err) => match err {
// We want to continue if the entry was not a Blob.
TreeWalkError::NotBlob => git2::TreeWalkResult::Ok,
// We found a ObjectType::Commit (likely a submodule) and
// so we can skip it.
TreeWalkError::Commit => git2::TreeWalkResult::Ok,
// But we want to keep the error and abort otherwise.
TreeWalkError::Git(err) => {
file_paths_or_error = Err(err);
git2::TreeWalkResult::Abort
},
},
}
})?;
file_paths_or_error
}
fn tree_entry_to_file_and_path(
&self,
tree_path: &str,
entry: &git2::TreeEntry,
) -> Result<(file_system::Path, file_system::Label, directory::File), TreeWalkError> {
// Account for the "root" of git being the empty string
let path = if tree_path.is_empty() {
Ok(file_system::Path::root())
} else {
file_system::Path::try_from(tree_path)
}?;
// We found a Commit object in the Tree, likely a submodule.
// We will skip this entry.
if let Some(git2::ObjectType::Commit) = entry.kind() {
return Err(TreeWalkError::Commit);
}
let object = entry.to_object(self.repo_ref)?;
let blob = object.as_blob().ok_or(TreeWalkError::NotBlob)?;
let name = str::from_utf8(entry.name_bytes())?;
let name = file_system::Label::try_from(name).map_err(Error::FileSystem)?;
Ok((
path,
name,
directory::File {
contents: blob.content().to_owned(),
size: blob.size(),
},
))
}
}
impl<'a> Vcs<Commit, Error> for RepositoryRef<'a> {
+110 -135
View File
@@ -8,17 +8,7 @@ use radicle_surf::git::{Author, BranchType, Commit};
use radicle_surf::{
diff::*,
file_system::{unsound, Path},
git::{
error::Error,
Branch,
BranchName,
Browser,
Namespace,
Oid,
RefScope,
Repository,
TagName,
},
git::{error::Error, Branch, BranchName, Namespace, Oid, RefScope, Repository, Rev, TagName},
};
const GIT_PLATINUM: &str = "../data/git-platinum";
@@ -28,29 +18,26 @@ 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::new("../..").unwrap();
let browser = Browser::new(&repo, Branch::local("main")).unwrap();
browser.get_directory().unwrap();
repo.as_ref()
.snapshot(&Branch::local("main").into())
.unwrap();
}
#[cfg(test)]
mod namespace {
use super::*;
use pretty_assertions::{assert_eq, assert_ne};
use radicle_surf::vcs::Vcs;
#[test]
fn switch_to_banana() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let mut browser = Browser::new_with_namespace(
&repo,
&Namespace::try_from("golden")?,
Branch::local("master"),
)?;
let history = browser.get();
let repo = repo.as_ref();
let history_master = repo.get_history(Branch::local("master").into())?;
repo.switch_namespace("golden")?;
let history_banana = repo.get_history(Branch::local("banana").into())?;
browser.branch(Branch::local("banana"))?;
assert_ne!(history, browser.get());
assert_ne!(history_master, history_banana);
Ok(())
}
@@ -58,28 +45,25 @@ mod namespace {
#[test]
fn me_namespace() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let browser = Browser::new(&repo, Branch::local("master"))?;
let history = browser.get();
let repo = repo.as_ref();
let history = repo.get_history(Branch::local("master").into())?;
assert_eq!(browser.which_namespace(), Ok(None));
assert_eq!(repo.which_namespace(), Ok(None));
let browser = browser
.switch_namespace(&Namespace::try_from("me")?, Branch::local("feature/#1194"))?;
repo.switch_namespace("me")?;
assert_eq!(repo.which_namespace(), Ok(Some(Namespace::try_from("me")?)));
assert_eq!(
browser.which_namespace(),
Ok(Some(Namespace::try_from("me")?))
);
assert_eq!(history, browser.get());
let history_feature = repo.get_history(Branch::local("feature/#1194").into())?;
assert_eq!(history, history_feature);
let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
let mut branches = browser.list_branches(RefScope::Local)?;
let mut branches = repo.list_branches(RefScope::Local)?;
branches.sort();
assert_eq!(expected_branches, branches);
let expected_branches: Vec<Branch> = vec![Branch::remote("feature/#1194", "fein")];
let mut branches = browser.list_branches(RefScope::Remote {
let mut branches = repo.list_branches(RefScope::Remote {
name: Some("fein".to_string()),
})?;
branches.sort();
@@ -92,22 +76,23 @@ mod namespace {
#[test]
fn golden_namespace() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let browser = Browser::new(&repo, Branch::local("master"))?;
let history = browser.get();
let repo = repo.as_ref();
let history = repo.get_history(Branch::local("master").into())?;
assert_eq!(browser.which_namespace(), Ok(None));
assert_eq!(repo.which_namespace(), Ok(None));
let golden_browser =
browser.switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?;
repo.switch_namespace("golden")?;
assert_eq!(
golden_browser.which_namespace(),
repo.which_namespace(),
Ok(Some(Namespace::try_from("golden")?))
);
assert_eq!(history, golden_browser.get());
let golden_history = repo.get_history(Branch::local("master").into())?;
assert_eq!(history, golden_history);
let expected_branches: Vec<Branch> = vec![Branch::local("banana"), Branch::local("master")];
let mut branches = golden_browser.list_branches(RefScope::Local)?;
let mut branches = repo.list_branches(RefScope::Local)?;
branches.sort();
assert_eq!(expected_branches, branches);
@@ -117,7 +102,7 @@ mod namespace {
Branch::remote("heelflip", "kickflip"),
Branch::remote("v0.1.0", "kickflip"),
];
let mut branches = golden_browser.list_branches(RefScope::Remote {
let mut branches = repo.list_branches(RefScope::Remote {
name: Some("kickflip".to_string()),
})?;
branches.sort();
@@ -130,24 +115,21 @@ mod namespace {
#[test]
fn silver_namespace() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let browser = Browser::new(&repo, Branch::local("master"))?;
let history = browser.get();
let repo = repo.as_ref();
let history = repo.get_history(Branch::local("master").into())?;
assert_eq!(browser.which_namespace(), Ok(None));
let silver_browser = browser.switch_namespace(
&Namespace::try_from("golden/silver")?,
Branch::local("master"),
)?;
assert_eq!(repo.which_namespace(), Ok(None));
repo.switch_namespace("golden/silver")?;
assert_eq!(
silver_browser.which_namespace(),
repo.which_namespace(),
Ok(Some(Namespace::try_from("golden/silver")?))
);
assert_ne!(history, silver_browser.get());
let silver_history = repo.get_history(Branch::local("master").into())?;
assert_ne!(history, silver_history);
let expected_branches: Vec<Branch> = vec![Branch::local("master")];
let mut branches = silver_browser.list_branches(RefScope::All)?;
let mut branches = repo.list_branches(RefScope::All)?;
branches.sort();
assert_eq!(expected_branches, branches);
@@ -158,6 +140,8 @@ mod namespace {
#[cfg(test)]
mod rev {
use radicle_surf::vcs::Vcs;
use super::*;
use std::str::FromStr;
@@ -175,13 +159,12 @@ mod rev {
#[test]
fn _master() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let mut browser = Browser::new(&repo, Branch::local("master"))?;
browser.rev(Branch::remote("master", "origin"))?;
let repo = repo.as_ref();
let history = repo.get_history(Branch::remote("master", "origin").into())?;
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
assert!(
browser
.as_history()
history
.find(|commit| if commit.id == commit1 {
Some(commit.clone())
} else {
@@ -190,13 +173,12 @@ mod rev {
.is_some(),
"commit_id={}, history =\n{:#?}",
commit1,
browser.as_history()
&history
);
let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
assert!(
browser
.as_history()
history
.find(|commit| if commit.id == commit2 {
Some(commit.clone())
} else {
@@ -205,7 +187,7 @@ mod rev {
.is_some(),
"commit_id={}, history =\n{:#?}",
commit2,
browser.as_history()
&history
);
Ok(())
@@ -214,12 +196,11 @@ mod rev {
#[test]
fn commit() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let mut browser = Browser::new(&repo, Branch::local("master"))?;
browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?;
let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
let history = repo.as_ref().get_history(rev)?;
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
assert!(browser
.as_history()
assert!(history
.find(|commit| if commit.id == commit1 {
Some(commit.clone())
} else {
@@ -233,9 +214,9 @@ mod rev {
#[test]
fn commit_parents() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let mut browser = Browser::new(&repo, Branch::local("master"))?;
browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?;
let commit = browser.as_history().first();
let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
let history = repo.as_ref().get_history(rev)?;
let commit = history.first();
assert_eq!(
commit.parents,
@@ -248,12 +229,11 @@ mod rev {
#[test]
fn commit_short() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let mut browser = Browser::new(&repo, Branch::local("master"))?;
browser.rev(browser.oid("3873745c8")?)?;
let rev: Rev = repo.as_ref().oid("3873745c8")?.into();
let history = repo.as_ref().get_history(rev)?;
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
assert!(browser
.as_history()
assert!(history
.find(|commit| if commit.id == commit1 {
Some(commit.clone())
} else {
@@ -267,11 +247,11 @@ mod rev {
#[test]
fn tag() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let mut browser = Browser::new(&repo, Branch::local("master"))?;
browser.rev(TagName::new("v0.2.0"))?;
let rev: Rev = TagName::new("v0.2.0").into();
let history = repo.as_ref().get_history(rev)?;
let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
assert_eq!(browser.as_history().first().id, commit1);
assert_eq!(history.first().id, commit1);
Ok(())
}
@@ -279,6 +259,8 @@ mod rev {
#[cfg(test)]
mod last_commit {
use radicle_surf::vcs::Vcs;
use super::*;
use std::str::FromStr;
@@ -286,52 +268,46 @@ mod last_commit {
fn readme_missing_and_memory() {
let repo = Repository::new(GIT_PLATINUM)
.expect("Could not retrieve ./data/git-platinum as git repository");
let mut browser =
Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
// Set the browser history to the initial commit
let commit =
let oid =
Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");
browser.commit(commit).unwrap();
let head_commit = browser.get().0.first().clone();
// memory.rs is commited later so it should not exist here.
let memory_last_commit = browser
.last_commit(Path::with_root(&[
unsound::label::new("src"),
unsound::label::new("memory.rs"),
]))
let rev: Rev = oid.into();
let memory_last_commit_oid = repo
.as_ref()
.last_commit(
Path::with_root(&[unsound::label::new("src"), unsound::label::new("memory.rs")]),
&rev,
)
.expect("Failed to get last commit")
.map(|commit| commit.id);
assert_eq!(memory_last_commit, None);
assert_eq!(memory_last_commit_oid, None);
// README.md exists in this commit.
let readme_last_commit = browser
.last_commit(Path::with_root(&[unsound::label::new("README.md")]))
let readme_last_commit = repo
.as_ref()
.last_commit(Path::with_root(&[unsound::label::new("README.md")]), &rev)
.expect("Failed to get last commit")
.map(|commit| commit.id);
assert_eq!(readme_last_commit, Some(head_commit.id));
assert_eq!(readme_last_commit, Some(oid));
}
#[test]
fn folder_svelte() {
let repo = Repository::new(GIT_PLATINUM)
.expect("Could not retrieve ./data/git-platinum as git repository");
let mut browser =
Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
// Check that last commit is the actual last commit even if head commit differs.
let commit =
let oid =
Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
browser.commit(commit).unwrap();
let rev: Rev = oid.into();
let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();
let folder_svelte = browser
.last_commit(unsound::path::new("~/examples/Folder.svelte"))
let folder_svelte = repo
.as_ref()
.last_commit(unsound::path::new("~/examples/Folder.svelte"), &rev)
.expect("Failed to get last commit")
.map(|commit| commit.id);
@@ -342,20 +318,19 @@ mod last_commit {
fn nest_directory() {
let repo = Repository::new(GIT_PLATINUM)
.expect("Could not retrieve ./data/git-platinum as git repository");
let mut browser =
Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
// Check that last commit is the actual last commit even if head commit differs.
let commit =
let oid =
Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
browser.commit(commit).unwrap();
let rev: Rev = oid.into();
let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap();
let nested_directory_tree_commit_id = browser
.last_commit(unsound::path::new(
"~/this/is/a/really/deeply/nested/directory/tree",
))
let nested_directory_tree_commit_id = repo
.as_ref()
.last_commit(
unsound::path::new("~/this/is/a/really/deeply/nested/directory/tree"),
&rev,
)
.expect("Failed to get last commit")
.map(|commit| commit.id);
@@ -367,24 +342,24 @@ mod last_commit {
fn can_get_last_commit_for_special_filenames() {
let repo = Repository::new(GIT_PLATINUM)
.expect("Could not retrieve ./data/git-platinum as git repository");
let mut browser =
Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
// Check that last commit is the actual last commit even if head commit differs.
let commit =
let oid =
Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA");
browser.commit(commit).unwrap();
let rev: Rev = oid.into();
let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();
let backslash_commit_id = browser
.last_commit(unsound::path::new("~/special/faux\\path"))
let backslash_commit_id = repo
.as_ref()
.last_commit(unsound::path::new("~/special/faux\\path"), &rev)
.expect("Failed to get last commit")
.map(|commit| commit.id);
assert_eq!(backslash_commit_id, Some(expected_commit_id));
let ogre_commit_id = browser
.last_commit(unsound::path::new("~/special/👹👹👹"))
let ogre_commit_id = repo
.as_ref()
.last_commit(unsound::path::new("~/special/👹👹👹"), &rev)
.expect("Failed to get last commit")
.map(|commit| commit.id);
assert_eq!(ogre_commit_id, Some(expected_commit_id));
@@ -394,15 +369,20 @@ mod last_commit {
fn root() {
let repo = Repository::new(GIT_PLATINUM)
.expect("Could not retrieve ./data/git-platinum as git repository");
let browser =
Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser");
let root_last_commit_id = browser
.last_commit(Path::root())
let rev: Rev = Branch::local("master").into();
let root_last_commit_id = repo
.as_ref()
.last_commit(Path::root(), &rev)
.expect("Failed to get last commit")
.map(|commit| commit.id);
assert_eq!(root_last_commit_id, Some(browser.get().first().id));
let expected_oid = repo
.as_ref()
.get_history(Branch::local("master").into())
.unwrap()
.first()
.id;
assert_eq!(root_last_commit_id, Some(expected_oid));
}
}
@@ -416,14 +396,13 @@ mod diff {
fn test_initial_diff() -> Result<(), Error> {
let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
let repo = Repository::new(GIT_PLATINUM)?;
// let commit = repo.0.find_commit(oid).unwrap();
let commit = repo.as_ref().get_git2_commit(oid).unwrap();
let repo = repo.as_ref();
let commit = repo.get_git2_commit(oid).unwrap();
assert!(commit.parents().count() == 0);
assert!(commit.parent(0).is_err());
let bro = Browser::new(&repo, Branch::local("master"))?;
let diff = bro.initial_diff(oid)?;
let diff = repo.initial_diff(oid)?;
let expected_diff = Diff {
created: vec![CreateFile {
@@ -453,15 +432,12 @@ mod diff {
#[test]
fn test_diff() -> Result<(), Error> {
let repo = Repository::new(GIT_PLATINUM)?;
let repo = repo.as_ref();
let commit = repo
.as_ref()
.get_git2_commit(Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?)
.unwrap();
let parent = commit.parent(0)?;
let bro = Browser::new(&repo, Branch::local("master"))?;
let diff = bro.diff(parent.id().into(), commit.id().into())?;
let diff = repo.diff(parent.id().into(), commit.id().into())?;
let expected_diff = Diff {
created: vec![],
@@ -572,8 +548,7 @@ mod threading {
fn basic_test() -> Result<(), Error> {
let shared_repo = Mutex::new(Repository::new(GIT_PLATINUM)?);
let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
let bro = Browser::new(&*locked_repo, Branch::local("master"))?;
let mut branches = bro.list_branches(RefScope::All)?;
let mut branches = locked_repo.as_ref().list_branches(RefScope::All)?;
branches.sort();
assert_eq!(