Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
radicle-surf: refactor source objects
Fintan Halpenny committed 3 years ago
commit b4d3bf69cff13986a8a6ac604cd55babbd45b5e6
parent 02321ab
21 files changed +614 -618
modified radicle-surf/Cargo.toml
@@ -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"
modified radicle-surf/examples/browsing.rs
@@ -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();
modified radicle-surf/src/file_system/directory.rs
@@ -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>,
    {
modified radicle-surf/src/git.rs
@@ -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)?)
    }
modified radicle-surf/src/git/glob.rs
@@ -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())
}
modified radicle-surf/src/git/history.rs
@@ -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()))?;
modified radicle-surf/src/git/repo.rs
@@ -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)
    }

modified radicle-surf/src/git/repo/iter.rs
@@ -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};
@@ -179,6 +221,18 @@ pub mod error {
    }

    #[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)]
        Git(#[from] git2::Error),
modified radicle-surf/src/lib.rs
@@ -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;
modified radicle-surf/src/source.rs
@@ -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;
modified radicle-surf/src/source/commit.rs
@@ -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 })
}

modified radicle-surf/src/source/object.rs
@@ -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};
-

-
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,
-
}
+
pub use tree::{Tree, TreeEntry};

-
#[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),
modified radicle-surf/src/source/object/blob.rs
@@ -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()),
modified radicle-surf/src/source/object/tree.rs
@@ -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,
-
    })
-
}
deleted radicle-surf/src/source/revision.rs
@@ -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),
-
    }
-
}
added radicle-surf/src/source/syntax.rs
@@ -0,0 +1,92 @@
+
// 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::path::Path;
+

+
use syntect::{
+
    easy::HighlightLines,
+
    highlighting::ThemeSet,
+
    parsing::SyntaxSet,
+
    util::LinesWithEndings,
+
};
+

+
lazy_static::lazy_static! {
+
    // The syntax set is slow to load (~30ms), so we make sure to only load it once.
+
    // It _will_ affect the latency of the first request that uses syntax highlighting,
+
    // but this is acceptable for now.
+
    pub static ref SYNTAX_SET: SyntaxSet = {
+
        let default_set = SyntaxSet::load_defaults_newlines();
+
        let mut builder = default_set.into_builder();
+

+
        if cfg!(debug_assertions) {
+
            // In development assets are relative to the proxy source.
+
            // Don't crash if we aren't able to load additional syntaxes for some reason.
+
            builder.add_from_folder("./assets", true).ok();
+
        } else {
+
            // In production assets are relative to the proxy executable.
+
            let exe_path = std::env::current_exe().expect("Can't get current exe path");
+
            let root_path = exe_path
+
                .parent()
+
                .expect("Could not get parent path of current executable");
+
            let mut tmp = root_path.to_path_buf();
+
            tmp.push("assets");
+
            let asset_path = tmp.to_str().expect("Couldn't convert pathbuf to str");
+

+
            // Don't crash if we aren't able to load additional syntaxes for some reason.
+
            match builder.add_from_folder(asset_path, true) {
+
                Ok(_) => (),
+
                Err(err) => log::warn!("Syntax builder error : {}", err),
+
            };
+
        }
+
        builder.build()
+
    };
+
}
+

+
/// 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));
+

+
    let ts = ThemeSet::load_defaults();
+
    let theme = ts.themes.get(theme_name);
+

+
    match (syntax, theme) {
+
        (Some(syntax), Some(theme)) => {
+
            let mut highlighter = HighlightLines::new(syntax, theme);
+
            let mut html = String::with_capacity(content.len());
+

+
            for line in LinesWithEndings::from(content) {
+
                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)
+
        },
+
        _ => None,
+
    }
+
}
added radicle-surf/src/source/view.rs
@@ -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,
+
        })
+
    })
+
}
deleted radicle-surf/src/syntax.rs
@@ -1,87 +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/>.
-

-
use std::path;
-

-
use syntect::{
-
    easy::HighlightLines,
-
    highlighting::ThemeSet,
-
    parsing::SyntaxSet,
-
    util::LinesWithEndings,
-
};
-

-
lazy_static::lazy_static! {
-
    // The syntax set is slow to load (~30ms), so we make sure to only load it once.
-
    // It _will_ affect the latency of the first request that uses syntax highlighting,
-
    // but this is acceptable for now.
-
    pub static ref SYNTAX_SET: SyntaxSet = {
-
        let default_set = SyntaxSet::load_defaults_newlines();
-
        let mut builder = default_set.into_builder();
-

-
        if cfg!(debug_assertions) {
-
            // In development assets are relative to the proxy source.
-
            // Don't crash if we aren't able to load additional syntaxes for some reason.
-
            builder.add_from_folder("./assets", true).ok();
-
        } else {
-
            // In production assets are relative to the proxy executable.
-
            let exe_path = std::env::current_exe().expect("Can't get current exe path");
-
            let root_path = exe_path
-
                .parent()
-
                .expect("Could not get parent path of current executable");
-
            let mut tmp = root_path.to_path_buf();
-
            tmp.push("assets");
-
            let asset_path = tmp.to_str().expect("Couldn't convert pathbuf to str");
-

-
            // Don't crash if we aren't able to load additional syntaxes for some reason.
-
            match builder.add_from_folder(asset_path, true) {
-
                Ok(_) => (),
-
                Err(err) => log::warn!("Syntax builder error : {}", err),
-
            };
-
        }
-
        builder.build()
-
    };
-
}
-

-
/// 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)
-
        .extension()
-
        .and_then(std::ffi::OsStr::to_str)
-
        .and_then(|ext| SYNTAX_SET.find_syntax_by_extension(ext));
-

-
    let ts = ThemeSet::load_defaults();
-
    let theme = ts.themes.get(theme_name);
-

-
    match (syntax, theme) {
-
        (Some(syntax), Some(theme)) => {
-
            let mut highlighter = HighlightLines::new(syntax, theme);
-
            let mut html = String::with_capacity(content.len());
-

-
            for line in LinesWithEndings::from(content) {
-
                let regions = highlighter.highlight(line, &SYNTAX_SET);
-
                syntect::html::append_highlighted_html_for_styled_line(
-
                    &regions[..],
-
                    syntect::html::IncludeBackground::No,
-
                    &mut html,
-
                );
-
            }
-
            Some(html)
-
        },
-
        _ => None,
-
    }
-
}
modified radicle-surf/t/src/git/last_commit.rs
@@ -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));
modified radicle-surf/t/src/git/namespace.rs
@@ -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()?)
modified radicle-surf/t/src/git/rev.rs
@@ -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));