mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-11-16 00:10:35 +08:00
copy radicle-surf and radicle-source into this repo.
Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
@@ -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"
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
®ions[..],
|
||||
syntect::html::IncludeBackground::No,
|
||||
&mut html,
|
||||
);
|
||||
}
|
||||
Some(html)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user