mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-24 22:00:38 +08:00
Merge remote-tracking branch 'origin/surf/source'
* origin/surf/source: radicle-surf: remove view radicle-surf: remove syntax highlighting radicle-surf: rename serialize to serde radicle-surf: refactor source objects radicle-surf: source module Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -20,7 +20,6 @@ test = false
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
serialize = ["serde"]
|
||||
# NOTE: testing `test_submodule_failure` on GH actions
|
||||
# is painful since it uses this specific repo and expects
|
||||
# certain branches to be setup. So we use this feature flag
|
||||
@@ -29,8 +28,8 @@ gh-actions = []
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.13"
|
||||
log = "0.4"
|
||||
nonempty = "0.5"
|
||||
serde = { features = ["serde_derive"], optional = true, version = "1" }
|
||||
thiserror = "1.0"
|
||||
|
||||
[dependencies.git2]
|
||||
@@ -52,6 +51,11 @@ features = ["serde"]
|
||||
version = "0.1.0"
|
||||
path = "../radicle-std-ext"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["serde_derive"]
|
||||
optional = true
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0"
|
||||
flate2 = "1"
|
||||
|
||||
@@ -41,7 +41,7 @@ fn main() {
|
||||
let repo = Repository::discover(&repo_path).unwrap();
|
||||
let now = Instant::now();
|
||||
let head = repo.head_oid().unwrap();
|
||||
let root = repo.root_dir(head).unwrap();
|
||||
let root = repo.root_dir(&head).unwrap();
|
||||
print_directory(&root, &repo, 0);
|
||||
|
||||
let elapsed_millis = now.elapsed().as_millis();
|
||||
|
||||
+18
-46
@@ -19,16 +19,12 @@
|
||||
|
||||
use std::{convert::TryFrom, path::PathBuf, slice};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{ser, Serialize, Serializer};
|
||||
|
||||
pub mod git;
|
||||
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Diff {
|
||||
pub created: Vec<CreateFile>,
|
||||
@@ -44,47 +40,35 @@ impl Default for Diff {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CreateFile {
|
||||
pub path: PathBuf,
|
||||
pub diff: FileDiff,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DeleteFile {
|
||||
pub path: PathBuf,
|
||||
pub diff: FileDiff,
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MoveFile {
|
||||
pub old_path: PathBuf,
|
||||
pub new_path: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CopyFile {
|
||||
pub old_path: PathBuf,
|
||||
pub new_path: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum EofNewLine {
|
||||
OldMissing,
|
||||
@@ -92,11 +76,7 @@ pub enum EofNewLine {
|
||||
BothMissing,
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ModifiedFile {
|
||||
pub path: PathBuf,
|
||||
@@ -106,25 +86,21 @@ pub struct ModifiedFile {
|
||||
|
||||
/// A set of changes belonging to one file.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
feature = "serde",
|
||||
derive(Serialize),
|
||||
serde(tag = "type", rename_all = "camelCase")
|
||||
)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FileDiff {
|
||||
Binary,
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
|
||||
Plain {
|
||||
hunks: Hunks,
|
||||
},
|
||||
}
|
||||
|
||||
/// Statistics describing a particular [`Diff`].
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Stats {
|
||||
/// Get the total number of files changed in a diff.
|
||||
@@ -136,11 +112,7 @@ pub struct Stats {
|
||||
}
|
||||
|
||||
/// A set of line changes.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Hunk {
|
||||
pub header: Line,
|
||||
@@ -148,7 +120,7 @@ pub struct Hunk {
|
||||
}
|
||||
|
||||
/// A set of [`Hunk`]s.
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Hunks(pub Vec<Hunk>);
|
||||
|
||||
@@ -215,7 +187,7 @@ impl From<String> for Line {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Line {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@@ -230,22 +202,22 @@ impl Serialize for Line {
|
||||
/// Single line delta. Two of these are need to represented a modified line: one
|
||||
/// addition and one deletion. Context is also represented with this type.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
feature = "serde",
|
||||
derive(Serialize),
|
||||
serde(tag = "type", rename_all = "camelCase")
|
||||
)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LineDiff {
|
||||
/// Line added.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
|
||||
Addition { line: Line, line_num: u32 },
|
||||
|
||||
/// Line deleted.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
|
||||
Deletion { line: Line, line_num: u32 },
|
||||
|
||||
/// Line context.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
|
||||
Context {
|
||||
line: Line,
|
||||
line_num_old: u32,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
//! See [`Directory`] for more information.
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::BTreeMap,
|
||||
convert::{Infallible, Into as _},
|
||||
path::{Path, PathBuf},
|
||||
@@ -186,6 +187,23 @@ pub enum Entry {
|
||||
Directory(Directory),
|
||||
}
|
||||
|
||||
impl PartialOrd for Entry {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Entry {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(Entry::File(x), Entry::File(y)) => x.name().cmp(y.name()),
|
||||
(Entry::File(_), Entry::Directory(_)) => Ordering::Less,
|
||||
(Entry::Directory(_), Entry::File(_)) => Ordering::Greater,
|
||||
(Entry::Directory(x), Entry::Directory(y)) => x.name().cmp(y.name()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Get a label for the `Entriess`, either the name of the [`File`]
|
||||
/// or the name of the [`Directory`].
|
||||
@@ -196,6 +214,20 @@ impl Entry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> PathBuf {
|
||||
match self {
|
||||
Entry::File(file) => file.path(),
|
||||
Entry::Directory(directory) => directory.path(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn location(&self) -> &Path {
|
||||
match self {
|
||||
Entry::File(file) => file.location(),
|
||||
Entry::Directory(directory) => directory.location(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_entry(
|
||||
entry: &git2::TreeEntry,
|
||||
path: PathBuf,
|
||||
@@ -329,7 +361,7 @@ impl Directory {
|
||||
&self,
|
||||
path: &P,
|
||||
repo: &Repository,
|
||||
) -> Result<Option<Entry>, crate::git::Error>
|
||||
) -> Result<Option<Entry>, error::Directory>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -351,12 +383,12 @@ impl Directory {
|
||||
&self,
|
||||
path: &P,
|
||||
repo: &Repository,
|
||||
) -> Result<Option<Oid>, crate::git::Error>
|
||||
) -> Result<Option<File>, error::Directory>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
Ok(match self.find_entry(path, repo)? {
|
||||
Some(Entry::File(f)) => Some(f.id),
|
||||
Some(Entry::File(file)) => Some(file),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
@@ -366,7 +398,7 @@ impl Directory {
|
||||
&self,
|
||||
path: P,
|
||||
repo: &Repository,
|
||||
) -> Result<Option<Self>, crate::git::Error>
|
||||
) -> Result<Option<Self>, error::Directory>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
|
||||
+29
-5
@@ -167,6 +167,14 @@ impl Revision for &str {
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for Branch {
|
||||
type Error = Error;
|
||||
|
||||
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
|
||||
(&self).object_id(repo)
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &Branch {
|
||||
type Error = Error;
|
||||
|
||||
@@ -176,6 +184,14 @@ impl Revision for &Branch {
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for Tag {
|
||||
type Error = Infallible;
|
||||
|
||||
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
|
||||
(&self).object_id(repo)
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &Tag {
|
||||
type Error = Infallible;
|
||||
|
||||
@@ -184,27 +200,35 @@ impl Revision for &Tag {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Revision> Revision for Box<R> {
|
||||
type Error = R::Error;
|
||||
|
||||
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
|
||||
self.as_ref().object_id(repo)
|
||||
}
|
||||
}
|
||||
|
||||
/// A common trait for anything that can convert to a `Commit`.
|
||||
pub trait ToCommit {
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
/// Converts to a commit in `repo`.
|
||||
fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error>;
|
||||
fn to_commit(&self, repo: &Repository) -> Result<Commit, Self::Error>;
|
||||
}
|
||||
|
||||
impl ToCommit for Commit {
|
||||
type Error = Infallible;
|
||||
|
||||
fn to_commit(self, _repo: &Repository) -> Result<Commit, Self::Error> {
|
||||
Ok(self)
|
||||
fn to_commit(&self, _repo: &Repository) -> Result<Commit, Self::Error> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Revision> ToCommit for R {
|
||||
type Error = Error;
|
||||
|
||||
fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error> {
|
||||
let oid = repo.object_id(&self)?;
|
||||
fn to_commit(&self, repo: &Repository) -> Result<Commit, Self::Error> {
|
||||
let oid = repo.object_id(self)?;
|
||||
let commit = repo.git2_repo().find_commit(oid.into())?;
|
||||
Ok(Commit::try_from(commit)?)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use std::{convert::TryFrom, str};
|
||||
use git_ext::Oid;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -34,7 +34,7 @@ pub enum Error {
|
||||
}
|
||||
|
||||
/// `Author` is the static information of a [`git2::Signature`].
|
||||
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Author {
|
||||
/// Name of the author.
|
||||
@@ -43,7 +43,7 @@ pub struct Author {
|
||||
pub email: String,
|
||||
/// Time the action was taken, e.g. time of commit.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
feature = "serde",
|
||||
serde(
|
||||
serialize_with = "serialize_time",
|
||||
deserialize_with = "deserialize_time"
|
||||
@@ -52,7 +52,7 @@ pub struct Author {
|
||||
pub time: git2::Time,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
fn deserialize_time<'de, D>(deserializer: D) -> Result<git2::Time, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -61,7 +61,7 @@ where
|
||||
Ok(git2::Time::new(seconds, 0))
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
fn serialize_time<S>(t: &git2::Time, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
@@ -100,7 +100,7 @@ impl<'repo> TryFrom<git2::Signature<'repo>> for Author {
|
||||
/// `Commit` is the static information of a [`git2::Commit`]. To get back the
|
||||
/// original `Commit` in the repository we can use the [`Oid`] to retrieve
|
||||
/// it.
|
||||
#[cfg_attr(feature = "serialize", derive(Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Commit {
|
||||
/// Object Id
|
||||
@@ -129,7 +129,7 @@ impl Commit {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Commit {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
||||
@@ -20,6 +20,8 @@ use std::marker::PhantomData;
|
||||
use git_ref_format::{
|
||||
refname,
|
||||
refspec::{self, PatternString, QualifiedPattern},
|
||||
Qualified,
|
||||
RefStr,
|
||||
RefString,
|
||||
};
|
||||
use thiserror::Error;
|
||||
@@ -39,6 +41,15 @@ pub struct Glob<T> {
|
||||
glob_type: PhantomData<T>, // To support different methods for different T.
|
||||
}
|
||||
|
||||
impl<T> Default for Glob<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
globs: Default::default(),
|
||||
glob_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Glob<T> {
|
||||
/// Return the [`QualifiedPattern`] globs of this `Glob`.
|
||||
pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
|
||||
@@ -303,6 +314,44 @@ impl From<Glob<Remote>> for Glob<Branch> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Glob<Qualified<'_>> {
|
||||
pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
|
||||
Self {
|
||||
globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
|
||||
glob_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Glob` for `refs/<category>`, starting with `glob`.
|
||||
pub fn categories<R>(category: R, glob: PatternString) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
let globs = vec![Self::qualify_category(category, glob)];
|
||||
Self {
|
||||
globs,
|
||||
glob_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a `refs/<category>` pattern to this `Glob`.
|
||||
pub fn insert<R>(mut self, category: R, glob: PatternString) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self.globs.push(Self::qualify_category(category, glob));
|
||||
self
|
||||
}
|
||||
|
||||
fn qualify_category<R>(category: R, glob: PatternString) -> QualifiedPattern<'static>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
let prefix = refname!("refs").and(category);
|
||||
qualify(&prefix, glob).expect("BUG: pattern is qualified")
|
||||
}
|
||||
}
|
||||
|
||||
fn qualify(prefix: &RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
|
||||
prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ enum FilterBy {
|
||||
|
||||
impl<'a> History<'a> {
|
||||
/// Creates a new history starting from `head`, in `repo`.
|
||||
pub fn new<C: ToCommit>(repo: &'a Repository, head: C) -> Result<Self, Error> {
|
||||
pub fn new<C: ToCommit>(repo: &'a Repository, head: &C) -> Result<Self, Error> {
|
||||
let head = head
|
||||
.to_commit(repo)
|
||||
.map_err(|err| Error::ToCommit(err.into()))?;
|
||||
|
||||
@@ -22,10 +22,7 @@ use std::{
|
||||
str,
|
||||
};
|
||||
|
||||
use git_ref_format::{
|
||||
refspec::{self, QualifiedPattern},
|
||||
Qualified,
|
||||
};
|
||||
use git_ref_format::{refspec::QualifiedPattern, Qualified, RefString};
|
||||
use radicle_git_ext::Oid;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -50,7 +47,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod iter;
|
||||
pub use iter::{Branches, Namespaces, Tags};
|
||||
pub use iter::{Branches, Categories, Namespaces, Tags};
|
||||
|
||||
use self::iter::{BranchNames, TagNames};
|
||||
|
||||
@@ -61,6 +58,8 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Branches(#[from] iter::error::Branch),
|
||||
#[error(transparent)]
|
||||
Categories(#[from] iter::error::Category),
|
||||
#[error(transparent)]
|
||||
Commit(#[from] commit::Error),
|
||||
/// An error that comes from performing a *diff* operations.
|
||||
#[error(transparent)]
|
||||
@@ -136,6 +135,16 @@ impl Repository {
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
pub fn categories(&self, pattern: &Glob<Qualified<'_>>) -> Result<Categories, Error> {
|
||||
let mut cats = Categories::default();
|
||||
for glob in pattern.globs() {
|
||||
let namespaced = self.namespaced_pattern(glob)?;
|
||||
let references = self.inner.references_glob(&namespaced)?;
|
||||
cats.push(references);
|
||||
}
|
||||
Ok(cats)
|
||||
}
|
||||
|
||||
/// Returns an iterator of namespaces that match `pattern`.
|
||||
pub fn namespaces(&self, pattern: &Glob<Namespace>) -> Result<Namespaces, Error> {
|
||||
let mut set = BTreeSet::new();
|
||||
@@ -189,7 +198,7 @@ impl Repository {
|
||||
///
|
||||
/// To visit inside any nested sub-directories, call `directory.get(&repo)`
|
||||
/// on the sub-directory.
|
||||
pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
|
||||
pub fn root_dir<C: ToCommit>(&self, commit: &C) -> Result<Directory, Error> {
|
||||
let commit = commit
|
||||
.to_commit(self)
|
||||
.map_err(|err| Error::ToCommit(err.into()))?;
|
||||
@@ -200,7 +209,7 @@ impl Repository {
|
||||
|
||||
/// Returns the last commit, if exists, for a `path` in the history of
|
||||
/// `rev`.
|
||||
pub fn last_commit<P, C>(&self, path: P, rev: C) -> Result<Option<Commit>, Error>
|
||||
pub fn last_commit<P, C>(&self, path: P, rev: &C) -> Result<Option<Commit>, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
C: ToCommit,
|
||||
@@ -215,7 +224,7 @@ impl Repository {
|
||||
}
|
||||
|
||||
/// Gets stats of `commit`.
|
||||
pub fn get_commit_stats<C: ToCommit>(&self, commit: C) -> Result<Stats, Error> {
|
||||
pub fn get_commit_stats<C: ToCommit>(&self, commit: &C) -> Result<Stats, Error> {
|
||||
let branches = self.branches(Glob::all_heads())?.count();
|
||||
let history = self.history(commit)?;
|
||||
let mut commits = 0;
|
||||
@@ -237,12 +246,6 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
/// Obtain the file content
|
||||
pub(crate) fn file_content(&self, object_id: Oid) -> Result<FileContent, Error> {
|
||||
let blob = self.inner.find_blob(object_id.into())?;
|
||||
Ok(FileContent::new(blob))
|
||||
}
|
||||
|
||||
/// Retrieves the file with `path` in this commit.
|
||||
pub fn get_commit_file<P, R>(&self, rev: &R, path: &P) -> Result<FileContent, crate::git::Error>
|
||||
where
|
||||
@@ -270,8 +273,8 @@ impl Repository {
|
||||
}
|
||||
|
||||
/// Lists tag names in the local RefScope.
|
||||
pub fn tag_names(&self) -> Result<TagNames, Error> {
|
||||
Ok(self.tags(&Glob::tags(refspec::pattern!("*")))?.names())
|
||||
pub fn tag_names(&self, filter: &Glob<Tag>) -> Result<TagNames, Error> {
|
||||
Ok(self.tags(filter)?.names())
|
||||
}
|
||||
|
||||
/// Returns the Oid of the current HEAD
|
||||
@@ -282,8 +285,18 @@ impl Repository {
|
||||
}
|
||||
|
||||
/// Switch to a `namespace`
|
||||
pub fn switch_namespace(&self, namespace: &str) -> Result<(), Error> {
|
||||
Ok(self.inner.set_namespace(namespace)?)
|
||||
pub fn switch_namespace(&self, namespace: &RefString) -> Result<(), Error> {
|
||||
Ok(self.inner.set_namespace(namespace.as_str())?)
|
||||
}
|
||||
|
||||
pub fn with_namespace<T, F>(&self, namespace: &RefString, f: F) -> Result<T, Error>
|
||||
where
|
||||
F: FnOnce() -> Result<T, Error>,
|
||||
{
|
||||
self.switch_namespace(namespace)?;
|
||||
let res = f();
|
||||
self.inner.remove_namespace()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns a full reference name with namespace(s) included.
|
||||
@@ -413,7 +426,7 @@ impl Repository {
|
||||
}
|
||||
|
||||
/// Returns the history with the `head` commit.
|
||||
pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
|
||||
pub fn history<C: ToCommit>(&self, head: &C) -> Result<History, Error> {
|
||||
History::new(self, head)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ use std::{
|
||||
convert::TryFrom as _,
|
||||
};
|
||||
|
||||
use git_ref_format::{lit, Qualified};
|
||||
use git_ref_format::{lit, Qualified, RefString};
|
||||
|
||||
use crate::git::{tag, Branch, Namespace, Tag};
|
||||
use crate::git::{refstr_join, tag, Branch, Namespace, Tag};
|
||||
|
||||
/// Iterator over [`Tag`]s.
|
||||
#[derive(Default)]
|
||||
@@ -165,7 +165,49 @@ impl Iterator for Namespaces {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Categories<'a> {
|
||||
references: Vec<git2::References<'a>>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<'a> Categories<'a> {
|
||||
pub(super) fn push(&mut self, references: git2::References<'a>) {
|
||||
self.references.push(references)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Categories<'a> {
|
||||
type Item = Result<(RefString, RefString), error::Category>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while self.current < self.references.len() {
|
||||
match self.references.get_mut(self.current) {
|
||||
Some(refs) => match refs.next() {
|
||||
Some(res) => {
|
||||
return Some(res.map_err(error::Category::from).and_then(|r| {
|
||||
let name = std::str::from_utf8(r.name_bytes())?;
|
||||
let name = git_ref_format::RefStr::try_from_str(name)?;
|
||||
let name = name.qualified().ok_or_else(|| {
|
||||
error::Category::NotQualified(name.to_ref_string())
|
||||
})?;
|
||||
let (_refs, category, c, cs) = name.non_empty_components();
|
||||
Ok((category.to_ref_string(), refstr_join(c, cs)))
|
||||
}));
|
||||
},
|
||||
None => self.current += 1,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub mod error {
|
||||
use std::str;
|
||||
|
||||
use git_ref_format::RefString;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::git::{branch, tag};
|
||||
@@ -178,6 +220,18 @@ pub mod error {
|
||||
Branch(#[from] branch::error::Branch),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Category {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("the reference '{0}' was expected to be qualified, i.e. 'refs/<category>/<path>'")]
|
||||
NotQualified(RefString),
|
||||
#[error(transparent)]
|
||||
RefFormat(#[from] git_ref_format::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] str::Utf8Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Tag {
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -17,15 +17,11 @@
|
||||
|
||||
pub use radicle_git_ext::Oid;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::Serialize;
|
||||
|
||||
/// Stats for a repository
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Serialize),
|
||||
serde(rename_all = "camelCase")
|
||||
)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
|
||||
pub struct Stats {
|
||||
/// Number of commits
|
||||
pub commits: usize,
|
||||
|
||||
+1
-17
@@ -82,20 +82,4 @@ extern crate radicle_git_ext as git_ext;
|
||||
pub mod diff;
|
||||
pub mod file_system;
|
||||
pub mod git;
|
||||
|
||||
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;
|
||||
pub mod source;
|
||||
|
||||
@@ -1,99 +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/>.
|
||||
|
||||
//! 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.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
pub mod blob;
|
||||
pub use blob::{Blob, BlobContent};
|
||||
|
||||
pub mod tree;
|
||||
pub use tree::{tree, Tree, TreeEntry};
|
||||
|
||||
use crate::{commit, file_system::directory, 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 {
|
||||
#[error(transparent)]
|
||||
Directory(#[from] directory::error::Directory),
|
||||
|
||||
/// An error occurred during a git operation.
|
||||
#[error(transparent)]
|
||||
Git(#[from] git::Error),
|
||||
|
||||
/// Trying to find a file path which could not be found.
|
||||
#[error("the path '{0}' was not found")]
|
||||
PathNotFound(PathBuf),
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Represents git object type 'blob', i.e. actual file contents.
|
||||
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
commit,
|
||||
git::Repository,
|
||||
object::{Error, 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: PathBuf,
|
||||
}
|
||||
|
||||
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>(repo: &Repository, maybe_revision: Option<Revision>, path: &P) -> Result<Blob, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
make_blob(repo, maybe_revision, path, content)
|
||||
}
|
||||
|
||||
fn make_blob<P, C>(
|
||||
repo: &Repository,
|
||||
maybe_revision: Option<Revision>,
|
||||
path: &P,
|
||||
content: C,
|
||||
) -> Result<Blob, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
C: FnOnce(&[u8]) -> BlobContent,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let revision = maybe_revision.unwrap();
|
||||
let root = repo.root_dir(&revision)?;
|
||||
|
||||
let file = root
|
||||
.find_file(&path, repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?;
|
||||
|
||||
let last_commit = repo
|
||||
.last_commit(path, &revision)?
|
||||
.map(|c| commit::Header::from(&c));
|
||||
// TODO: fuck this
|
||||
let name = path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_os_string()
|
||||
.into_string()
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
let file_content = repo.file_content(file)?;
|
||||
let content = content(file_content.as_bytes());
|
||||
|
||||
Ok(Blob {
|
||||
content,
|
||||
info: Info {
|
||||
name,
|
||||
object_type: ObjectType::Blob,
|
||||
last_commit,
|
||||
},
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a [`BlobContent`] given a byte slice.
|
||||
fn content(content: &[u8]) -> BlobContent {
|
||||
match str::from_utf8(content) {
|
||||
Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
|
||||
Err(_) => BlobContent::Binary(content.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Represents git object type 'tree', i.e. like directory entries in Unix.
|
||||
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use git_ref_format::refname;
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
commit,
|
||||
file_system::directory,
|
||||
git::Repository,
|
||||
object::{Error, 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: PathBuf,
|
||||
/// 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: PathBuf,
|
||||
}
|
||||
|
||||
#[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>(
|
||||
repo: &Repository,
|
||||
maybe_revision: Option<Revision>,
|
||||
maybe_prefix: Option<&P>,
|
||||
) -> Result<Tree, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let maybe_prefix = maybe_prefix.map(|p| p.as_ref());
|
||||
let rev = match maybe_revision {
|
||||
Some(r) => r,
|
||||
None => Revision::Branch {
|
||||
name: refname!("main"),
|
||||
remote: None,
|
||||
},
|
||||
};
|
||||
|
||||
let prefix_dir = match maybe_prefix {
|
||||
None => repo.root_dir(&rev)?,
|
||||
Some(path) => repo
|
||||
.root_dir(&rev)?
|
||||
.find_directory(path, repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
|
||||
};
|
||||
|
||||
let mut entries = prefix_dir
|
||||
.entries(repo)?
|
||||
.entries()
|
||||
.fold(Vec::new(), |mut entries, entry| {
|
||||
let entry_path = match maybe_prefix {
|
||||
Some(path) => {
|
||||
let mut path = path.to_path_buf();
|
||||
path.push(entry.name());
|
||||
path
|
||||
},
|
||||
None => PathBuf::new(),
|
||||
};
|
||||
|
||||
let info = Info {
|
||||
name: entry.name().clone(),
|
||||
object_type: match entry {
|
||||
directory::Entry::Directory(_) => ObjectType::Tree,
|
||||
directory::Entry::File { .. } => ObjectType::Blob,
|
||||
},
|
||||
last_commit: None,
|
||||
};
|
||||
|
||||
entries.push(TreeEntry {
|
||||
info,
|
||||
path: entry_path,
|
||||
});
|
||||
entries
|
||||
});
|
||||
|
||||
// We want to ensure that in the response Tree entries come first. `Ord` being
|
||||
// derived on the enum ensures Variant declaration order.
|
||||
//
|
||||
// https://doc.rust-lang.org/std/cmp/trait.Ord.html#derivable
|
||||
entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
|
||||
|
||||
let last_commit = if maybe_prefix.is_none() {
|
||||
let history = repo.history(&rev)?;
|
||||
Some(commit::Header::from(history.head()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let name = match maybe_prefix {
|
||||
None => "".into(),
|
||||
Some(path) => path.file_name().unwrap().to_str().unwrap().to_string(),
|
||||
};
|
||||
let info = Info {
|
||||
name,
|
||||
object_type: ObjectType::Tree,
|
||||
last_commit,
|
||||
};
|
||||
|
||||
Ok(Tree {
|
||||
path: maybe_prefix.map_or(PathBuf::new(), |path| path.to_path_buf()),
|
||||
entries,
|
||||
info,
|
||||
})
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Represents revisions
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use git_ref_format::{lit, refspec, Qualified, RefString};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
|
||||
use crate::git::{self, Error, Glob, Repository};
|
||||
|
||||
/// Types of a peer.
|
||||
pub enum Category<P, U> {
|
||||
/// Local peer.
|
||||
Local {
|
||||
/// Peer Id
|
||||
peer_id: P,
|
||||
/// User name
|
||||
user: U,
|
||||
},
|
||||
/// Remote peer.
|
||||
Remote {
|
||||
/// Peer Id
|
||||
peer_id: P,
|
||||
/// User name
|
||||
user: U,
|
||||
},
|
||||
}
|
||||
|
||||
/// A revision selector for a `Browser`.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(Deserialize, Serialize),
|
||||
serde(rename_all = "camelCase", tag = "type")
|
||||
)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Revision {
|
||||
/// Select a tag under the name provided.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
Tag {
|
||||
/// Name of the tag.
|
||||
name: RefString,
|
||||
},
|
||||
/// Select a branch under the name provided.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
Branch {
|
||||
/// Name of the branch.
|
||||
name: RefString,
|
||||
/// The remote peer, if specified.
|
||||
remote: Option<RefString>,
|
||||
},
|
||||
/// Select a SHA1 under the name provided.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
Sha {
|
||||
/// The SHA1 value.
|
||||
sha: Oid,
|
||||
},
|
||||
}
|
||||
|
||||
impl git::Revision for &Revision {
|
||||
type Error = git2::Error;
|
||||
|
||||
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
|
||||
match self {
|
||||
Revision::Tag { name } => match name.qualified() {
|
||||
None => Qualified::from(lit::refs_tags(name)).object_id(repo),
|
||||
Some(name) => name.object_id(repo),
|
||||
},
|
||||
Revision::Branch { name, remote } => match remote {
|
||||
Some(remote) => {
|
||||
Qualified::from(lit::refs_remotes(remote.join(name))).object_id(repo)
|
||||
},
|
||||
None => git::Branch::local(name).refname().object_id(repo),
|
||||
},
|
||||
Revision::Sha { sha } => Ok(*sha),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundled response to retrieve both branches and tags for
|
||||
/// a user's repo.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Revisions<P, U> {
|
||||
/// The peer peer_id for the user.
|
||||
pub peer_id: P,
|
||||
/// The user who owns these revisions.
|
||||
pub user: U,
|
||||
/// List of branch reference names.
|
||||
pub branches: BTreeSet<RefString>,
|
||||
/// List of tag reference names.
|
||||
pub tags: BTreeSet<RefString>,
|
||||
}
|
||||
|
||||
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
|
||||
/// remote branches.
|
||||
///
|
||||
/// If there are no branches then this returns `None`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * If we cannot get the branches from the `Browser`
|
||||
pub fn remote<U>(
|
||||
repo: &Repository,
|
||||
peer_id: RefString,
|
||||
user: U,
|
||||
) -> Result<Revisions<RefString, U>, Error> {
|
||||
let pattern = peer_id.to_pattern(refspec::pattern!("*"));
|
||||
let branches = repo
|
||||
.branch_names(Glob::remotes(pattern))?
|
||||
.map(|name| name.map(RefString::from))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Revisions {
|
||||
peer_id,
|
||||
user,
|
||||
branches,
|
||||
// TODO(rudolfs): implement remote peer tags once we decide how
|
||||
// https://radicle.community/t/git-tags/214
|
||||
tags: BTreeSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
|
||||
/// local branches.
|
||||
///
|
||||
/// If there are no branches then this returns `None`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * If we cannot get the branches from the `Browser`
|
||||
pub fn local<U>(
|
||||
repo: &Repository,
|
||||
peer_id: RefString,
|
||||
user: U,
|
||||
) -> Result<Revisions<RefString, U>, Error> {
|
||||
let branches = repo
|
||||
.branch_names(Glob::all_heads())?
|
||||
.map(|name| name.map(RefString::from))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let tags = repo
|
||||
.tag_names()?
|
||||
.map(|name| name.map(RefString::from))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Revisions {
|
||||
peer_id,
|
||||
user,
|
||||
branches,
|
||||
tags,
|
||||
})
|
||||
}
|
||||
|
||||
/// Provide the [`Revisions`] of a peer.
|
||||
///
|
||||
/// If the peer is [`Category::Local`], meaning that is the current person doing
|
||||
/// the browsing and no remote is set for the reference.
|
||||
///
|
||||
/// Othewise, the peer is [`Category::Remote`], meaning that we are looking into
|
||||
/// a remote part of a reference.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * If we cannot get the branches from the `Browser`
|
||||
pub fn revisions<U>(
|
||||
repo: &Repository,
|
||||
peer: Category<RefString, U>,
|
||||
) -> Result<Revisions<RefString, U>, Error> {
|
||||
match peer {
|
||||
Category::Local { peer_id, user } => local(repo, peer_id, user),
|
||||
Category::Remote { peer_id, user } => remote(repo, peer_id, user),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! The `source` module provides a layer on top of the [`crate::git`]
|
||||
//! functionality.
|
||||
//!
|
||||
//! It provides data types of [`Blob`]s, [`Tree`]s, and [`Commit`]s
|
||||
//! (see [git objects][git-objects]). These types are analgous to
|
||||
//! [`crate::file_system::File`], [`crate::file_system::Directory`], and
|
||||
//! [`crate::git::Commit`]. However, they provide extra metadata and
|
||||
//! can be serialized to serve to other applications. For example,
|
||||
//! they could be used in an HTTP server for viewing a Git repository.
|
||||
//!
|
||||
//! [git-objects]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||
|
||||
pub mod object;
|
||||
pub use object::{blob, tree, Blob, BlobContent, Tree, TreeEntry};
|
||||
|
||||
pub mod commit;
|
||||
pub use commit::{commit, commits, Commit};
|
||||
|
||||
mod person;
|
||||
pub use person::Person;
|
||||
@@ -20,7 +20,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use git_ref_format::RefString;
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
@@ -29,14 +29,13 @@ use serde::{
|
||||
use crate::{
|
||||
diff,
|
||||
git::{self, glob, Glob, Repository},
|
||||
person::Person,
|
||||
revision::Revision,
|
||||
source::person::Person,
|
||||
};
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
|
||||
/// Commit statistics.
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
#[derive(Clone)]
|
||||
pub struct Stats {
|
||||
/// Additions.
|
||||
@@ -46,7 +45,7 @@ pub struct Stats {
|
||||
}
|
||||
|
||||
/// Representation of a changeset between two revs.
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
#[derive(Clone)]
|
||||
pub struct Commit {
|
||||
/// The commit header.
|
||||
@@ -115,7 +114,7 @@ impl From<git::Commit> for Header {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Header {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@@ -133,7 +132,7 @@ impl Serialize for Header {
|
||||
}
|
||||
|
||||
/// A selection of commit headers and their statistics.
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize))]
|
||||
pub struct Commits {
|
||||
/// The commit headers
|
||||
pub headers: Vec<Header>,
|
||||
@@ -228,18 +227,13 @@ pub fn header(repo: &Repository, sha1: Oid) -> Result<Header, Error> {
|
||||
///
|
||||
/// Will return [`Error`] if the project doesn't exist or the surf interaction
|
||||
/// fails.
|
||||
pub fn commits(repo: &Repository, maybe_revision: Option<Revision>) -> Result<Commits, Error> {
|
||||
let rev = match maybe_revision {
|
||||
Some(revision) => revision,
|
||||
None => Revision::Sha {
|
||||
sha: repo.head_oid()?,
|
||||
},
|
||||
};
|
||||
|
||||
let stats = repo.get_commit_stats(&rev)?;
|
||||
let commits: Result<Vec<git::Commit>, git::Error> = repo.history(&rev)?.collect();
|
||||
let headers = commits?.iter().map(Header::from).collect();
|
||||
|
||||
pub fn commits<R>(repo: &Repository, revision: &R) -> Result<Commits, Error>
|
||||
where
|
||||
R: git::Revision,
|
||||
{
|
||||
let stats = repo.get_commit_stats(revision)?;
|
||||
let commits = repo.history(revision)?.collect::<Result<Vec<_>, _>>()?;
|
||||
let headers = commits.into_iter().map(Header::from).collect();
|
||||
Ok(Commits { headers, stats })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod blob;
|
||||
pub use blob::{Blob, BlobContent};
|
||||
|
||||
pub mod tree;
|
||||
pub use tree::{Tree, TreeEntry};
|
||||
|
||||
use crate::{file_system::directory, git};
|
||||
|
||||
/// An error reported by object types.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Directory(#[from] directory::error::Directory),
|
||||
|
||||
#[error(transparent)]
|
||||
File(#[from] directory::error::File),
|
||||
|
||||
/// An error occurred during a git operation.
|
||||
#[error(transparent)]
|
||||
Git(#[from] git::Error),
|
||||
|
||||
/// Trying to find a file path which could not be found.
|
||||
#[error("the path '{0}' was not found")]
|
||||
PathNotFound(PathBuf),
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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::{path::Path, str};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
file_system::{File, FileContent},
|
||||
git::{self, Repository},
|
||||
source::{commit, object::Error},
|
||||
};
|
||||
|
||||
/// File data abstraction.
|
||||
pub struct Blob {
|
||||
pub file: File,
|
||||
pub content: BlobContent,
|
||||
pub commit: Option<commit::Header>,
|
||||
}
|
||||
|
||||
impl Blob {
|
||||
/// Returns the [`Blob`] for a file at `revision` under `path`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return [`Error`] if the project doesn't exist or a surf interaction
|
||||
/// fails.
|
||||
pub fn new<P, R>(repo: &Repository, revision: &R, path: &P) -> Result<Blob, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
R: git::Revision,
|
||||
{
|
||||
Self::make_blob(repo, revision, path, |c| BlobContent::from(c))
|
||||
}
|
||||
|
||||
fn make_blob<P, R>(
|
||||
repo: &Repository,
|
||||
revision: &R,
|
||||
path: &P,
|
||||
content: impl FnOnce(FileContent) -> BlobContent,
|
||||
) -> Result<Blob, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
R: git::Revision,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let root = repo.root_dir(revision)?;
|
||||
|
||||
let file = root
|
||||
.find_file(&path, repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?;
|
||||
|
||||
let last_commit = repo
|
||||
.last_commit(path, revision)?
|
||||
.map(|c| commit::Header::from(&c));
|
||||
|
||||
let content = content(file.content(repo)?);
|
||||
|
||||
Ok(Blob {
|
||||
file,
|
||||
content,
|
||||
commit: last_commit,
|
||||
})
|
||||
}
|
||||
|
||||
/// Indicates if the content of the [`Blob`] is binary.
|
||||
#[must_use]
|
||||
pub fn is_binary(&self) -> bool {
|
||||
matches!(self.content, BlobContent::Binary(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Blob {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
const FIELDS: usize = 5;
|
||||
let mut state = serializer.serialize_struct("Blob", FIELDS)?;
|
||||
state.serialize_field("binary", &self.is_binary())?;
|
||||
state.serialize_field("content", &self.content)?;
|
||||
state.serialize_field("lastCommit", &self.commit)?;
|
||||
state.serialize_field("name", &self.file.name())?;
|
||||
state.serialize_field("path", &self.file.location())?;
|
||||
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 binary and needs special treatment.
|
||||
Binary(Vec<u8>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for BlobContent {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Plain(content) => serializer.serialize_str(content),
|
||||
Self::Binary(bytes) => {
|
||||
let encoded = base64::encode(bytes);
|
||||
serializer.serialize_str(&encoded)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileContent<'_>> for BlobContent {
|
||||
fn from(content: FileContent) -> Self {
|
||||
let content = content.as_bytes();
|
||||
match str::from_utf8(content) {
|
||||
Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
|
||||
Err(_) => BlobContent::Binary(content.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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::path::Path;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
file_system::{directory, Directory},
|
||||
git::{self, Repository},
|
||||
source::{commit, object::Error},
|
||||
};
|
||||
|
||||
/// Result of a directory listing, carries other trees and blobs.
|
||||
pub struct Tree {
|
||||
pub directory: Directory,
|
||||
pub commit: Option<commit::Header>,
|
||||
/// Entries listed in that tree result.
|
||||
pub entries: Vec<TreeEntry>,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
/// Retrieve the [`Tree`] for the given `revision` and directory `prefix`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return [`Error`] if any of the surf interactions fail.
|
||||
pub fn new<P, R>(repo: &Repository, revision: &R, prefix: Option<&P>) -> Result<Tree, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
R: git::Revision,
|
||||
{
|
||||
let prefix = prefix.map(|p| p.as_ref());
|
||||
|
||||
let prefix_dir = match prefix {
|
||||
None => repo.root_dir(revision)?,
|
||||
Some(path) => repo
|
||||
.root_dir(revision)?
|
||||
.find_directory(path, repo)?
|
||||
.ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
|
||||
};
|
||||
|
||||
let mut entries = prefix_dir
|
||||
.entries(repo)?
|
||||
.entries()
|
||||
.cloned()
|
||||
.map(TreeEntry::from)
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort();
|
||||
|
||||
let last_commit = if prefix.is_none() {
|
||||
let history = repo.history(revision)?;
|
||||
Some(commit::Header::from(history.head()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Tree {
|
||||
entries,
|
||||
directory: prefix_dir,
|
||||
commit: last_commit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Tree {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
const FIELDS: usize = 4;
|
||||
let mut state = serializer.serialize_struct("Tree", FIELDS)?;
|
||||
state.serialize_field("entries", &self.entries)?;
|
||||
state.serialize_field("lastCommit", &self.commit)?;
|
||||
state.serialize_field("name", &self.directory.name())?;
|
||||
state.serialize_field("path", &self.directory.location())?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry in a Tree result.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct TreeEntry {
|
||||
pub entry: directory::Entry,
|
||||
}
|
||||
|
||||
impl From<directory::Entry> for TreeEntry {
|
||||
fn from(entry: directory::Entry) -> Self {
|
||||
Self { entry }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TreeEntry> for directory::Entry {
|
||||
fn from(TreeEntry { entry }: TreeEntry) -> Self {
|
||||
entry
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for TreeEntry {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
const FIELDS: usize = 4;
|
||||
let mut state = serializer.serialize_struct("TreeEntry", FIELDS)?;
|
||||
state.serialize_field("path", &self.entry.location())?;
|
||||
state.serialize_field("name", &self.entry.name())?;
|
||||
state.serialize_field("lastCommit", &None::<commit::Header>)?;
|
||||
state.serialize_field(
|
||||
"kind",
|
||||
match self.entry {
|
||||
directory::Entry::File(_) => "blob",
|
||||
directory::Entry::Directory(_) => "directory",
|
||||
},
|
||||
)?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,9 @@
|
||||
|
||||
//! 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))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Person {
|
||||
/// Name part of the commit signature.
|
||||
@@ -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(
|
||||
®ions[..],
|
||||
syntect::html::IncludeBackground::No,
|
||||
&mut html,
|
||||
);
|
||||
}
|
||||
Some(html)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ publish = false
|
||||
test = true
|
||||
|
||||
[features]
|
||||
serialize = []
|
||||
test = []
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -37,7 +36,7 @@ path = "../../radicle-git-ext"
|
||||
|
||||
[dev-dependencies.radicle-surf]
|
||||
path = ".."
|
||||
features = ["serialize"]
|
||||
features = ["serde"]
|
||||
|
||||
[dev-dependencies.test-helpers]
|
||||
path = "../../test/test-helpers"
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
|
||||
const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(test)]
|
||||
mod branch;
|
||||
#[cfg(test)]
|
||||
mod code_browsing;
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(test)]
|
||||
mod commit;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,7 +5,6 @@ use radicle_git_ext::Oid;
|
||||
use radicle_surf::git::{Author, Commit};
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_test_commits(commit in commits_strategy()) {
|
||||
|
||||
@@ -122,7 +122,6 @@ fn test_branch_diff() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[test]
|
||||
fn test_diff_serde() {
|
||||
use radicle_surf::diff::{Hunks, MoveFile};
|
||||
|
||||
@@ -18,7 +18,7 @@ fn readme_missing_and_memory() {
|
||||
|
||||
// memory.rs is commited later so it should not exist here.
|
||||
let memory_last_commit_oid = repo
|
||||
.last_commit(Path::new("src/memory.rs"), oid)
|
||||
.last_commit(Path::new("src/memory.rs"), &oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -26,7 +26,7 @@ fn readme_missing_and_memory() {
|
||||
|
||||
// README.md exists in this commit.
|
||||
let readme_last_commit = repo
|
||||
.last_commit(Path::new("README.md"), oid)
|
||||
.last_commit(Path::new("README.md"), &oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -44,7 +44,7 @@ fn folder_svelte() {
|
||||
let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();
|
||||
|
||||
let folder_svelte = repo
|
||||
.last_commit(Path::new("examples/Folder.svelte"), oid)
|
||||
.last_commit(Path::new("examples/Folder.svelte"), &oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -64,7 +64,7 @@ fn nest_directory() {
|
||||
let nested_directory_tree_commit_id = repo
|
||||
.last_commit(
|
||||
Path::new("this/is/a/really/deeply/nested/directory/tree"),
|
||||
oid,
|
||||
&oid,
|
||||
)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
@@ -85,13 +85,13 @@ fn can_get_last_commit_for_special_filenames() {
|
||||
let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();
|
||||
|
||||
let backslash_commit_id = repo
|
||||
.last_commit(Path::new(r"special/faux\\path"), oid)
|
||||
.last_commit(Path::new(r"special/faux\\path"), &oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
assert_eq!(backslash_commit_id, Some(expected_commit_id));
|
||||
|
||||
let ogre_commit_id = repo
|
||||
.last_commit(Path::new("special/👹👹👹"), oid)
|
||||
.last_commit(Path::new("special/👹👹👹"), &oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
assert_eq!(ogre_commit_id, Some(expected_commit_id));
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::GIT_PLATINUM;
|
||||
fn switch_to_banana() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let history_master = repo.history(&Branch::local(refname!("master")))?;
|
||||
repo.switch_namespace("golden")?;
|
||||
repo.switch_namespace(&refname!("golden"))?;
|
||||
let history_banana = repo.history(&Branch::local(refname!("banana")))?;
|
||||
|
||||
assert_ne!(history_master.head(), history_banana.head());
|
||||
@@ -23,7 +23,7 @@ fn me_namespace() -> Result<(), Error> {
|
||||
|
||||
assert_eq!(repo.which_namespace().unwrap(), None);
|
||||
|
||||
repo.switch_namespace("me")?;
|
||||
repo.switch_namespace(&refname!("me"))?;
|
||||
assert_eq!(repo.which_namespace().unwrap(), Some("me".parse()?));
|
||||
|
||||
let history_feature = repo.history(&Branch::local(refname!("feature/#1194")))?;
|
||||
@@ -58,7 +58,7 @@ fn golden_namespace() -> Result<(), Error> {
|
||||
|
||||
assert_eq!(repo.which_namespace().unwrap(), None);
|
||||
|
||||
repo.switch_namespace("golden")?;
|
||||
repo.switch_namespace(&refname!("golden"))?;
|
||||
|
||||
assert_eq!(repo.which_namespace().unwrap(), Some("golden".parse()?));
|
||||
|
||||
@@ -101,7 +101,7 @@ fn silver_namespace() -> Result<(), Error> {
|
||||
|
||||
assert_eq!(repo.which_namespace().unwrap(), None);
|
||||
|
||||
repo.switch_namespace("golden/silver")?;
|
||||
repo.switch_namespace(&refname!("golden/silver"))?;
|
||||
assert_eq!(
|
||||
repo.which_namespace().unwrap(),
|
||||
Some("golden/silver".parse()?)
|
||||
|
||||
@@ -44,7 +44,7 @@ fn _master() -> Result<(), Error> {
|
||||
fn commit() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
let mut history = repo.history(rev)?;
|
||||
let mut history = repo.history(&rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(history.any(|commit| commit.unwrap().id == commit1));
|
||||
@@ -56,7 +56,7 @@ fn commit() -> Result<(), Error> {
|
||||
fn commit_parents() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
let history = repo.history(rev)?;
|
||||
let history = repo.history(&rev)?;
|
||||
let commit = history.head();
|
||||
|
||||
assert_eq!(
|
||||
@@ -71,7 +71,7 @@ fn commit_parents() -> Result<(), Error> {
|
||||
fn commit_short() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let rev = repo.oid("3873745c8")?;
|
||||
let mut history = repo.history(rev)?;
|
||||
let mut history = repo.history(&rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(history.any(|commit| commit.unwrap().id == commit1));
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ features = ["test"]
|
||||
|
||||
[dev-dependencies.radicle-surf-test]
|
||||
path = "../radicle-surf/t"
|
||||
features = ["serialize", "test"]
|
||||
features = ["test"]
|
||||
|
||||
[dev-dependencies.git-storage-test]
|
||||
path = "../git-storage/t"
|
||||
|
||||
Reference in New Issue
Block a user