radicle-surf: refactor source objects

Refactor the source data types to reuse existing logic and update to
the latest protocol.

The `Blob` and `Tree` data types are updated to be backed by `File`
and `Directory` respectively. This allows the construction of them to
be much simpler and reuse any existing logic on those types. This also
lead to the removal of `ObjectType` and `Info`.

The `Revision` type was removed in favour of using the `Revision`
trait.

The `Revisions` type was renamed to `View`, while also renaming the
`revision` module to `view`. A `View` is a simple encoding of what a
peer's "view" would look like, i.e. what references do they have. This
also required the introduction of getting free-form categories in the
reference db, i.e. `refs/<category>`, and so a `Categories` iterator
was also introduced.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny
2022-11-30 13:22:44 +00:00
parent 02321aba7c
commit b4d3bf69cf
20 changed files with 534 additions and 538 deletions
+10
View File
@@ -21,6 +21,7 @@ doctest = false
[features]
serialize = ["serde"]
syntax = ["syntect", "lazy_static"]
# NOTE: testing `test_submodule_failure` on GH actions
# is painful since it uses this specific repo and expects
# certain branches to be setup. So we use this feature flag
@@ -29,6 +30,7 @@ gh-actions = []
[dependencies]
base64 = "0.13"
log = "0.4"
nonempty = "0.5"
serde = { features = ["serde_derive"], optional = true, version = "1" }
thiserror = "1.0"
@@ -43,6 +45,10 @@ version = "0.1.0"
path = "../git-ref-format"
features = ["macro", "serde"]
[dependencies.lazy_static]
version = "1"
optional = true
[dependencies.radicle-git-ext]
version = "0.2.0"
path = "../radicle-git-ext"
@@ -52,6 +58,10 @@ features = ["serde"]
version = "0.1.0"
path = "../radicle-std-ext"
[dependencies.syntect]
version = "5"
optional = true
[build-dependencies]
anyhow = "1.0"
flate2 = "1"
+1 -1
View File
@@ -41,7 +41,7 @@ fn main() {
let repo = Repository::discover(&repo_path).unwrap();
let now = Instant::now();
let head = repo.head_oid().unwrap();
let root = repo.root_dir(head).unwrap();
let root = repo.root_dir(&head).unwrap();
print_directory(&root, &repo, 0);
let elapsed_millis = now.elapsed().as_millis();
+36 -4
View File
@@ -21,6 +21,7 @@
//! See [`Directory`] for more information.
use std::{
cmp::Ordering,
collections::BTreeMap,
convert::{Infallible, Into as _},
path::{Path, PathBuf},
@@ -186,6 +187,23 @@ pub enum Entry {
Directory(Directory),
}
impl PartialOrd for Entry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Entry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Entry::File(x), Entry::File(y)) => x.name().cmp(y.name()),
(Entry::File(_), Entry::Directory(_)) => Ordering::Less,
(Entry::Directory(_), Entry::File(_)) => Ordering::Greater,
(Entry::Directory(x), Entry::Directory(y)) => x.name().cmp(y.name()),
}
}
}
impl Entry {
/// Get a label for the `Entriess`, either the name of the [`File`]
/// or the name of the [`Directory`].
@@ -196,6 +214,20 @@ impl Entry {
}
}
pub fn path(&self) -> PathBuf {
match self {
Entry::File(file) => file.path(),
Entry::Directory(directory) => directory.path(),
}
}
pub fn location(&self) -> &Path {
match self {
Entry::File(file) => file.location(),
Entry::Directory(directory) => directory.location(),
}
}
pub(crate) fn from_entry(
entry: &git2::TreeEntry,
path: PathBuf,
@@ -329,7 +361,7 @@ impl Directory {
&self,
path: &P,
repo: &Repository,
) -> Result<Option<Entry>, crate::git::Error>
) -> Result<Option<Entry>, error::Directory>
where
P: AsRef<Path>,
{
@@ -351,12 +383,12 @@ impl Directory {
&self,
path: &P,
repo: &Repository,
) -> Result<Option<Oid>, crate::git::Error>
) -> Result<Option<File>, error::Directory>
where
P: AsRef<Path>,
{
Ok(match self.find_entry(path, repo)? {
Some(Entry::File(f)) => Some(f.id),
Some(Entry::File(file)) => Some(file),
_ => None,
})
}
@@ -366,7 +398,7 @@ impl Directory {
&self,
path: P,
repo: &Repository,
) -> Result<Option<Self>, crate::git::Error>
) -> Result<Option<Self>, error::Directory>
where
P: AsRef<Path>,
{
+29 -5
View File
@@ -167,6 +167,14 @@ impl Revision for &str {
}
}
impl Revision for Branch {
type Error = Error;
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
(&self).object_id(repo)
}
}
impl Revision for &Branch {
type Error = Error;
@@ -176,6 +184,14 @@ impl Revision for &Branch {
}
}
impl Revision for Tag {
type Error = Infallible;
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
(&self).object_id(repo)
}
}
impl Revision for &Tag {
type Error = Infallible;
@@ -184,27 +200,35 @@ impl Revision for &Tag {
}
}
impl<R: Revision> Revision for Box<R> {
type Error = R::Error;
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
self.as_ref().object_id(repo)
}
}
/// A common trait for anything that can convert to a `Commit`.
pub trait ToCommit {
type Error: std::error::Error + Send + Sync + 'static;
/// Converts to a commit in `repo`.
fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error>;
fn to_commit(&self, repo: &Repository) -> Result<Commit, Self::Error>;
}
impl ToCommit for Commit {
type Error = Infallible;
fn to_commit(self, _repo: &Repository) -> Result<Commit, Self::Error> {
Ok(self)
fn to_commit(&self, _repo: &Repository) -> Result<Commit, Self::Error> {
Ok(self.clone())
}
}
impl<R: Revision> ToCommit for R {
type Error = Error;
fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error> {
let oid = repo.object_id(&self)?;
fn to_commit(&self, repo: &Repository) -> Result<Commit, Self::Error> {
let oid = repo.object_id(self)?;
let commit = repo.git2_repo().find_commit(oid.into())?;
Ok(Commit::try_from(commit)?)
}
+49
View File
@@ -20,6 +20,8 @@ use std::marker::PhantomData;
use git_ref_format::{
refname,
refspec::{self, PatternString, QualifiedPattern},
Qualified,
RefStr,
RefString,
};
use thiserror::Error;
@@ -39,6 +41,15 @@ pub struct Glob<T> {
glob_type: PhantomData<T>, // To support different methods for different T.
}
impl<T> Default for Glob<T> {
fn default() -> Self {
Self {
globs: Default::default(),
glob_type: PhantomData,
}
}
}
impl<T> Glob<T> {
/// Return the [`QualifiedPattern`] globs of this `Glob`.
pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
@@ -303,6 +314,44 @@ impl From<Glob<Remote>> for Glob<Branch> {
}
}
impl Glob<Qualified<'_>> {
pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
Self {
globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
glob_type: PhantomData,
}
}
/// Creates a `Glob` for `refs/<category>`, starting with `glob`.
pub fn categories<R>(category: R, glob: PatternString) -> Self
where
R: AsRef<RefStr>,
{
let globs = vec![Self::qualify_category(category, glob)];
Self {
globs,
glob_type: PhantomData,
}
}
/// Adds a `refs/<category>` pattern to this `Glob`.
pub fn insert<R>(mut self, category: R, glob: PatternString) -> Self
where
R: AsRef<RefStr>,
{
self.globs.push(Self::qualify_category(category, glob));
self
}
fn qualify_category<R>(category: R, glob: PatternString) -> QualifiedPattern<'static>
where
R: AsRef<RefStr>,
{
let prefix = refname!("refs").and(category);
qualify(&prefix, glob).expect("BUG: pattern is qualified")
}
}
fn qualify(prefix: &RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
}
+1 -1
View File
@@ -38,7 +38,7 @@ enum FilterBy {
impl<'a> History<'a> {
/// Creates a new history starting from `head`, in `repo`.
pub fn new<C: ToCommit>(repo: &'a Repository, head: C) -> Result<Self, Error> {
pub fn new<C: ToCommit>(repo: &'a Repository, head: &C) -> Result<Self, Error> {
let head = head
.to_commit(repo)
.map_err(|err| Error::ToCommit(err.into()))?;
+32 -19
View File
@@ -22,10 +22,7 @@ use std::{
str,
};
use git_ref_format::{
refspec::{self, QualifiedPattern},
Qualified,
};
use git_ref_format::{refspec::QualifiedPattern, Qualified, RefString};
use radicle_git_ext::Oid;
use thiserror::Error;
@@ -50,7 +47,7 @@ use crate::{
};
pub mod iter;
pub use iter::{Branches, Namespaces, Tags};
pub use iter::{Branches, Categories, Namespaces, Tags};
use self::iter::{BranchNames, TagNames};
@@ -61,6 +58,8 @@ pub enum Error {
#[error(transparent)]
Branches(#[from] iter::error::Branch),
#[error(transparent)]
Categories(#[from] iter::error::Category),
#[error(transparent)]
Commit(#[from] commit::Error),
/// An error that comes from performing a *diff* operations.
#[error(transparent)]
@@ -136,6 +135,16 @@ impl Repository {
Ok(tags)
}
pub fn categories(&self, pattern: &Glob<Qualified<'_>>) -> Result<Categories, Error> {
let mut cats = Categories::default();
for glob in pattern.globs() {
let namespaced = self.namespaced_pattern(glob)?;
let references = self.inner.references_glob(&namespaced)?;
cats.push(references);
}
Ok(cats)
}
/// Returns an iterator of namespaces that match `pattern`.
pub fn namespaces(&self, pattern: &Glob<Namespace>) -> Result<Namespaces, Error> {
let mut set = BTreeSet::new();
@@ -189,7 +198,7 @@ impl Repository {
///
/// To visit inside any nested sub-directories, call `directory.get(&repo)`
/// on the sub-directory.
pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
pub fn root_dir<C: ToCommit>(&self, commit: &C) -> Result<Directory, Error> {
let commit = commit
.to_commit(self)
.map_err(|err| Error::ToCommit(err.into()))?;
@@ -200,7 +209,7 @@ impl Repository {
/// 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,
@@ -215,7 +224,7 @@ impl Repository {
}
/// Gets stats of `commit`.
pub fn get_commit_stats<C: ToCommit>(&self, commit: C) -> Result<Stats, Error> {
pub fn get_commit_stats<C: ToCommit>(&self, commit: &C) -> Result<Stats, Error> {
let branches = self.branches(Glob::all_heads())?.count();
let history = self.history(commit)?;
let mut commits = 0;
@@ -237,12 +246,6 @@ impl Repository {
})
}
/// Obtain the file content
pub(crate) fn file_content(&self, object_id: Oid) -> Result<FileContent, Error> {
let blob = self.inner.find_blob(object_id.into())?;
Ok(FileContent::new(blob))
}
/// Retrieves the file with `path` in this commit.
pub fn get_commit_file<P, R>(&self, rev: &R, path: &P) -> Result<FileContent, crate::git::Error>
where
@@ -270,8 +273,8 @@ impl Repository {
}
/// Lists tag names in the local RefScope.
pub fn tag_names(&self) -> Result<TagNames, Error> {
Ok(self.tags(&Glob::tags(refspec::pattern!("*")))?.names())
pub fn tag_names(&self, filter: &Glob<Tag>) -> Result<TagNames, Error> {
Ok(self.tags(filter)?.names())
}
/// Returns the Oid of the current HEAD
@@ -282,8 +285,18 @@ impl Repository {
}
/// Switch to a `namespace`
pub fn switch_namespace(&self, namespace: &str) -> Result<(), Error> {
Ok(self.inner.set_namespace(namespace)?)
pub fn switch_namespace(&self, namespace: &RefString) -> Result<(), Error> {
Ok(self.inner.set_namespace(namespace.as_str())?)
}
pub fn with_namespace<T, F>(&self, namespace: &RefString, f: F) -> Result<T, Error>
where
F: FnOnce() -> Result<T, Error>,
{
self.switch_namespace(namespace)?;
let res = f();
self.inner.remove_namespace()?;
res
}
/// Returns a full reference name with namespace(s) included.
@@ -413,7 +426,7 @@ impl Repository {
}
/// Returns the history with the `head` commit.
pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
pub fn history<C: ToCommit>(&self, head: &C) -> Result<History, Error> {
History::new(self, head)
}
+56 -2
View File
@@ -6,9 +6,9 @@ use std::{
convert::TryFrom as _,
};
use git_ref_format::{lit, Qualified};
use git_ref_format::{lit, Qualified, RefString};
use crate::git::{tag, Branch, Namespace, Tag};
use crate::git::{refstr_join, tag, Branch, Namespace, Tag};
/// Iterator over [`Tag`]s.
#[derive(Default)]
@@ -165,7 +165,49 @@ impl Iterator for Namespaces {
}
}
#[derive(Default)]
pub struct Categories<'a> {
references: Vec<git2::References<'a>>,
current: usize,
}
impl<'a> Categories<'a> {
pub(super) fn push(&mut self, references: git2::References<'a>) {
self.references.push(references)
}
}
impl<'a> Iterator for Categories<'a> {
type Item = Result<(RefString, RefString), error::Category>;
fn next(&mut self) -> Option<Self::Item> {
while self.current < self.references.len() {
match self.references.get_mut(self.current) {
Some(refs) => match refs.next() {
Some(res) => {
return Some(res.map_err(error::Category::from).and_then(|r| {
let name = std::str::from_utf8(r.name_bytes())?;
let name = git_ref_format::RefStr::try_from_str(name)?;
let name = name.qualified().ok_or_else(|| {
error::Category::NotQualified(name.to_ref_string())
})?;
let (_refs, category, c, cs) = name.non_empty_components();
Ok((category.to_ref_string(), refstr_join(c, cs)))
}));
},
None => self.current += 1,
},
None => break,
}
}
None
}
}
pub mod error {
use std::str;
use git_ref_format::RefString;
use thiserror::Error;
use crate::git::{branch, tag};
@@ -178,6 +220,18 @@ pub mod error {
Branch(#[from] branch::error::Branch),
}
#[derive(Debug, Error)]
pub enum Category {
#[error(transparent)]
Git(#[from] git2::Error),
#[error("the reference '{0}' was expected to be qualified, i.e. 'refs/<category>/<path>'")]
NotQualified(RefString),
#[error(transparent)]
RefFormat(#[from] git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
}
#[derive(Debug, Error)]
pub enum Tag {
#[error(transparent)]
-5
View File
@@ -83,8 +83,3 @@ pub mod diff;
pub mod file_system;
pub mod git;
pub mod source;
#[cfg(feature = "syntax")]
pub mod syntax;
#[cfg(feature = "syntax")]
pub use syntax::SYNTAX_SET;
+9 -4
View File
@@ -11,13 +11,18 @@
//! [git-objects]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
pub mod object;
pub use object::{blob, tree, Blob, BlobContent, Info, ObjectType, Tree};
pub use object::{blob, tree, Blob, BlobContent, Tree, TreeEntry};
pub mod commit;
pub use commit::{commit, commits, Commit};
pub mod person;
mod person;
pub use person::Person;
pub mod revision;
pub use revision::Revision;
mod view;
pub use view::{view, View};
#[cfg(feature = "syntax")]
pub mod syntax;
#[cfg(feature = "syntax")]
pub use syntax::SYNTAX_SET;
+8 -13
View File
@@ -29,7 +29,7 @@ use serde::{
use crate::{
diff,
git::{self, glob, Glob, Repository},
source::{person::Person, revision::Revision},
source::person::Person,
};
use radicle_git_ext::Oid;
@@ -227,18 +227,13 @@ pub fn header(repo: &Repository, sha1: Oid) -> Result<Header, Error> {
///
/// Will return [`Error`] if the project doesn't exist or the surf interaction
/// fails.
pub fn commits(repo: &Repository, maybe_revision: Option<Revision>) -> Result<Commits, Error> {
let rev = match maybe_revision {
Some(revision) => revision,
None => Revision::Sha {
sha: repo.head_oid()?,
},
};
let stats = repo.get_commit_stats(&rev)?;
let commits: Result<Vec<git::Commit>, git::Error> = repo.history(&rev)?.collect();
let headers = commits?.iter().map(Header::from).collect();
pub fn commits<R>(repo: &Repository, revision: &R) -> Result<Commits, Error>
where
R: git::Revision,
{
let stats = repo.get_commit_stats(revision)?;
let commits = repo.history(revision)?.collect::<Result<Vec<_>, _>>()?;
let headers = commits.into_iter().map(Header::from).collect();
Ok(Commits { headers, stats })
}
+5 -57
View File
@@ -20,68 +20,13 @@
use std::path::PathBuf;
#[cfg(feature = "serialize")]
use serde::{
ser::{SerializeStruct as _, Serializer},
Serialize,
};
pub mod blob;
pub use blob::{Blob, BlobContent};
pub mod tree;
pub use tree::{tree, Tree, TreeEntry};
pub use tree::{Tree, TreeEntry};
use crate::{file_system::directory, git, source::commit};
/// Git object types.
///
/// `shafiul.github.io/gitbook/1_the_git_object_model.html`
#[derive(Debug, Eq, Ord, PartialOrd, PartialEq)]
pub enum ObjectType {
/// References a list of other trees and blobs.
Tree,
/// Used to store file data.
Blob,
}
#[cfg(feature = "serialize")]
impl Serialize for ObjectType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Blob => serializer.serialize_unit_variant("ObjectType", 0, "BLOB"),
Self::Tree => serializer.serialize_unit_variant("ObjectType", 1, "TREE"),
}
}
}
/// Set of extra information we carry for blob and tree objects returned from
/// the API.
pub struct Info {
/// Name part of an object.
pub name: String,
/// The type of the object.
pub object_type: ObjectType,
/// The last commmit that touched this object.
pub last_commit: Option<commit::Header>,
}
#[cfg(feature = "serialize")]
impl Serialize for Info {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Info", 3)?;
state.serialize_field("name", &self.name)?;
state.serialize_field("objectType", &self.object_type)?;
state.serialize_field("lastCommit", &self.last_commit)?;
state.end()
}
}
use crate::{file_system::directory, git};
/// An error reported by object types.
#[derive(Debug, thiserror::Error)]
@@ -89,6 +34,9 @@ pub enum Error {
#[error(transparent)]
Directory(#[from] directory::error::Directory),
#[error(transparent)]
File(#[from] directory::error::File),
/// An error occurred during a git operation.
#[error(transparent)]
Git(#[from] git::Error),
+95 -105
View File
@@ -18,10 +18,7 @@
//! 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, PathBuf},
str,
};
use std::{path::Path, str};
#[cfg(feature = "serialize")]
use serde::{
@@ -30,28 +27,63 @@ use serde::{
};
use crate::{
git::Repository,
source::{
commit,
object::{Error, Info, ObjectType},
revision::Revision,
},
file_system::{File, FileContent},
git::{self, Repository},
source::{commit, object::Error},
};
#[cfg(feature = "syntax")]
use crate::syntax;
/// File data abstraction.
pub struct Blob {
/// Actual content of the file, if the content is ASCII.
pub file: File,
pub content: BlobContent,
/// Extra info for the file.
pub info: Info,
/// Absolute path to the object from the root of the repo.
pub path: PathBuf,
pub commit: Option<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,
content,
commit: last_commit,
})
}
/// Indicates if the content of the [`Blob`] is binary.
#[must_use]
pub fn is_binary(&self) -> bool {
@@ -71,12 +103,14 @@ impl Serialize for Blob {
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Blob", 5)?;
const FIELDS: usize = 6;
let mut state = serializer.serialize_struct("Blob", FIELDS)?;
state.serialize_field("binary", &self.is_binary())?;
state.serialize_field("html", &self.is_html())?;
state.serialize_field("content", &self.content)?;
state.serialize_field("info", &self.info)?;
state.serialize_field("path", &self.path)?;
state.serialize_field("lastCommit", &self.commit)?;
state.serialize_field("name", &self.file.name())?;
state.serialize_field("path", &self.file.location())?;
state.end()
}
}
@@ -88,9 +122,9 @@ pub enum BlobContent {
Plain(String),
/// Content is syntax-highlighted HTML.
///
/// Note that is necessary to enable the `syntax` feature flag for this
/// variant to be constructed. Use `highlighting::blob`, instead of
/// [`blob`] to get highlighted content.
/// Note that it is necessary to enable the `syntax` feature flag
/// for this variant to be constructed. Use `Blob::highlighted`,
/// instead of `Blob::new` to get highlighted content.
Html(String),
/// Content is binary and needs special treatment.
Binary(Vec<u8>),
@@ -112,98 +146,54 @@ impl Serialize for BlobContent {
}
}
/// 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 blob<P>(repo: &Repository, maybe_revision: Option<Revision>, path: &P) -> Result<Blob, Error>
where
P: AsRef<Path>,
{
make_blob(repo, maybe_revision, path, content)
}
fn make_blob<P, C>(
repo: &Repository,
maybe_revision: Option<Revision>,
path: &P,
content: C,
) -> Result<Blob, Error>
where
P: AsRef<Path>,
C: FnOnce(&[u8]) -> BlobContent,
{
let path = path.as_ref();
let revision = maybe_revision.unwrap();
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));
// TODO: fuck this
let name = path
.file_name()
.unwrap()
.to_os_string()
.into_string()
.ok()
.unwrap();
let file_content = repo.file_content(file)?;
let content = content(file_content.as_bytes());
Ok(Blob {
content,
info: Info {
name,
object_type: ObjectType::Blob,
last_commit,
},
path: path.to_path_buf(),
})
}
/// Return a [`BlobContent`] given a byte slice.
fn content(content: &[u8]) -> BlobContent {
match str::from_utf8(content) {
Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
Err(_) => BlobContent::Binary(content.to_owned()),
impl From<FileContent<'_>> for BlobContent {
fn from(content: FileContent) -> Self {
let content = content.as_bytes();
match str::from_utf8(content) {
Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
Err(_) => BlobContent::Binary(content.to_owned()),
}
}
}
#[cfg(feature = "syntax")]
pub mod highlighting {
use crate::source::syntax;
use super::*;
/// 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 blob<P>(
browser: &mut Browser,
maybe_revision: Option<Revision<P>>,
path: &str,
theme: Option<&str>,
) -> Result<Blob, Error>
where
P: ToString,
{
make_blob(browser, maybe_revision, path, |contents| {
content(path, contents, theme)
})
impl Blob {
/// Returns the [`Blob`] for a file at `revision` under `path`.
///
/// The content of the [`Blob`] will be highlighted, if possible.
///
/// # Errors
///
/// Will return [`Error`] if the project doesn't exist or a surf
/// interaction fails.
pub fn highlighted<P, R>(
browser: &Repository,
revision: &R,
path: &P,
theme: Option<&str>,
) -> Result<Blob, Error>
where
P: AsRef<Path>,
R: git::Revision,
{
Self::make_blob(browser, revision, path, |contents| {
content(path, &contents, theme)
})
}
}
/// Return a [`BlobContent`] given a file path, content and theme. Attempts
/// to perform syntax highlighting when the theme is `Some`.
fn content(path: &str, content: &[u8], theme_name: Option<&str>) -> BlobContent {
fn content<P>(path: P, content: &FileContent, theme_name: Option<&str>) -> BlobContent
where
P: AsRef<Path>,
{
let content = content.as_bytes();
let content = match str::from_utf8(content) {
Ok(content) => content,
Err(_) => return BlobContent::Binary(content.to_owned()),
+82 -113
View File
@@ -18,9 +18,8 @@
//! 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, PathBuf};
use std::path::Path;
use git_ref_format::refname;
#[cfg(feature = "serialize")]
use serde::{
ser::{SerializeStruct as _, Serializer},
@@ -28,46 +27,95 @@ use serde::{
};
use crate::{
file_system::directory,
git::Repository,
source::{
commit,
object::{Error, Info, ObjectType},
revision::Revision,
},
file_system::{directory, Directory},
git::{self, Repository},
source::{commit, object::Error},
};
/// Result of a directory listing, carries other trees and blobs.
pub struct Tree {
/// Absolute path to the tree object from the repo root.
pub path: PathBuf,
pub directory: Directory,
pub commit: Option<commit::Header>,
/// Entries listed in that tree result.
pub entries: Vec<TreeEntry>,
/// Extra info for the tree object.
pub info: Info,
}
#[cfg(feature = "serialize")]
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 {
entries,
directory: prefix_dir,
commit: last_commit,
})
}
}
#[cfg(feature = "serde")]
impl Serialize for Tree {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Tree", 3)?;
state.serialize_field("path", &self.path)?;
const FIELDS: usize = 4;
let mut state = serializer.serialize_struct("Tree", FIELDS)?;
state.serialize_field("entries", &self.entries)?;
state.serialize_field("info", &self.info)?;
state.serialize_field("lastCommit", &self.commit)?;
state.serialize_field("name", &self.directory.name())?;
state.serialize_field("path", &self.directory.location())?;
state.end()
}
}
// TODO(xla): Ensure correct by construction.
/// Entry in a Tree result.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct TreeEntry {
/// Extra info for the entry.
pub info: Info,
/// Absolute path to the object from the root of the repo.
pub path: PathBuf,
pub entry: directory::Entry,
}
impl From<directory::Entry> for TreeEntry {
fn from(entry: directory::Entry) -> Self {
Self { entry }
}
}
impl From<TreeEntry> for directory::Entry {
fn from(TreeEntry { entry }: TreeEntry) -> Self {
entry
}
}
#[cfg(feature = "serialize")]
@@ -76,97 +124,18 @@ impl Serialize for TreeEntry {
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Tree", 2)?;
state.serialize_field("path", &self.path)?;
state.serialize_field("info", &self.info)?;
const FIELDS: usize = 4;
let mut state = serializer.serialize_struct("TreeEntry", FIELDS)?;
state.serialize_field("path", &self.entry.location())?;
state.serialize_field("name", &self.entry.name())?;
state.serialize_field("lastCommit", &None::<commit::Header>)?;
state.serialize_field(
"kind",
match self.entry {
directory::Entry::File(_) => "blob",
directory::Entry::Directory(_) => "directory",
},
)?;
state.end()
}
}
/// Retrieve the [`Tree`] for the given `revision` and directory `prefix`.
///
/// # Errors
///
/// Will return [`Error`] if any of the surf interactions fail.
pub fn tree<P>(
repo: &Repository,
maybe_revision: Option<Revision>,
maybe_prefix: Option<&P>,
) -> Result<Tree, Error>
where
P: AsRef<Path>,
{
let maybe_prefix = maybe_prefix.map(|p| p.as_ref());
let rev = match maybe_revision {
Some(r) => r,
None => Revision::Branch {
name: refname!("main"),
remote: None,
},
};
let prefix_dir = match maybe_prefix {
None => repo.root_dir(&rev)?,
Some(path) => repo
.root_dir(&rev)?
.find_directory(path, repo)?
.ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
};
let mut entries = prefix_dir
.entries(repo)?
.entries()
.fold(Vec::new(), |mut entries, entry| {
let entry_path = match maybe_prefix {
Some(path) => {
let mut path = path.to_path_buf();
path.push(entry.name());
path
},
None => PathBuf::new(),
};
let info = Info {
name: entry.name().clone(),
object_type: match entry {
directory::Entry::Directory(_) => ObjectType::Tree,
directory::Entry::File { .. } => ObjectType::Blob,
},
last_commit: None,
};
entries.push(TreeEntry {
info,
path: entry_path,
});
entries
});
// We want to ensure that in the response Tree entries come first. `Ord` being
// derived on the enum ensures Variant declaration order.
//
// https://doc.rust-lang.org/std/cmp/trait.Ord.html#derivable
entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
let last_commit = if maybe_prefix.is_none() {
let history = repo.history(&rev)?;
Some(commit::Header::from(history.head()))
} else {
None
};
let name = match maybe_prefix {
None => "".into(),
Some(path) => path.file_name().unwrap().to_str().unwrap().to_string(),
};
let info = Info {
name,
object_type: ObjectType::Tree,
last_commit,
};
Ok(Tree {
path: maybe_prefix.map_or(PathBuf::new(), |path| path.to_path_buf()),
entries,
info,
})
}
-189
View File
@@ -1,189 +0,0 @@
// This file is part of radicle-surf
// <https://github.com/radicle-dev/radicle-surf>
//
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 or
// later as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Represents revisions
use std::collections::BTreeSet;
use git_ref_format::{lit, refspec, Qualified, RefString};
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
use radicle_git_ext::Oid;
use crate::git::{self, Error, Glob, Repository};
/// Types of a peer.
pub enum Category<P, U> {
/// Local peer.
Local {
/// Peer Id
peer_id: P,
/// User name
user: U,
},
/// Remote peer.
Remote {
/// Peer Id
peer_id: P,
/// User name
user: U,
},
}
/// A revision selector for a `Browser`.
#[cfg_attr(
feature = "serialize",
derive(Deserialize, Serialize),
serde(rename_all = "camelCase", tag = "type")
)]
#[derive(Debug, Clone)]
pub enum Revision {
/// Select a tag under the name provided.
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
Tag {
/// Name of the tag.
name: RefString,
},
/// Select a branch under the name provided.
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
Branch {
/// Name of the branch.
name: RefString,
/// The remote peer, if specified.
remote: Option<RefString>,
},
/// Select a SHA1 under the name provided.
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
Sha {
/// The SHA1 value.
sha: Oid,
},
}
impl git::Revision for &Revision {
type Error = git2::Error;
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
match self {
Revision::Tag { name } => match name.qualified() {
None => Qualified::from(lit::refs_tags(name)).object_id(repo),
Some(name) => name.object_id(repo),
},
Revision::Branch { name, remote } => match remote {
Some(remote) => {
Qualified::from(lit::refs_remotes(remote.join(name))).object_id(repo)
},
None => git::Branch::local(name).refname().object_id(repo),
},
Revision::Sha { sha } => Ok(*sha),
}
}
}
/// Bundled response to retrieve both branches and tags for
/// a user's repo.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Revisions<P, U> {
/// The peer peer_id for the user.
pub peer_id: P,
/// The user who owns these revisions.
pub user: U,
/// List of branch reference names.
pub branches: BTreeSet<RefString>,
/// List of tag reference names.
pub tags: BTreeSet<RefString>,
}
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
/// remote branches.
///
/// If there are no branches then this returns `None`.
///
/// # Errors
///
/// * If we cannot get the branches from the `Browser`
pub fn remote<U>(
repo: &Repository,
peer_id: RefString,
user: U,
) -> Result<Revisions<RefString, U>, Error> {
let pattern = peer_id.to_pattern(refspec::pattern!("*"));
let branches = repo
.branch_names(Glob::remotes(pattern))?
.map(|name| name.map(RefString::from))
.collect::<Result<_, _>>()?;
Ok(Revisions {
peer_id,
user,
branches,
// TODO(rudolfs): implement remote peer tags once we decide how
// https://radicle.community/t/git-tags/214
tags: BTreeSet::new(),
})
}
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
/// local branches.
///
/// If there are no branches then this returns `None`.
///
/// # Errors
///
/// * If we cannot get the branches from the `Browser`
pub fn local<U>(
repo: &Repository,
peer_id: RefString,
user: U,
) -> Result<Revisions<RefString, U>, Error> {
let branches = repo
.branch_names(Glob::all_heads())?
.map(|name| name.map(RefString::from))
.collect::<Result<_, _>>()?;
let tags = repo
.tag_names()?
.map(|name| name.map(RefString::from))
.collect::<Result<_, _>>()?;
Ok(Revisions {
peer_id,
user,
branches,
tags,
})
}
/// Provide the [`Revisions`] of a peer.
///
/// If the peer is [`Category::Local`], meaning that is the current person doing
/// the browsing and no remote is set for the reference.
///
/// Othewise, the peer is [`Category::Remote`], meaning that we are looking into
/// a remote part of a reference.
///
/// # Errors
///
/// * If we cannot get the branches from the `Browser`
pub fn revisions<U>(
repo: &Repository,
peer: Category<RefString, U>,
) -> Result<Revisions<RefString, U>, Error> {
match peer {
Category::Local { peer_id, user } => local(repo, peer_id, user),
Category::Remote { peer_id, user } => remote(repo, peer_id, user),
}
}
@@ -15,7 +15,7 @@
// 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::path;
use std::path::Path;
use syntect::{
easy::HighlightLines,
@@ -56,10 +56,14 @@ lazy_static::lazy_static! {
};
}
/// Return a [`BlobContent`] given a file path, content and theme. Attempts to
/// perform syntax highlighting when the theme is `Some`.
pub fn highlight(path: &str, content: &str, theme_name: &str) -> Option<String> {
let syntax = path::Path::new(path)
/// Return highlighted text given a file path, the original text, and
/// a theme.
pub fn highlight<P>(path: P, content: &str, theme_name: &str) -> Option<String>
where
P: AsRef<Path>,
{
let syntax = path
.as_ref()
.extension()
.and_then(std::ffi::OsStr::to_str)
.and_then(|ext| SYNTAX_SET.find_syntax_by_extension(ext));
@@ -73,12 +77,13 @@ pub fn highlight(path: &str, content: &str, theme_name: &str) -> Option<String>
let mut html = String::with_capacity(content.len());
for line in LinesWithEndings::from(content) {
let regions = highlighter.highlight(line, &SYNTAX_SET);
let regions = highlighter.highlight_line(line, &SYNTAX_SET).ok()?;
syntect::html::append_highlighted_html_for_styled_line(
&regions[..],
syntect::html::IncludeBackground::No,
&mut html,
);
)
.ok();
}
Some(html)
},
+96
View File
@@ -0,0 +1,96 @@
// This file is part of radicle-surf
// <https://github.com/radicle-dev/radicle-surf>
//
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 or
// later as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::collections::{BTreeMap, BTreeSet};
use git_ref_format::{RefStr, RefString};
use crate::git::{Error, Glob, Repository};
/// A `View` represents a logical view of peer's repository in the
/// [Heartwood protocol][heartwood].
///
/// The `Id` parameter allows the specification of what the identifier
/// for a peer is in Heartwood.
///
/// The `P` parameter allows the specification of what a person looks
/// like in Heartwood, whether that be a DID document, website, etc.
///
/// [heartwood]: https://github.com/radicle-dev/heartwood
pub struct View<Id, P> {
/// Identifier for the peer that this `View` is associated to.
pub identifier: Id,
/// Personal information for the peer that this `View` is associated to.
pub person: Option<P>,
/// All `refs/heads` and `refs/remotes` reference names.
pub branches: BTreeSet<RefString>,
/// All `refs/tags` reference names.
pub tags: BTreeSet<RefString>,
/// Any `refs/<category>` reference names, where each key in the map is a
/// `<category>` and the set is the references under that category.
pub references: BTreeMap<RefString, BTreeSet<RefString>>,
}
/// Construct a [`View`] for the `identifier` and `person`.
///
/// `identifier` is assumed to act as the namespace in the Heartwood storage.
///
/// `categories` is the set of git non-standard reference categories,
/// i.e. `refs/heads`, `refs/remotes`, `refs/tags`, and `refs/notes`.
pub fn view<Id, R, P>(
repo: &Repository,
identifier: Id,
person: Option<P>,
categories: impl IntoIterator<Item = RefString>,
) -> Result<View<Id, P>, Error>
where
Id: Clone + Into<R>,
R: AsRef<RefStr>,
{
let namespace = identifier.clone().into();
repo.with_namespace(&namespace.as_ref().to_ref_string(), move || {
let branches = repo
.branch_names(Glob::all_heads())?
.map(|name| name.map(RefString::from))
.collect::<Result<_, _>>()?;
let tags = repo
.tag_names(&Glob::all_tags())?
.map(|name| name.map(RefString::from))
.collect::<Result<_, _>>()?;
let categories = categories.into_iter().fold(Glob::default(), |globs, cat| {
globs.and(Glob::all_category(cat))
});
let references =
repo.categories(&categories)?
.try_fold(BTreeMap::new(), |mut map, r| {
let (cat, name) = r?;
map.entry(cat)
.and_modify(|names: &mut BTreeSet<RefString>| {
names.insert(name.clone());
})
.or_insert_with(|| BTreeSet::from_iter(Some(name)));
Ok::<_, Error>(map)
})?;
Ok(View {
identifier,
person,
branches,
tags,
references,
})
})
}
+6 -6
View File
@@ -18,7 +18,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(Path::new("src/memory.rs"), &oid)
.expect("Failed to get last commit")
.map(|commit| commit.id);
@@ -26,7 +26,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(Path::new("README.md"), &oid)
.expect("Failed to get last commit")
.map(|commit| commit.id);
@@ -44,7 +44,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(Path::new("examples/Folder.svelte"), &oid)
.expect("Failed to get last commit")
.map(|commit| commit.id);
@@ -64,7 +64,7 @@ fn nest_directory() {
let nested_directory_tree_commit_id = repo
.last_commit(
Path::new("this/is/a/really/deeply/nested/directory/tree"),
oid,
&oid,
)
.expect("Failed to get last commit")
.map(|commit| commit.id);
@@ -85,13 +85,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(Path::new(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(Path::new("special/👹👹👹"), &oid)
.expect("Failed to get last commit")
.map(|commit| commit.id);
assert_eq!(ogre_commit_id, Some(expected_commit_id));
+4 -4
View File
@@ -8,7 +8,7 @@ use super::GIT_PLATINUM;
fn switch_to_banana() -> Result<(), Error> {
let repo = Repository::open(GIT_PLATINUM)?;
let history_master = repo.history(&Branch::local(refname!("master")))?;
repo.switch_namespace("golden")?;
repo.switch_namespace(&refname!("golden"))?;
let history_banana = repo.history(&Branch::local(refname!("banana")))?;
assert_ne!(history_master.head(), history_banana.head());
@@ -23,7 +23,7 @@ fn me_namespace() -> Result<(), Error> {
assert_eq!(repo.which_namespace().unwrap(), None);
repo.switch_namespace("me")?;
repo.switch_namespace(&refname!("me"))?;
assert_eq!(repo.which_namespace().unwrap(), Some("me".parse()?));
let history_feature = repo.history(&Branch::local(refname!("feature/#1194")))?;
@@ -58,7 +58,7 @@ fn golden_namespace() -> Result<(), Error> {
assert_eq!(repo.which_namespace().unwrap(), None);
repo.switch_namespace("golden")?;
repo.switch_namespace(&refname!("golden"))?;
assert_eq!(repo.which_namespace().unwrap(), Some("golden".parse()?));
@@ -101,7 +101,7 @@ fn silver_namespace() -> Result<(), Error> {
assert_eq!(repo.which_namespace().unwrap(), None);
repo.switch_namespace("golden/silver")?;
repo.switch_namespace(&refname!("golden/silver"))?;
assert_eq!(
repo.which_namespace().unwrap(),
Some("golden/silver".parse()?)
+3 -3
View File
@@ -44,7 +44,7 @@ fn _master() -> Result<(), Error> {
fn commit() -> Result<(), Error> {
let repo = Repository::open(GIT_PLATINUM)?;
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
let mut history = repo.history(rev)?;
let mut history = repo.history(&rev)?;
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
assert!(history.any(|commit| commit.unwrap().id == commit1));
@@ -56,7 +56,7 @@ fn commit() -> Result<(), Error> {
fn commit_parents() -> Result<(), Error> {
let repo = Repository::open(GIT_PLATINUM)?;
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
let history = repo.history(rev)?;
let history = repo.history(&rev)?;
let commit = history.head();
assert_eq!(
@@ -71,7 +71,7 @@ fn commit_parents() -> Result<(), Error> {
fn commit_short() -> Result<(), Error> {
let repo = Repository::open(GIT_PLATINUM)?;
let rev = repo.oid("3873745c8")?;
let mut history = repo.history(rev)?;
let mut history = repo.history(&rev)?;
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
assert!(history.any(|commit| commit.unwrap().id == commit1));