copy radicle-surf and radicle-source into this repo.

Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
Han Xu
2022-09-16 22:17:49 -07:00
parent ce7dc3890a
commit 58d69ebf7f
49 changed files with 9783 additions and 1 deletions
+31
View File
@@ -0,0 +1,31 @@
[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"
+108
View File
@@ -0,0 +1,108 @@
// 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 })
}
+243
View File
@@ -0,0 +1,243 @@
// 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 })
}
+46
View File
@@ -0,0 +1,46 @@
// 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),
}
+51
View File
@@ -0,0 +1,51 @@
// 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};
+76
View File
@@ -0,0 +1,76 @@
// 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()
}
}
+218
View File
@@ -0,0 +1,218 @@
// 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),
}
}
}
+177
View File
@@ -0,0 +1,177 @@
// 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,
})
}
+44
View File
@@ -0,0 +1,44 @@
// 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
}
}
+28
View File
@@ -0,0 +1,28 @@
// 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,
}
+175
View File
@@ -0,0 +1,175 @@
// 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),
}
}
+87
View File
@@ -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,
}
}
+60
View File
@@ -0,0 +1,60 @@
// 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)
}