Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'han/merge-source'
Fintan Halpenny committed 3 years ago
commit 2ba997727dedab98fc3992bc5b6de324ed29f4f0
parent a11beda
26 files changed +1142 -1353
modified .github/workflows/ci.yaml
@@ -85,9 +85,9 @@ jobs:
        with:
          fetch-depth: 0
      # The 'main' branch is needed for radicle-surf vcs::git::tests::test_submodule_failure,
-
      # however actions/checkout uses the default branch 'master' at this moment.
+
      # however actions/checkout uses the init.defaultBranch 'master' at this moment.
      # We will improve the test soon to remove this need.
-
      - run: git branch main
+
      - run: git branch -f main  # -f in case of running against 'main' branch.
      - run: git branch -u origin/main main
      - uses: actions-rs/toolchain@v1
        with:
@@ -120,7 +120,7 @@ jobs:
      - uses: actions/checkout@master
        with:
          fetch-depth: 0
-
      - run: git branch main
+
      - run: git branch -f main
      - run: git branch -u origin/main main
      - uses: actions-rs/toolchain@v1
        with:
modified Cargo.toml
@@ -6,7 +6,6 @@ members = [
  "radicle-git-ext",
  "radicle-git-types",
  "radicle-macros",
-
  "radicle-source",
  "radicle-std-ext",
  "radicle-surf",
  # TODO: port gitd-lib over
deleted radicle-source/Cargo.toml
@@ -1,31 +0,0 @@
-
[package]
-
name = "radicle-source"
-
description = "A high level API for browsing source files"
-
version = "0.4.0"
-
authors = ["The Radicle Team <dev@radicle.xyz>"]
-
edition = "2018"
-
homepage = "https://github.com/radicle-dev/radicle-surf"
-
repository = "https://github.com/radicle-dev/radicle-surf"
-
license = "GPL-3.0-or-later"
-

-
[features]
-
syntax = ["syntect"]
-

-
[dependencies]
-
base64 = "0.13"
-
log = "0.4"
-
lazy_static = "1.4"
-
nonempty = "0.6"
-
serde = { version = "1.0", features = [ "derive" ] }
-
syntect = { version = "4.2", optional = true }
-
thiserror = "1.0"
-

-
[dependencies.git2]
-
version = ">= 0.12"
-
default-features = false
-
features = []
-

-
[dependencies.radicle-surf]
-
version = "^0.8.0"
-
features = ["serialize"]
-
path = "../radicle-surf"
deleted radicle-source/src/branch.rs
@@ -1,108 +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::fmt;
-

-
use serde::{Deserialize, Serialize};
-

-
use radicle_surf::vcs::git::{self, Browser, RefScope};
-

-
use crate::error::Error;
-

-
/// Branch name representation.
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
-
pub struct Branch(pub(crate) String);
-

-
impl From<String> for Branch {
-
    fn from(name: String) -> Self {
-
        Self(name)
-
    }
-
}
-

-
impl From<git::Branch> for Branch {
-
    fn from(branch: git::Branch) -> Self {
-
        Self(branch.name.to_string())
-
    }
-
}
-

-
impl fmt::Display for Branch {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.0)
-
    }
-
}
-

-
/// Given a project id to a repo returns the list of branches.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn branches(browser: &Browser<'_>, filter: RefScope) -> Result<Vec<Branch>, Error> {
-
    let mut branches = browser
-
        .list_branches(filter)?
-
        .into_iter()
-
        .map(|b| Branch(b.name.name().to_string()))
-
        .collect::<Vec<Branch>>();
-

-
    branches.sort();
-

-
    Ok(branches)
-
}
-

-
/// Information about a locally checked out repository.
-
#[derive(Deserialize, Serialize)]
-
pub struct LocalState {
-
    /// List of branches.
-
    branches: Vec<Branch>,
-
}
-

-
/// Given a path to a repo returns the list of branches and if it is managed by
-
/// coco.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the repository doesn't exist.
-
pub fn local_state(repo_path: &str, default_branch: &str) -> Result<LocalState, Error> {
-
    let repo = git2::Repository::open(repo_path).map_err(git::error::Error::from)?;
-
    let first_branch = repo
-
        .branches(Some(git2::BranchType::Local))
-
        .map_err(git::error::Error::from)?
-
        .filter_map(|branch_result| {
-
            let (branch, _) = branch_result.ok()?;
-
            let name = branch.name().ok()?;
-
            name.map(String::from)
-
        })
-
        .min()
-
        .ok_or(Error::NoBranches)?;
-

-
    let repo = git::Repository::new(repo_path)?;
-

-
    let browser = match Browser::new(&repo, git::Branch::local(default_branch)) {
-
        Ok(browser) => browser,
-
        Err(_) => Browser::new(&repo, git::Branch::local(&first_branch))?,
-
    };
-

-
    let mut branches = browser
-
        .list_branches(RefScope::Local)?
-
        .into_iter()
-
        .map(|b| Branch(b.name.name().to_string()))
-
        .collect::<Vec<Branch>>();
-

-
    branches.sort();
-

-
    Ok(LocalState { branches })
-
}
deleted radicle-source/src/commit.rs
@@ -1,243 +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::convert::TryFrom as _;
-

-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use radicle_surf::{
-
    diff,
-
    vcs::git::{self, Browser, Rev},
-
};
-

-
use crate::{branch::Branch, error::Error, person::Person, revision::Revision};
-

-
/// Commit statistics.
-
#[derive(Clone, Serialize)]
-
pub struct Stats {
-
    /// Additions.
-
    pub additions: u64,
-
    /// Deletions.
-
    pub deletions: u64,
-
}
-

-
/// Representation of a changeset between two revs.
-
#[derive(Clone, Serialize)]
-
pub struct Commit {
-
    /// The commit header.
-
    pub header: Header,
-
    /// The change statistics for this commit.
-
    pub stats: Stats,
-
    /// The changeset introduced by this commit.
-
    pub diff: diff::Diff,
-
    /// The list of branches this commit belongs to.
-
    pub branches: Vec<Branch>,
-
}
-

-
/// Representation of a code commit.
-
#[derive(Clone)]
-
pub struct Header {
-
    /// Identifier of the commit in the form of a sha1 hash. Often referred to
-
    /// as oid or object id.
-
    pub sha1: git2::Oid,
-
    /// The author of the commit.
-
    pub author: Person,
-
    /// The summary of the commit message body.
-
    pub summary: String,
-
    /// The entire commit message body.
-
    pub message: String,
-
    /// The committer of the commit.
-
    pub committer: Person,
-
    /// The recorded time of the committer signature. This is a convenience
-
    /// alias until we expose the actual author and commiter signatures.
-
    pub committer_time: git2::Time,
-
}
-

-
impl Header {
-
    /// Returns the commit description text. This is the text after the one-line
-
    /// summary.
-
    #[must_use]
-
    pub fn description(&self) -> &str {
-
        self.message
-
            .strip_prefix(&self.summary)
-
            .unwrap_or(&self.message)
-
            .trim()
-
    }
-
}
-

-
impl From<&git::Commit> for Header {
-
    fn from(commit: &git::Commit) -> Self {
-
        Self {
-
            sha1: commit.id,
-
            author: Person {
-
                name: commit.author.name.clone(),
-
                email: commit.author.email.clone(),
-
            },
-
            summary: commit.summary.clone(),
-
            message: commit.message.clone(),
-
            committer: Person {
-
                name: commit.committer.name.clone(),
-
                email: commit.committer.email.clone(),
-
            },
-
            committer_time: commit.committer.time,
-
        }
-
    }
-
}
-

-
impl Serialize for Header {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Header", 6)?;
-
        state.serialize_field("sha1", &self.sha1.to_string())?;
-
        state.serialize_field("author", &self.author)?;
-
        state.serialize_field("summary", &self.summary)?;
-
        state.serialize_field("description", &self.description())?;
-
        state.serialize_field("committer", &self.committer)?;
-
        state.serialize_field("committerTime", &self.committer_time.seconds())?;
-
        state.end()
-
    }
-
}
-

-
/// A selection of commit headers and their statistics.
-
#[derive(Serialize)]
-
pub struct Commits {
-
    /// The commit headers
-
    pub headers: Vec<Header>,
-
    /// The statistics for the commit headers
-
    pub stats: radicle_surf::vcs::git::Stats,
-
}
-

-
/// Retrieves a [`Commit`].
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn commit(browser: &mut Browser<'_>, sha1: git2::Oid) -> Result<Commit, Error> {
-
    browser.commit(sha1)?;
-

-
    let history = browser.get();
-
    let commit = history.first();
-

-
    let diff = if let Some(parent) = commit.parents.first() {
-
        browser.diff(*parent, sha1)?
-
    } else {
-
        browser.initial_diff(sha1)?
-
    };
-

-
    let mut deletions = 0;
-
    let mut additions = 0;
-

-
    for file in &diff.modified {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    match line {
-
                        diff::LineDiff::Addition { .. } => additions += 1,
-
                        diff::LineDiff::Deletion { .. } => deletions += 1,
-
                        _ => {},
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    for file in &diff.created {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    if let diff::LineDiff::Addition { .. } = line {
-
                        additions += 1
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    for file in &diff.deleted {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    if let diff::LineDiff::Deletion { .. } = line {
-
                        deletions += 1
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    let branches = browser
-
        .revision_branches(sha1)?
-
        .into_iter()
-
        .map(Branch::from)
-
        .collect();
-

-
    Ok(Commit {
-
        header: Header::from(commit),
-
        stats: Stats {
-
            additions,
-
            deletions,
-
        },
-
        diff,
-
        branches,
-
    })
-
}
-

-
/// Retrieves the [`Header`] for the given `sha1`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn header(browser: &mut Browser<'_>, sha1: git2::Oid) -> Result<Header, Error> {
-
    browser.commit(sha1)?;
-

-
    let history = browser.get();
-
    let commit = history.first();
-

-
    Ok(Header::from(commit))
-
}
-

-
/// Retrieves the [`Commit`] history for the given `revision`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn commits<P>(
-
    browser: &mut Browser<'_>,
-
    maybe_revision: Option<Revision<P>>,
-
) -> Result<Commits, Error>
-
where
-
    P: ToString,
-
{
-
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
-

-
    if let Some(revision) = maybe_revision {
-
        browser.rev(revision)?;
-
    }
-

-
    let headers = browser.get().iter().map(Header::from).collect();
-
    let stats = browser.get_stats()?;
-

-
    Ok(Commits { headers, stats })
-
}
deleted radicle-source/src/error.rs
@@ -1,46 +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 radicle_surf::{file_system, git};
-

-
/// An error occurred when interacting with [`radicle_surf`] for browsing source
-
/// code.
-
#[derive(Debug, thiserror::Error)]
-
pub enum Error {
-
    /// We expect at least one [`crate::revision::Revisions`] when looking at a
-
    /// project, however the computation found none.
-
    #[error(
-
        "while trying to get user revisions we could not find any, there should be at least one"
-
    )]
-
    EmptyRevisions,
-

-
    /// An error occurred during a [`radicle_surf::file_system`] operation.
-
    #[error(transparent)]
-
    FileSystem(#[from] file_system::Error),
-

-
    /// An error occurred during a [`radicle_surf::git`] operation.
-
    #[error(transparent)]
-
    Git(#[from] git::error::Error),
-

-
    /// When trying to query a repositories branches, but there are none.
-
    #[error("the repository has no branches")]
-
    NoBranches,
-

-
    /// Trying to find a file path which could not be found.
-
    #[error("the path '{0}' was not found")]
-
    PathNotFound(file_system::Path),
-
}
deleted radicle-source/src/lib.rs
@@ -1,51 +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/>.
-

-
//! Source code related functionality.
-

-
/// To avoid incompatible versions of `radicle-surf`, `radicle-source`
-
/// re-exports the package under the `surf` alias.
-
pub use radicle_surf as surf;
-

-
pub mod branch;
-
pub use branch::{branches, local_state, Branch, LocalState};
-

-
pub mod commit;
-
pub use commit::{commit, commits, Commit};
-

-
pub mod error;
-
pub use error::Error;
-

-
pub mod object;
-
pub use object::{blob, tree, Blob, BlobContent, Info, ObjectType, Tree};
-

-
pub mod oid;
-
pub use oid::Oid;
-

-
pub mod person;
-
pub use person::Person;
-

-
pub mod revision;
-
pub use revision::Revision;
-

-
#[cfg(feature = "syntax")]
-
pub mod syntax;
-
#[cfg(feature = "syntax")]
-
pub use syntax::SYNTAX_SET;
-

-
pub mod tag;
-
pub use tag::{tags, Tag};
deleted radicle-source/src/object.rs
@@ -1,76 +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 serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
pub mod blob;
-
pub use blob::{blob, Blob, BlobContent};
-

-
pub mod tree;
-
pub use tree::{tree, Tree, TreeEntry};
-

-
use crate::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,
-
}
-

-
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>,
-
}
-

-
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()
-
    }
-
}
deleted radicle-source/src/object/blob.rs
@@ -1,218 +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::{
-
    convert::TryFrom as _,
-
    str::{self, FromStr as _},
-
};
-

-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use radicle_surf::{
-
    file_system,
-
    vcs::git::{Browser, Rev},
-
};
-

-
use crate::{
-
    commit,
-
    error::Error,
-
    object::{Info, ObjectType},
-
    revision::Revision,
-
};
-

-
#[cfg(feature = "syntax")]
-
use crate::syntax;
-

-
/// File data abstraction.
-
pub struct Blob {
-
    /// Actual content of the file, if the content is ASCII.
-
    pub content: BlobContent,
-
    /// Extra info for the file.
-
    pub info: Info,
-
    /// Absolute path to the object from the root of the repo.
-
    pub path: String,
-
}
-

-
impl Blob {
-
    /// Indicates if the content of the [`Blob`] is binary.
-
    #[must_use]
-
    pub fn is_binary(&self) -> bool {
-
        matches!(self.content, BlobContent::Binary(_))
-
    }
-

-
    /// Indicates if the content of the [`Blob`] is HTML.
-
    #[must_use]
-
    pub const fn is_html(&self) -> bool {
-
        matches!(self.content, BlobContent::Html(_))
-
    }
-
}
-

-
impl Serialize for Blob {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Blob", 5)?;
-
        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.end()
-
    }
-
}
-

-
/// Variants of blob content.
-
#[derive(PartialEq, Eq)]
-
pub enum BlobContent {
-
    /// Content is plain text and can be passed as a string.
-
    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.
-
    Html(String),
-
    /// Content is binary and needs special treatment.
-
    Binary(Vec<u8>),
-
}
-

-
impl Serialize for BlobContent {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        match self {
-
            Self::Plain(content) | Self::Html(content) => serializer.serialize_str(content),
-
            Self::Binary(bytes) => {
-
                let encoded = base64::encode(bytes);
-
                serializer.serialize_str(&encoded)
-
            },
-
        }
-
    }
-
}
-

-
/// 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,
-
) -> Result<Blob, Error>
-
where
-
    P: ToString,
-
{
-
    make_blob(browser, maybe_revision, path, content)
-
}
-

-
fn make_blob<P, C>(
-
    browser: &mut Browser,
-
    maybe_revision: Option<Revision<P>>,
-
    path: &str,
-
    content: C,
-
) -> Result<Blob, Error>
-
where
-
    P: ToString,
-
    C: FnOnce(&[u8]) -> BlobContent,
-
{
-
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
-
    if let Some(revision) = maybe_revision {
-
        browser.rev(revision)?;
-
    }
-

-
    let root = browser.get_directory()?;
-
    let p = file_system::Path::from_str(path)?;
-

-
    let file = root
-
        .find_file(p.clone())
-
        .ok_or_else(|| Error::PathNotFound(p.clone()))?;
-

-
    let mut commit_path = file_system::Path::root();
-
    commit_path.append(p.clone());
-

-
    let last_commit = browser
-
        .last_commit(commit_path)?
-
        .map(|c| commit::Header::from(&c));
-
    let (_rest, last) = p.split_last();
-

-
    let content = content(&file.contents);
-

-
    Ok(Blob {
-
        content,
-
        info: Info {
-
            name: last.to_string(),
-
            object_type: ObjectType::Blob,
-
            last_commit,
-
        },
-
        path: path.to_string(),
-
    })
-
}
-

-
/// 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()),
-
    }
-
}
-

-
#[cfg(feature = "syntax")]
-
pub mod highlighting {
-
    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)
-
        })
-
    }
-

-
    /// 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 {
-
        let content = match str::from_utf8(content) {
-
            Ok(content) => content,
-
            Err(_) => return BlobContent::Binary(content.to_owned()),
-
        };
-

-
        match theme_name {
-
            None => BlobContent::Plain(content.to_owned()),
-
            Some(theme) => syntax::highlight(path, content, theme)
-
                .map_or_else(|| BlobContent::Plain(content.to_owned()), BlobContent::Html),
-
        }
-
    }
-
}
deleted radicle-source/src/object/tree.rs
@@ -1,177 +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::{convert::TryFrom as _, str::FromStr as _};
-

-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use radicle_surf::{
-
    file_system,
-
    vcs::git::{Browser, Rev},
-
};
-

-
use crate::{
-
    commit,
-
    error::Error,
-
    object::{Info, ObjectType},
-
    revision::Revision,
-
};
-

-
/// 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: String,
-
    /// Entries listed in that tree result.
-
    pub entries: Vec<TreeEntry>,
-
    /// Extra info for the tree object.
-
    pub info: Info,
-
}
-

-
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)?;
-
        state.serialize_field("entries", &self.entries)?;
-
        state.serialize_field("info", &self.info)?;
-
        state.end()
-
    }
-
}
-

-
// TODO(xla): Ensure correct by construction.
-
/// Entry in a Tree result.
-
pub struct TreeEntry {
-
    /// Extra info for the entry.
-
    pub info: Info,
-
    /// Absolute path to the object from the root of the repo.
-
    pub path: String,
-
}
-

-
impl Serialize for TreeEntry {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Tree", 2)?;
-
        state.serialize_field("path", &self.path)?;
-
        state.serialize_field("info", &self.info)?;
-
        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>(
-
    browser: &mut Browser<'_>,
-
    maybe_revision: Option<Revision<P>>,
-
    maybe_prefix: Option<String>,
-
) -> Result<Tree, Error>
-
where
-
    P: ToString,
-
{
-
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
-
    let prefix = maybe_prefix.unwrap_or_default();
-

-
    if let Some(revision) = maybe_revision {
-
        browser.rev(revision)?;
-
    }
-

-
    let path = if prefix == "/" || prefix.is_empty() {
-
        file_system::Path::root()
-
    } else {
-
        file_system::Path::from_str(&prefix)?
-
    };
-

-
    let root_dir = browser.get_directory()?;
-
    let prefix_dir = if path.is_root() {
-
        root_dir
-
    } else {
-
        root_dir
-
            .find_directory(path.clone())
-
            .ok_or_else(|| Error::PathNotFound(path.clone()))?
-
    };
-
    let mut prefix_contents = prefix_dir.list_directory();
-
    prefix_contents.sort();
-

-
    let entries_results: Result<Vec<TreeEntry>, Error> = prefix_contents
-
        .iter()
-
        .map(|(label, system_type)| {
-
            let entry_path = if path.is_root() {
-
                file_system::Path::new(label.clone())
-
            } else {
-
                let mut p = path.clone();
-
                p.push(label.clone());
-
                p
-
            };
-
            let mut commit_path = file_system::Path::root();
-
            commit_path.append(entry_path.clone());
-

-
            let info = Info {
-
                name: label.to_string(),
-
                object_type: match system_type {
-
                    file_system::SystemType::Directory => ObjectType::Tree,
-
                    file_system::SystemType::File => ObjectType::Blob,
-
                },
-
                last_commit: None,
-
            };
-

-
            Ok(TreeEntry {
-
                info,
-
                path: entry_path.to_string(),
-
            })
-
        })
-
        .collect();
-

-
    let mut entries = entries_results?;
-

-
    // 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 path.is_root() {
-
        Some(commit::Header::from(browser.get().first()))
-
    } else {
-
        None
-
    };
-
    let name = if path.is_root() {
-
        "".into()
-
    } else {
-
        let (_first, last) = path.split_last();
-
        last.to_string()
-
    };
-
    let info = Info {
-
        name,
-
        object_type: ObjectType::Tree,
-
        last_commit,
-
    };
-

-
    Ok(Tree {
-
        path: prefix,
-
        entries,
-
        info,
-
    })
-
}
deleted radicle-source/src/oid.rs
@@ -1,44 +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::convert::TryFrom;
-

-
use serde::{Deserialize, Serialize};
-

-
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
-
#[serde(try_from = "&str", into = "String")]
-
pub struct Oid(pub git2::Oid);
-

-
impl TryFrom<&str> for Oid {
-
    type Error = git2::Error;
-

-
    fn try_from(value: &str) -> Result<Self, Self::Error> {
-
        value.parse().map(Oid)
-
    }
-
}
-

-
impl From<Oid> for String {
-
    fn from(oid: Oid) -> Self {
-
        oid.0.to_string()
-
    }
-
}
-

-
impl From<Oid> for git2::Oid {
-
    fn from(oid: Oid) -> Self {
-
        oid.0
-
    }
-
}
deleted radicle-source/src/person.rs
@@ -1,28 +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 serde::Serialize;
-

-
/// Representation of a person (e.g. committer, author, signer) from a
-
/// repository. Usually extracted from a signature.
-
#[derive(Clone, Debug, Serialize)]
-
pub struct Person {
-
    /// Name part of the commit signature.
-
    pub name: String,
-
    /// Email part of the commit signature.
-
    pub email: String,
-
}
deleted radicle-source/src/revision.rs
@@ -1,175 +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::convert::TryFrom;
-

-
use nonempty::NonEmpty;
-
use serde::{Deserialize, Serialize};
-

-
use radicle_surf::vcs::git::{self, Browser, RefScope, Rev};
-

-
use crate::{
-
    branch::{branches, Branch},
-
    error::Error,
-
    oid::Oid,
-
    tag::{tags, Tag},
-
};
-

-
pub enum Category<P, U> {
-
    Local { peer_id: P, user: U },
-
    Remote { peer_id: P, user: U },
-
}
-

-
/// A revision selector for a `Browser`.
-
#[derive(Debug, Clone, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase", tag = "type")]
-
pub enum Revision<P> {
-
    /// Select a tag under the name provided.
-
    #[serde(rename_all = "camelCase")]
-
    Tag {
-
        /// Name of the tag.
-
        name: String,
-
    },
-
    /// Select a branch under the name provided.
-
    #[serde(rename_all = "camelCase")]
-
    Branch {
-
        /// Name of the branch.
-
        name: String,
-
        /// The remote peer, if specified.
-
        peer_id: Option<P>,
-
    },
-
    /// Select a SHA1 under the name provided.
-
    #[serde(rename_all = "camelCase")]
-
    Sha {
-
        /// The SHA1 value.
-
        sha: Oid,
-
    },
-
}
-

-
impl<P> TryFrom<Revision<P>> for Rev
-
where
-
    P: ToString,
-
{
-
    type Error = Error;
-

-
    fn try_from(other: Revision<P>) -> Result<Self, Self::Error> {
-
        match other {
-
            Revision::Tag { name } => Ok(git::TagName::new(&name).into()),
-
            Revision::Branch { name, peer_id } => Ok(match peer_id {
-
                Some(peer) => {
-
                    git::Branch::remote(&format!("heads/{}", name), &peer.to_string()).into()
-
                },
-
                None => git::Branch::local(&name).into(),
-
            }),
-
            Revision::Sha { sha } => {
-
                let oid: git2::Oid = sha.into();
-
                Ok(oid.into())
-
            },
-
        }
-
    }
-
}
-

-
/// Bundled response to retrieve both [`Branch`]es and [`Tag`]s 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 [`git::Branch`].
-
    pub branches: NonEmpty<Branch>,
-
    /// List of [`git::Tag`].
-
    pub tags: Vec<Tag>,
-
}
-

-
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
-
/// branches as [`RefScope::Remote`].
-
///
-
/// If there are no branches then this returns `None`.
-
///
-
/// # Errors
-
///
-
///   * If we cannot get the branches from the `Browser`
-
pub fn remote<P, U>(
-
    browser: &Browser,
-
    peer_id: P,
-
    user: U,
-
) -> Result<Option<Revisions<P, U>>, Error>
-
where
-
    P: Clone + ToString,
-
{
-
    let remote_branches = branches(browser, Some(peer_id.clone()).into())?;
-
    Ok(
-
        NonEmpty::from_vec(remote_branches).map(|branches| Revisions {
-
            peer_id,
-
            user,
-
            branches,
-
            // TODO(rudolfs): implement remote peer tags once we decide how
-
            // https://radicle.community/t/git-tags/214
-
            tags: vec![],
-
        }),
-
    )
-
}
-

-
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
-
/// branches as [`RefScope::Local`].
-
///
-
/// If there are no branches then this returns `None`.
-
///
-
/// # Errors
-
///
-
///   * If we cannot get the branches from the `Browser`
-
pub fn local<P, U>(browser: &Browser, peer_id: P, user: U) -> Result<Option<Revisions<P, U>>, Error>
-
where
-
    P: Clone + ToString,
-
{
-
    let local_branches = branches(browser, RefScope::Local)?;
-
    let tags = tags(browser)?;
-
    Ok(
-
        NonEmpty::from_vec(local_branches).map(|branches| 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<P, U>(
-
    browser: &Browser,
-
    peer: Category<P, U>,
-
) -> Result<Option<Revisions<P, U>>, Error>
-
where
-
    P: Clone + ToString,
-
{
-
    match peer {
-
        Category::Local { peer_id, user } => local(browser, peer_id, user),
-
        Category::Remote { peer_id, user } => remote(browser, peer_id, user),
-
    }
-
}
deleted radicle-source/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,
-
    }
-
}
deleted radicle-source/src/tag.rs
@@ -1,60 +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::fmt;
-

-
use serde::Serialize;
-

-
use radicle_surf::{git::RefScope, vcs::git::Browser};
-

-
use crate::error::Error;
-

-
/// Tag name representation.
-
///
-
/// We still need full tag support.
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-
pub struct Tag(pub(crate) String);
-

-
impl From<String> for Tag {
-
    fn from(name: String) -> Self {
-
        Self(name)
-
    }
-
}
-

-
impl fmt::Display for Tag {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.0)
-
    }
-
}
-

-
/// Retrieves the list of [`Tag`] for the given project `id`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn tags(browser: &Browser<'_>) -> Result<Vec<Tag>, Error> {
-
    let tag_names = browser.list_tags(RefScope::Local)?;
-
    let mut tags: Vec<Tag> = tag_names
-
        .into_iter()
-
        .map(|tag_name| Tag(tag_name.name().to_string()))
-
        .collect();
-

-
    tags.sort();
-

-
    Ok(tags)
-
}
modified radicle-surf/Cargo.toml
@@ -24,9 +24,11 @@ serialize = ["serde"]
gh-actions = []

[dependencies]
+
base64 = "0.13"
either = "1.5"
nom = "6"
nonempty = "0.5"
+
radicle-git-ext = { path = "../radicle-git-ext", features = ["serde"] }
regex = ">= 1.5.5"
serde = { features = ["serde_derive"], optional = true, version = "1" }
thiserror = "1.0"
added radicle-surf/src/commit.rs
@@ -0,0 +1,266 @@
+
// 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 a commit.
+

+
use std::convert::TryFrom as _;
+

+
#[cfg(feature = "serialize")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::{
+
    diff,
+
    file_system,
+
    person::Person,
+
    revision::Revision,
+
    vcs::git::{self, BranchName, Browser, Rev},
+
};
+

+
/// Commit statistics.
+
#[cfg_attr(feature = "serialize", derive(Serialize))]
+
#[derive(Clone)]
+
pub struct Stats {
+
    /// Additions.
+
    pub additions: u64,
+
    /// Deletions.
+
    pub deletions: u64,
+
}
+

+
/// Representation of a changeset between two revs.
+
#[cfg_attr(feature = "serialize", derive(Serialize))]
+
#[derive(Clone)]
+
pub struct Commit {
+
    /// The commit header.
+
    pub header: Header,
+
    /// The change statistics for this commit.
+
    pub stats: Stats,
+
    /// The changeset introduced by this commit.
+
    pub diff: diff::Diff,
+
    /// The list of branches this commit belongs to.
+
    pub branches: Vec<BranchName>,
+
}
+

+
/// Representation of a code commit.
+
#[derive(Clone)]
+
pub struct Header {
+
    /// Identifier of the commit in the form of a sha1 hash. Often referred to
+
    /// as oid or object id.
+
    pub sha1: git2::Oid,
+
    /// The author of the commit.
+
    pub author: Person,
+
    /// The summary of the commit message body.
+
    pub summary: String,
+
    /// The entire commit message body.
+
    pub message: String,
+
    /// The committer of the commit.
+
    pub committer: Person,
+
    /// The recorded time of the committer signature. This is a convenience
+
    /// alias until we expose the actual author and commiter signatures.
+
    pub committer_time: git2::Time,
+
}
+

+
impl Header {
+
    /// Returns the commit description text. This is the text after the one-line
+
    /// summary.
+
    #[must_use]
+
    pub fn description(&self) -> &str {
+
        self.message
+
            .strip_prefix(&self.summary)
+
            .unwrap_or(&self.message)
+
            .trim()
+
    }
+
}
+

+
impl From<&git::Commit> for Header {
+
    fn from(commit: &git::Commit) -> Self {
+
        Self {
+
            sha1: commit.id,
+
            author: Person {
+
                name: commit.author.name.clone(),
+
                email: commit.author.email.clone(),
+
            },
+
            summary: commit.summary.clone(),
+
            message: commit.message.clone(),
+
            committer: Person {
+
                name: commit.committer.name.clone(),
+
                email: commit.committer.email.clone(),
+
            },
+
            committer_time: commit.committer.time,
+
        }
+
    }
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for Header {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Header", 6)?;
+
        state.serialize_field("sha1", &self.sha1.to_string())?;
+
        state.serialize_field("author", &self.author)?;
+
        state.serialize_field("summary", &self.summary)?;
+
        state.serialize_field("description", &self.description())?;
+
        state.serialize_field("committer", &self.committer)?;
+
        state.serialize_field("committerTime", &self.committer_time.seconds())?;
+
        state.end()
+
    }
+
}
+

+
/// A selection of commit headers and their statistics.
+
#[cfg_attr(feature = "serialize", derive(Serialize))]
+
pub struct Commits {
+
    /// The commit headers
+
    pub headers: Vec<Header>,
+
    /// The statistics for the commit headers
+
    pub stats: git::Stats,
+
}
+

+
/// Retrieves a [`Commit`].
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn commit(browser: &mut Browser<'_>, sha1: git2::Oid) -> Result<Commit, Error> {
+
    browser.commit(sha1)?;
+

+
    let history = browser.get();
+
    let commit = history.first();
+

+
    let diff = if let Some(parent) = commit.parents.first() {
+
        browser.diff(*parent, sha1)?
+
    } else {
+
        browser.initial_diff(sha1)?
+
    };
+

+
    let mut deletions = 0;
+
    let mut additions = 0;
+

+
    for file in &diff.modified {
+
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
+
            for hunk in hunks.iter() {
+
                for line in &hunk.lines {
+
                    match line {
+
                        diff::LineDiff::Addition { .. } => additions += 1,
+
                        diff::LineDiff::Deletion { .. } => deletions += 1,
+
                        _ => {},
+
                    }
+
                }
+
            }
+
        }
+
    }
+

+
    for file in &diff.created {
+
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
+
            for hunk in hunks.iter() {
+
                for line in &hunk.lines {
+
                    if let diff::LineDiff::Addition { .. } = line {
+
                        additions += 1
+
                    }
+
                }
+
            }
+
        }
+
    }
+

+
    for file in &diff.deleted {
+
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
+
            for hunk in hunks.iter() {
+
                for line in &hunk.lines {
+
                    if let diff::LineDiff::Deletion { .. } = line {
+
                        deletions += 1
+
                    }
+
                }
+
            }
+
        }
+
    }
+

+
    let branches = browser
+
        .revision_branches(sha1)?
+
        .into_iter()
+
        .map(|b| b.name)
+
        .collect();
+

+
    Ok(Commit {
+
        header: Header::from(commit),
+
        stats: Stats {
+
            additions,
+
            deletions,
+
        },
+
        diff,
+
        branches,
+
    })
+
}
+

+
/// Retrieves the [`Header`] for the given `sha1`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn header(browser: &mut Browser<'_>, sha1: git2::Oid) -> Result<Header, Error> {
+
    browser.commit(sha1)?;
+

+
    let history = browser.get();
+
    let commit = history.first();
+

+
    Ok(Header::from(commit))
+
}
+

+
/// Retrieves the [`Commit`] history for the given `revision`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn commits<P>(
+
    browser: &mut Browser<'_>,
+
    maybe_revision: Option<Revision<P>>,
+
) -> Result<Commits, Error>
+
where
+
    P: ToString,
+
{
+
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
+

+
    if let Some(revision) = maybe_revision {
+
        browser.rev(revision)?;
+
    }
+

+
    let headers = browser.get().iter().map(Header::from).collect();
+
    let stats = browser.get_stats()?;
+

+
    Ok(Commits { headers, stats })
+
}
+

+
/// An error reported by commit API.
+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    /// An error occurred during a file system operation.
+
    #[error(transparent)]
+
    FileSystem(#[from] file_system::Error),
+

+
    /// An error occurred during a git operation.
+
    #[error(transparent)]
+
    Git(#[from] git::error::Error),
+

+
    /// Trying to find a file path which could not be found.
+
    #[error("the path '{0}' was not found")]
+
    PathNotFound(file_system::Path),
+
}
modified radicle-surf/src/lib.rs
@@ -86,6 +86,23 @@ pub mod diff;
pub mod file_system;
pub mod vcs;

+
pub mod commit;
+
pub use commit::{commit, commits, Commit};
+

+
pub mod object;
+
pub use object::{blob, tree as objectTree, Blob, BlobContent, Info, ObjectType, Tree};
+

+
pub mod person;
+
pub use person::Person;
+

+
pub mod revision;
+
pub use revision::Revision;
+

+
#[cfg(feature = "syntax")]
+
pub mod syntax;
+
#[cfg(feature = "syntax")]
+
pub use syntax::SYNTAX_SET;
+

// Private modules
mod nonempty;
mod tree;
added radicle-surf/src/object.rs
@@ -0,0 +1,98 @@
+
// 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/>.
+

+
//! Common definitions for git objects (blob and tree).
+
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+

+
#[cfg(feature = "serialize")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
pub mod blob;
+
pub use blob::{blob, Blob, BlobContent};
+

+
pub mod tree;
+
pub use tree::{tree, Tree, TreeEntry};
+

+
use crate::{commit, file_system, git};
+

+
/// 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()
+
    }
+
}
+

+
/// An error reported by object types.
+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    /// An error occurred during a file system operation.
+
    #[error(transparent)]
+
    FileSystem(#[from] file_system::Error),
+

+
    /// An error occurred during a git operation.
+
    #[error(transparent)]
+
    Git(#[from] git::error::Error),
+

+
    /// Trying to find a file path which could not be found.
+
    #[error("the path '{0}' was not found")]
+
    PathNotFound(file_system::Path),
+
}
added radicle-surf/src/object/blob.rs
@@ -0,0 +1,220 @@
+
// 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 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::{
+
    convert::TryFrom as _,
+
    str::{self, FromStr as _},
+
};
+

+
#[cfg(feature = "serialize")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::{
+
    commit,
+
    file_system,
+
    object::{Error, Info, ObjectType},
+
    revision::Revision,
+
    vcs::git::{Browser, Rev},
+
};
+

+
#[cfg(feature = "syntax")]
+
use crate::syntax;
+

+
/// File data abstraction.
+
pub struct Blob {
+
    /// Actual content of the file, if the content is ASCII.
+
    pub content: BlobContent,
+
    /// Extra info for the file.
+
    pub info: Info,
+
    /// Absolute path to the object from the root of the repo.
+
    pub path: String,
+
}
+

+
impl Blob {
+
    /// Indicates if the content of the [`Blob`] is binary.
+
    #[must_use]
+
    pub fn is_binary(&self) -> bool {
+
        matches!(self.content, BlobContent::Binary(_))
+
    }
+

+
    /// Indicates if the content of the [`Blob`] is HTML.
+
    #[must_use]
+
    pub const fn is_html(&self) -> bool {
+
        matches!(self.content, BlobContent::Html(_))
+
    }
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for Blob {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Blob", 5)?;
+
        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.end()
+
    }
+
}
+

+
/// Variants of blob content.
+
#[derive(PartialEq, Eq)]
+
pub enum BlobContent {
+
    /// Content is plain text and can be passed as a string.
+
    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.
+
    Html(String),
+
    /// Content is binary and needs special treatment.
+
    Binary(Vec<u8>),
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for BlobContent {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        match self {
+
            Self::Plain(content) | Self::Html(content) => serializer.serialize_str(content),
+
            Self::Binary(bytes) => {
+
                let encoded = base64::encode(bytes);
+
                serializer.serialize_str(&encoded)
+
            },
+
        }
+
    }
+
}
+

+
/// 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,
+
) -> Result<Blob, Error>
+
where
+
    P: ToString,
+
{
+
    make_blob(browser, maybe_revision, path, content)
+
}
+

+
fn make_blob<P, C>(
+
    browser: &mut Browser,
+
    maybe_revision: Option<Revision<P>>,
+
    path: &str,
+
    content: C,
+
) -> Result<Blob, Error>
+
where
+
    P: ToString,
+
    C: FnOnce(&[u8]) -> BlobContent,
+
{
+
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
+
    if let Some(revision) = maybe_revision {
+
        browser.rev(revision)?;
+
    }
+

+
    let root = browser.get_directory()?;
+
    let p = file_system::Path::from_str(path)?;
+

+
    let file = root
+
        .find_file(p.clone())
+
        .ok_or_else(|| Error::PathNotFound(p.clone()))?;
+

+
    let mut commit_path = file_system::Path::root();
+
    commit_path.append(p.clone());
+

+
    let last_commit = browser
+
        .last_commit(commit_path)?
+
        .map(|c| commit::Header::from(&c));
+
    let (_rest, last) = p.split_last();
+

+
    let content = content(&file.contents);
+

+
    Ok(Blob {
+
        content,
+
        info: Info {
+
            name: last.to_string(),
+
            object_type: ObjectType::Blob,
+
            last_commit,
+
        },
+
        path: path.to_string(),
+
    })
+
}
+

+
/// 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()),
+
    }
+
}
+

+
#[cfg(feature = "syntax")]
+
pub mod highlighting {
+
    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)
+
        })
+
    }
+

+
    /// 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 {
+
        let content = match str::from_utf8(content) {
+
            Ok(content) => content,
+
            Err(_) => return BlobContent::Binary(content.to_owned()),
+
        };
+

+
        match theme_name {
+
            None => BlobContent::Plain(content.to_owned()),
+
            Some(theme) => syntax::highlight(path, content, theme)
+
                .map_or_else(|| BlobContent::Plain(content.to_owned()), BlobContent::Html),
+
        }
+
    }
+
}
added radicle-surf/src/object/tree.rs
@@ -0,0 +1,179 @@
+
// 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 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::{convert::TryFrom as _, str::FromStr as _};
+

+
#[cfg(feature = "serialize")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::{
+
    commit,
+
    file_system,
+
    object::{Error, Info, ObjectType},
+
    revision::Revision,
+
    vcs::git::{Browser, Rev},
+
};
+

+
/// 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: String,
+
    /// Entries listed in that tree result.
+
    pub entries: Vec<TreeEntry>,
+
    /// Extra info for the tree object.
+
    pub info: Info,
+
}
+

+
#[cfg(feature = "serialize")]
+
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)?;
+
        state.serialize_field("entries", &self.entries)?;
+
        state.serialize_field("info", &self.info)?;
+
        state.end()
+
    }
+
}
+

+
// TODO(xla): Ensure correct by construction.
+
/// Entry in a Tree result.
+
pub struct TreeEntry {
+
    /// Extra info for the entry.
+
    pub info: Info,
+
    /// Absolute path to the object from the root of the repo.
+
    pub path: String,
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for TreeEntry {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Tree", 2)?;
+
        state.serialize_field("path", &self.path)?;
+
        state.serialize_field("info", &self.info)?;
+
        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>(
+
    browser: &mut Browser<'_>,
+
    maybe_revision: Option<Revision<P>>,
+
    maybe_prefix: Option<String>,
+
) -> Result<Tree, Error>
+
where
+
    P: ToString,
+
{
+
    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
+
    let prefix = maybe_prefix.unwrap_or_default();
+

+
    if let Some(revision) = maybe_revision {
+
        browser.rev(revision)?;
+
    }
+

+
    let path = if prefix == "/" || prefix.is_empty() {
+
        file_system::Path::root()
+
    } else {
+
        file_system::Path::from_str(&prefix)?
+
    };
+

+
    let root_dir = browser.get_directory()?;
+
    let prefix_dir = if path.is_root() {
+
        root_dir
+
    } else {
+
        root_dir
+
            .find_directory(path.clone())
+
            .ok_or_else(|| Error::PathNotFound(path.clone()))?
+
    };
+
    let mut prefix_contents = prefix_dir.list_directory();
+
    prefix_contents.sort();
+

+
    let entries_results: Result<Vec<TreeEntry>, Error> = prefix_contents
+
        .iter()
+
        .map(|(label, system_type)| {
+
            let entry_path = if path.is_root() {
+
                file_system::Path::new(label.clone())
+
            } else {
+
                let mut p = path.clone();
+
                p.push(label.clone());
+
                p
+
            };
+
            let mut commit_path = file_system::Path::root();
+
            commit_path.append(entry_path.clone());
+

+
            let info = Info {
+
                name: label.to_string(),
+
                object_type: match system_type {
+
                    file_system::SystemType::Directory => ObjectType::Tree,
+
                    file_system::SystemType::File => ObjectType::Blob,
+
                },
+
                last_commit: None,
+
            };
+

+
            Ok(TreeEntry {
+
                info,
+
                path: entry_path.to_string(),
+
            })
+
        })
+
        .collect();
+

+
    let mut entries = entries_results?;
+

+
    // 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 path.is_root() {
+
        Some(commit::Header::from(browser.get().first()))
+
    } else {
+
        None
+
    };
+
    let name = if path.is_root() {
+
        "".into()
+
    } else {
+
        let (_first, last) = path.split_last();
+
        last.to_string()
+
    };
+
    let info = Info {
+
        name,
+
        object_type: ObjectType::Tree,
+
        last_commit,
+
    };
+

+
    Ok(Tree {
+
        path: prefix,
+
        entries,
+
        info,
+
    })
+
}
added radicle-surf/src/person.rs
@@ -0,0 +1,32 @@
+
// 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 a person in a repo.
+

+
#[cfg(feature = "serialize")]
+
use serde::Serialize;
+

+
/// Representation of a person (e.g. committer, author, signer) from a
+
/// repository. Usually extracted from a signature.
+
#[cfg_attr(feature = "serialize", derive(Serialize))]
+
#[derive(Clone, Debug)]
+
pub struct Person {
+
    /// Name part of the commit signature.
+
    pub name: String,
+
    /// Email part of the commit signature.
+
    pub email: String,
+
}
added radicle-surf/src/revision.rs
@@ -0,0 +1,194 @@
+
// 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::convert::TryFrom;
+

+
use nonempty::NonEmpty;
+

+
#[cfg(feature = "serialize")]
+
use serde::{Deserialize, Serialize};
+

+
use radicle_git_ext::Oid;
+

+
use crate::{
+
    git::BranchName,
+
    vcs::git::{self, error::Error, Browser, RefScope, Rev, TagName},
+
};
+

+
/// 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<P> {
+
    /// Select a tag under the name provided.
+
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
+
    Tag {
+
        /// Name of the tag.
+
        name: String,
+
    },
+
    /// Select a branch under the name provided.
+
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
+
    Branch {
+
        /// Name of the branch.
+
        name: String,
+
        /// The remote peer, if specified.
+
        peer_id: Option<P>,
+
    },
+
    /// Select a SHA1 under the name provided.
+
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
+
    Sha {
+
        /// The SHA1 value.
+
        sha: Oid,
+
    },
+
}
+

+
impl<P> TryFrom<Revision<P>> for Rev
+
where
+
    P: ToString,
+
{
+
    type Error = Error;
+

+
    fn try_from(other: Revision<P>) -> Result<Self, Self::Error> {
+
        match other {
+
            Revision::Tag { name } => Ok(git::TagName::new(&name).into()),
+
            Revision::Branch { name, peer_id } => Ok(match peer_id {
+
                Some(peer) => {
+
                    git::Branch::remote(&format!("heads/{}", name), &peer.to_string()).into()
+
                },
+
                None => git::Branch::local(&name).into(),
+
            }),
+
            Revision::Sha { sha } => {
+
                let oid: git2::Oid = sha.into();
+
                Ok(oid.into())
+
            },
+
        }
+
    }
+
}
+

+
/// Bundled response to retrieve both [`BranchName`]es and [`TagName`]s 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 [`git::BranchName`].
+
    pub branches: NonEmpty<BranchName>,
+
    /// List of [`git::TagName`].
+
    pub tags: Vec<TagName>,
+
}
+

+
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
+
/// branches as [`RefScope::Remote`].
+
///
+
/// If there are no branches then this returns `None`.
+
///
+
/// # Errors
+
///
+
///   * If we cannot get the branches from the `Browser`
+
pub fn remote<P, U>(
+
    browser: &Browser,
+
    peer_id: P,
+
    user: U,
+
) -> Result<Option<Revisions<P, U>>, Error>
+
where
+
    P: Clone + ToString,
+
{
+
    let remote_branches = browser.branch_names(Some(peer_id.clone()).into())?;
+
    Ok(
+
        NonEmpty::from_vec(remote_branches).map(|branches| Revisions {
+
            peer_id,
+
            user,
+
            branches,
+
            // TODO(rudolfs): implement remote peer tags once we decide how
+
            // https://radicle.community/t/git-tags/214
+
            tags: vec![],
+
        }),
+
    )
+
}
+

+
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
+
/// branches as [`RefScope::Local`].
+
///
+
/// If there are no branches then this returns `None`.
+
///
+
/// # Errors
+
///
+
///   * If we cannot get the branches from the `Browser`
+
pub fn local<P, U>(browser: &Browser, peer_id: P, user: U) -> Result<Option<Revisions<P, U>>, Error>
+
where
+
    P: Clone + ToString,
+
{
+
    let local_branches = browser.branch_names(RefScope::Local)?;
+
    let tags = browser.tag_names()?;
+
    Ok(
+
        NonEmpty::from_vec(local_branches).map(|branches| 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<P, U>(
+
    browser: &Browser,
+
    peer: Category<P, U>,
+
) -> Result<Option<Revisions<P, U>>, Error>
+
where
+
    P: Clone + ToString,
+
{
+
    match peer {
+
        Category::Local { peer_id, user } => local(browser, peer_id, user),
+
        Category::Remote { peer_id, user } => remote(browser, peer_id, user),
+
    }
+
}
added radicle-surf/src/syntax.rs
@@ -0,0 +1,87 @@
+
// 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/src/vcs/git.rs
@@ -616,6 +616,24 @@ impl<'a> Browser<'a> {
        self.repository.list_branches(filter)
    }

+
    /// Given a project id to a repo returns the list of branches.
+
    ///
+
    /// # Errors
+
    ///
+
    /// Will return [`Error`] if the project doesn't exist or the surf
+
    /// interaction fails.
+
    pub fn branch_names(&self, filter: RefScope) -> Result<Vec<BranchName>, Error> {
+
        let mut branches = self
+
            .list_branches(filter)?
+
            .into_iter()
+
            .map(|b| b.name)
+
            .collect::<Vec<BranchName>>();
+

+
        branches.sort();
+

+
        Ok(branches)
+
    }
+

    /// List the names of the _tags_ that are contained in the underlying
    /// [`Repository`].
    ///
@@ -713,6 +731,23 @@ impl<'a> Browser<'a> {
        self.repository.list_tags(scope)
    }

+
    /// Returns a sorted list of [`TagName`] from the browser.
+
    ///
+
    /// # Errors
+
    ///
+
    /// * [`error::Error::Git`]
+
    pub fn tag_names(&self) -> Result<Vec<TagName>, Error> {
+
        let tag_names = self.list_tags(RefScope::Local)?;
+
        let mut tags: Vec<TagName> = tag_names
+
            .into_iter()
+
            .map(|tag_name| tag_name.name())
+
            .collect();
+

+
        tags.sort();
+

+
        Ok(tags)
+
    }
+

    /// List the namespaces within a `Browser`, filtering out ones that do not
    /// parse correctly.
    ///
modified radicle-surf/src/vcs/git/commit.rs
@@ -31,9 +31,12 @@ pub struct Author {
    /// Email of the author.
    pub email: String,
    /// Time the action was taken, e.g. time of commit.
-
    #[serde(
-
        serialize_with = "serialize_time",
-
        deserialize_with = "deserialize_time"
+
    #[cfg_attr(
+
        feature = "serialize",
+
        serde(
+
            serialize_with = "serialize_time",
+
            deserialize_with = "deserialize_time"
+
        )
    )]
    pub time: git2::Time,
}
@@ -91,7 +94,7 @@ impl<'repo> TryFrom<git2::Signature<'repo>> for Author {
pub struct Commit {
    // TODO: Replace git2::Oid with git_ext::Oid (https://github.com/radicle-dev/radicle-git/issues/5)
    /// Object ID of the Commit, i.e. the SHA1 digest.
-
    #[serde(deserialize_with = "deserialize_oid")]
+
    #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_oid"))]
    pub id: Oid,
    /// The author of the commit.
    pub author: Author,
@@ -102,7 +105,7 @@ pub struct Commit {
    /// The summary message of the commit.
    pub summary: String,
    /// The parents of this commit.
-
    #[serde(deserialize_with = "deserialize_vec_oid")]
+
    #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_vec_oid"))]
    pub parents: Vec<Oid>,
}

@@ -193,6 +196,7 @@ impl<'repo> TryFrom<git2::Commit<'repo>> for Commit {
    }
}

+
#[cfg(feature = "serialize")]
#[cfg(test)]
pub mod tests {
    use git2::Oid;