diff --git a/radicle-surf/Cargo.toml b/radicle-surf/Cargo.toml index fc8eece..f240586 100644 --- a/radicle-surf/Cargo.toml +++ b/radicle-surf/Cargo.toml @@ -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" diff --git a/radicle-surf/examples/browsing.rs b/radicle-surf/examples/browsing.rs index 91c9c19..a1128db 100644 --- a/radicle-surf/examples/browsing.rs +++ b/radicle-surf/examples/browsing.rs @@ -41,7 +41,7 @@ fn main() { let repo = Repository::discover(&repo_path).unwrap(); let now = Instant::now(); let head = repo.head_oid().unwrap(); - let root = repo.root_dir(head).unwrap(); + let root = repo.root_dir(&head).unwrap(); print_directory(&root, &repo, 0); let elapsed_millis = now.elapsed().as_millis(); diff --git a/radicle-surf/src/diff.rs b/radicle-surf/src/diff.rs index 855894c..8742734 100644 --- a/radicle-surf/src/diff.rs +++ b/radicle-surf/src/diff.rs @@ -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, @@ -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); @@ -215,7 +187,7 @@ impl From for Line { } } -#[cfg(feature = "serialize")] +#[cfg(feature = "serde")] impl Serialize for Line { fn serialize(&self, serializer: S) -> Result 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, diff --git a/radicle-surf/src/file_system/directory.rs b/radicle-surf/src/file_system/directory.rs index 21b3d1b..fea75eb 100644 --- a/radicle-surf/src/file_system/directory.rs +++ b/radicle-surf/src/file_system/directory.rs @@ -21,6 +21,7 @@ //! See [`Directory`] for more information. use std::{ + cmp::Ordering, collections::BTreeMap, convert::{Infallible, Into as _}, path::{Path, PathBuf}, @@ -186,6 +187,23 @@ pub enum Entry { Directory(Directory), } +impl PartialOrd for Entry { + fn partial_cmp(&self, other: &Self) -> Option { + 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, crate::git::Error> + ) -> Result, error::Directory> where P: AsRef, { @@ -351,12 +383,12 @@ impl Directory { &self, path: &P, repo: &Repository, - ) -> Result, crate::git::Error> + ) -> Result, error::Directory> where P: AsRef, { 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, crate::git::Error> + ) -> Result, error::Directory> where P: AsRef, { diff --git a/radicle-surf/src/git.rs b/radicle-surf/src/git.rs index 3da31f5..0a8296c 100644 --- a/radicle-surf/src/git.rs +++ b/radicle-surf/src/git.rs @@ -167,6 +167,14 @@ impl Revision for &str { } } +impl Revision for Branch { + type Error = Error; + + fn object_id(&self, repo: &Repository) -> Result { + (&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 { + (&self).object_id(repo) + } +} + impl Revision for &Tag { type Error = Infallible; @@ -184,27 +200,35 @@ impl Revision for &Tag { } } +impl Revision for Box { + type Error = R::Error; + + fn object_id(&self, repo: &Repository) -> Result { + 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; + fn to_commit(&self, repo: &Repository) -> Result; } impl ToCommit for Commit { type Error = Infallible; - fn to_commit(self, _repo: &Repository) -> Result { - Ok(self) + fn to_commit(&self, _repo: &Repository) -> Result { + Ok(self.clone()) } } impl ToCommit for R { type Error = Error; - fn to_commit(self, repo: &Repository) -> Result { - let oid = repo.object_id(&self)?; + fn to_commit(&self, repo: &Repository) -> Result { + let oid = repo.object_id(self)?; let commit = repo.git2_repo().find_commit(oid.into())?; Ok(Commit::try_from(commit)?) } diff --git a/radicle-surf/src/git/commit.rs b/radicle-surf/src/git/commit.rs index 97ed8e7..272da9f 100644 --- a/radicle-surf/src/git/commit.rs +++ b/radicle-surf/src/git/commit.rs @@ -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 where D: Deserializer<'de>, @@ -61,7 +61,7 @@ where Ok(git2::Time::new(seconds, 0)) } -#[cfg(feature = "serialize")] +#[cfg(feature = "serde")] fn serialize_time(t: &git2::Time, serializer: S) -> Result where S: Serializer, @@ -100,7 +100,7 @@ impl<'repo> TryFrom> 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(&self, serializer: S) -> Result where diff --git a/radicle-surf/src/git/glob.rs b/radicle-surf/src/git/glob.rs index 22d87db..9536cc8 100644 --- a/radicle-surf/src/git/glob.rs +++ b/radicle-surf/src/git/glob.rs @@ -20,6 +20,8 @@ use std::marker::PhantomData; use git_ref_format::{ refname, refspec::{self, PatternString, QualifiedPattern}, + Qualified, + RefStr, RefString, }; use thiserror::Error; @@ -39,6 +41,15 @@ pub struct Glob { glob_type: PhantomData, // To support different methods for different T. } +impl Default for Glob { + fn default() -> Self { + Self { + globs: Default::default(), + glob_type: PhantomData, + } + } +} + impl Glob { /// Return the [`QualifiedPattern`] globs of this `Glob`. pub fn globs(&self) -> impl Iterator> { @@ -303,6 +314,44 @@ impl From> for Glob { } } +impl Glob> { + pub fn all_category>(category: R) -> Self { + Self { + globs: vec![Self::qualify_category(category, refspec::pattern!("*"))], + glob_type: PhantomData, + } + } + + /// Creates a `Glob` for `refs/`, starting with `glob`. + pub fn categories(category: R, glob: PatternString) -> Self + where + R: AsRef, + { + let globs = vec![Self::qualify_category(category, glob)]; + Self { + globs, + glob_type: PhantomData, + } + } + + /// Adds a `refs/` pattern to this `Glob`. + pub fn insert(mut self, category: R, glob: PatternString) -> Self + where + R: AsRef, + { + self.globs.push(Self::qualify_category(category, glob)); + self + } + + fn qualify_category(category: R, glob: PatternString) -> QualifiedPattern<'static> + where + R: AsRef, + { + let prefix = refname!("refs").and(category); + qualify(&prefix, glob).expect("BUG: pattern is qualified") + } +} + fn qualify(prefix: &RefString, glob: PatternString) -> Option> { prefix.to_pattern(glob).qualified().map(|q| q.into_owned()) } diff --git a/radicle-surf/src/git/history.rs b/radicle-surf/src/git/history.rs index 45d25dc..26e6d33 100644 --- a/radicle-surf/src/git/history.rs +++ b/radicle-surf/src/git/history.rs @@ -38,7 +38,7 @@ enum FilterBy { impl<'a> History<'a> { /// Creates a new history starting from `head`, in `repo`. - pub fn new(repo: &'a Repository, head: C) -> Result { + pub fn new(repo: &'a Repository, head: &C) -> Result { let head = head .to_commit(repo) .map_err(|err| Error::ToCommit(err.into()))?; diff --git a/radicle-surf/src/git/repo.rs b/radicle-surf/src/git/repo.rs index a1770ce..04bb7b6 100644 --- a/radicle-surf/src/git/repo.rs +++ b/radicle-surf/src/git/repo.rs @@ -22,10 +22,7 @@ use std::{ str, }; -use git_ref_format::{ - refspec::{self, QualifiedPattern}, - Qualified, -}; +use git_ref_format::{refspec::QualifiedPattern, Qualified, RefString}; use radicle_git_ext::Oid; use thiserror::Error; @@ -50,7 +47,7 @@ use crate::{ }; pub mod iter; -pub use iter::{Branches, Namespaces, Tags}; +pub use iter::{Branches, Categories, Namespaces, Tags}; use self::iter::{BranchNames, TagNames}; @@ -61,6 +58,8 @@ pub enum Error { #[error(transparent)] Branches(#[from] iter::error::Branch), #[error(transparent)] + Categories(#[from] iter::error::Category), + #[error(transparent)] Commit(#[from] commit::Error), /// An error that comes from performing a *diff* operations. #[error(transparent)] @@ -136,6 +135,16 @@ impl Repository { Ok(tags) } + pub fn categories(&self, pattern: &Glob>) -> Result { + 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) -> Result { 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(&self, commit: C) -> Result { + pub fn root_dir(&self, commit: &C) -> Result { 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(&self, path: P, rev: C) -> Result, Error> + pub fn last_commit(&self, path: P, rev: &C) -> Result, Error> where P: AsRef, C: ToCommit, @@ -215,7 +224,7 @@ impl Repository { } /// Gets stats of `commit`. - pub fn get_commit_stats(&self, commit: C) -> Result { + pub fn get_commit_stats(&self, commit: &C) -> Result { 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 { - 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(&self, rev: &R, path: &P) -> Result where @@ -270,8 +273,8 @@ impl Repository { } /// Lists tag names in the local RefScope. - pub fn tag_names(&self) -> Result { - Ok(self.tags(&Glob::tags(refspec::pattern!("*")))?.names()) + pub fn tag_names(&self, filter: &Glob) -> Result { + 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(&self, namespace: &RefString, f: F) -> Result + where + F: FnOnce() -> Result, + { + 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(&self, head: C) -> Result { + pub fn history(&self, head: &C) -> Result { History::new(self, head) } diff --git a/radicle-surf/src/git/repo/iter.rs b/radicle-surf/src/git/repo/iter.rs index 36920ab..33ec758 100644 --- a/radicle-surf/src/git/repo/iter.rs +++ b/radicle-surf/src/git/repo/iter.rs @@ -6,9 +6,9 @@ use std::{ convert::TryFrom as _, }; -use git_ref_format::{lit, Qualified}; +use git_ref_format::{lit, Qualified, RefString}; -use crate::git::{tag, Branch, Namespace, Tag}; +use crate::git::{refstr_join, tag, Branch, Namespace, Tag}; /// Iterator over [`Tag`]s. #[derive(Default)] @@ -165,7 +165,49 @@ impl Iterator for Namespaces { } } +#[derive(Default)] +pub struct Categories<'a> { + references: Vec>, + 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 { + 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//'")] + NotQualified(RefString), + #[error(transparent)] + RefFormat(#[from] git_ref_format::Error), + #[error(transparent)] + Utf8(#[from] str::Utf8Error), + } + #[derive(Debug, Error)] pub enum Tag { #[error(transparent)] diff --git a/radicle-surf/src/git/stats.rs b/radicle-surf/src/git/stats.rs index 5f0ad79..c2ac38b 100644 --- a/radicle-surf/src/git/stats.rs +++ b/radicle-surf/src/git/stats.rs @@ -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, diff --git a/radicle-surf/src/lib.rs b/radicle-surf/src/lib.rs index 815fd75..53e50e8 100644 --- a/radicle-surf/src/lib.rs +++ b/radicle-surf/src/lib.rs @@ -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; diff --git a/radicle-surf/src/object.rs b/radicle-surf/src/object.rs deleted file mode 100644 index 4ef08b3..0000000 --- a/radicle-surf/src/object.rs +++ /dev/null @@ -1,99 +0,0 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . - -//! 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(&self, serializer: S) -> Result - 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, -} - -#[cfg(feature = "serialize")] -impl Serialize for Info { - fn serialize(&self, serializer: S) -> Result - 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), -} diff --git a/radicle-surf/src/object/blob.rs b/radicle-surf/src/object/blob.rs deleted file mode 100644 index b8bef4d..0000000 --- a/radicle-surf/src/object/blob.rs +++ /dev/null @@ -1,216 +0,0 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . - -//! 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(&self, serializer: S) -> Result - 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), -} - -#[cfg(feature = "serialize")] -impl Serialize for BlobContent { - fn serialize(&self, serializer: S) -> Result - 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

(repo: &Repository, maybe_revision: Option, path: &P) -> Result -where - P: AsRef, -{ - make_blob(repo, maybe_revision, path, content) -} - -fn make_blob( - repo: &Repository, - maybe_revision: Option, - path: &P, - content: C, -) -> Result -where - P: AsRef, - 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

( - browser: &mut Browser, - maybe_revision: Option>, - path: &str, - theme: Option<&str>, - ) -> Result - 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), - } - } -} diff --git a/radicle-surf/src/object/tree.rs b/radicle-surf/src/object/tree.rs deleted file mode 100644 index e60c26c..0000000 --- a/radicle-surf/src/object/tree.rs +++ /dev/null @@ -1,170 +0,0 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . - -//! 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, - /// Extra info for the tree object. - pub info: Info, -} - -#[cfg(feature = "serialize")] -impl Serialize for Tree { - fn serialize(&self, serializer: S) -> Result - 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(&self, serializer: S) -> Result - 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

( - repo: &Repository, - maybe_revision: Option, - maybe_prefix: Option<&P>, -) -> Result -where - P: AsRef, -{ - 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, - }) -} diff --git a/radicle-surf/src/revision.rs b/radicle-surf/src/revision.rs deleted file mode 100644 index 9651cf7..0000000 --- a/radicle-surf/src/revision.rs +++ /dev/null @@ -1,189 +0,0 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . - -//! 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 { - /// 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, - }, - /// 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 { - 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 { - /// 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, - /// List of tag reference names. - pub tags: BTreeSet, -} - -/// 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( - repo: &Repository, - peer_id: RefString, - user: U, -) -> Result, Error> { - let pattern = peer_id.to_pattern(refspec::pattern!("*")); - let branches = repo - .branch_names(Glob::remotes(pattern))? - .map(|name| name.map(RefString::from)) - .collect::>()?; - 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( - repo: &Repository, - peer_id: RefString, - user: U, -) -> Result, Error> { - let branches = repo - .branch_names(Glob::all_heads())? - .map(|name| name.map(RefString::from)) - .collect::>()?; - let tags = repo - .tag_names()? - .map(|name| name.map(RefString::from)) - .collect::>()?; - 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( - repo: &Repository, - peer: Category, -) -> Result, Error> { - match peer { - Category::Local { peer_id, user } => local(repo, peer_id, user), - Category::Remote { peer_id, user } => remote(repo, peer_id, user), - } -} diff --git a/radicle-surf/src/source.rs b/radicle-surf/src/source.rs new file mode 100644 index 0000000..9e16e85 --- /dev/null +++ b/radicle-surf/src/source.rs @@ -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; diff --git a/radicle-surf/src/commit.rs b/radicle-surf/src/source/commit.rs similarity index 90% rename from radicle-surf/src/commit.rs rename to radicle-surf/src/source/commit.rs index d2b77cf..3bb3ce6 100644 --- a/radicle-surf/src/commit.rs +++ b/radicle-surf/src/source/commit.rs @@ -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 for Header { } } -#[cfg(feature = "serialize")] +#[cfg(feature = "serde")] impl Serialize for Header { fn serialize(&self, serializer: S) -> Result 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

, @@ -228,18 +227,13 @@ pub fn header(repo: &Repository, sha1: Oid) -> Result { /// /// Will return [`Error`] if the project doesn't exist or the surf interaction /// fails. -pub fn commits(repo: &Repository, maybe_revision: Option) -> Result { - 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, git::Error> = repo.history(&rev)?.collect(); - let headers = commits?.iter().map(Header::from).collect(); - +pub fn commits(repo: &Repository, revision: &R) -> Result +where + R: git::Revision, +{ + let stats = repo.get_commit_stats(revision)?; + let commits = repo.history(revision)?.collect::, _>>()?; + let headers = commits.into_iter().map(Header::from).collect(); Ok(Commits { headers, stats }) } diff --git a/radicle-surf/src/source/object.rs b/radicle-surf/src/source/object.rs new file mode 100644 index 0000000..6c943b5 --- /dev/null +++ b/radicle-surf/src/source/object.rs @@ -0,0 +1,47 @@ +// This file is part of radicle-surf +// +// +// Copyright (C) 2019-2020 The Radicle Team +// +// 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 . + +//! 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), +} diff --git a/radicle-surf/src/source/object/blob.rs b/radicle-surf/src/source/object/blob.rs new file mode 100644 index 0000000..c3f95ad --- /dev/null +++ b/radicle-surf/src/source/object/blob.rs @@ -0,0 +1,144 @@ +// This file is part of radicle-surf +// +// +// Copyright (C) 2019-2020 The Radicle Team +// +// 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 . + +//! 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, +} + +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(repo: &Repository, revision: &R, path: &P) -> Result + where + P: AsRef, + R: git::Revision, + { + Self::make_blob(repo, revision, path, |c| BlobContent::from(c)) + } + + fn make_blob( + repo: &Repository, + revision: &R, + path: &P, + content: impl FnOnce(FileContent) -> BlobContent, + ) -> Result + where + P: AsRef, + 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(&self, serializer: S) -> Result + 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), +} + +#[cfg(feature = "serde")] +impl Serialize for BlobContent { + fn serialize(&self, serializer: S) -> Result + 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> 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()), + } + } +} diff --git a/radicle-surf/src/source/object/tree.rs b/radicle-surf/src/source/object/tree.rs new file mode 100644 index 0000000..c06c416 --- /dev/null +++ b/radicle-surf/src/source/object/tree.rs @@ -0,0 +1,141 @@ +// This file is part of radicle-surf +// +// +// Copyright (C) 2019-2020 The Radicle Team +// +// 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 . + +//! 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, + /// Entries listed in that tree result. + pub entries: Vec, +} + +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(repo: &Repository, revision: &R, prefix: Option<&P>) -> Result + where + P: AsRef, + 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::>(); + 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(&self, serializer: S) -> Result + 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 for TreeEntry { + fn from(entry: directory::Entry) -> Self { + Self { entry } + } +} + +impl From for directory::Entry { + fn from(TreeEntry { entry }: TreeEntry) -> Self { + entry + } +} + +#[cfg(feature = "serde")] +impl Serialize for TreeEntry { + fn serialize(&self, serializer: S) -> Result + 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::)?; + state.serialize_field( + "kind", + match self.entry { + directory::Entry::File(_) => "blob", + directory::Entry::Directory(_) => "directory", + }, + )?; + state.end() + } +} diff --git a/radicle-surf/src/person.rs b/radicle-surf/src/source/person.rs similarity index 91% rename from radicle-surf/src/person.rs rename to radicle-surf/src/source/person.rs index e200715..ad7ceec 100644 --- a/radicle-surf/src/person.rs +++ b/radicle-surf/src/source/person.rs @@ -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. diff --git a/radicle-surf/src/syntax.rs b/radicle-surf/src/syntax.rs deleted file mode 100644 index b572a35..0000000 --- a/radicle-surf/src/syntax.rs +++ /dev/null @@ -1,87 +0,0 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . - -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 { - 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, - } -} diff --git a/radicle-surf/t/Cargo.toml b/radicle-surf/t/Cargo.toml index efda92d..c3d86f9 100644 --- a/radicle-surf/t/Cargo.toml +++ b/radicle-surf/t/Cargo.toml @@ -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" diff --git a/radicle-surf/t/src/git.rs b/radicle-surf/t/src/git.rs index 513583a..1c989d1 100644 --- a/radicle-surf/t/src/git.rs +++ b/radicle-surf/t/src/git.rs @@ -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)] diff --git a/radicle-surf/t/src/git/commit.rs b/radicle-surf/t/src/git/commit.rs index ef90cf5..5ad1bed 100644 --- a/radicle-surf/t/src/git/commit.rs +++ b/radicle-surf/t/src/git/commit.rs @@ -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()) { diff --git a/radicle-surf/t/src/git/diff.rs b/radicle-surf/t/src/git/diff.rs index 1b89d5c..fa00741 100644 --- a/radicle-surf/t/src/git/diff.rs +++ b/radicle-surf/t/src/git/diff.rs @@ -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}; diff --git a/radicle-surf/t/src/git/last_commit.rs b/radicle-surf/t/src/git/last_commit.rs index d12dd18..335acf1 100644 --- a/radicle-surf/t/src/git/last_commit.rs +++ b/radicle-surf/t/src/git/last_commit.rs @@ -18,7 +18,7 @@ fn readme_missing_and_memory() { // memory.rs is commited later so it should not exist here. let memory_last_commit_oid = repo - .last_commit(Path::new("src/memory.rs"), oid) + .last_commit(Path::new("src/memory.rs"), &oid) .expect("Failed to get last commit") .map(|commit| commit.id); @@ -26,7 +26,7 @@ fn readme_missing_and_memory() { // README.md exists in this commit. let readme_last_commit = repo - .last_commit(Path::new("README.md"), oid) + .last_commit(Path::new("README.md"), &oid) .expect("Failed to get last commit") .map(|commit| commit.id); @@ -44,7 +44,7 @@ fn folder_svelte() { let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap(); let folder_svelte = repo - .last_commit(Path::new("examples/Folder.svelte"), oid) + .last_commit(Path::new("examples/Folder.svelte"), &oid) .expect("Failed to get last commit") .map(|commit| commit.id); @@ -64,7 +64,7 @@ fn nest_directory() { let nested_directory_tree_commit_id = repo .last_commit( Path::new("this/is/a/really/deeply/nested/directory/tree"), - oid, + &oid, ) .expect("Failed to get last commit") .map(|commit| commit.id); @@ -85,13 +85,13 @@ fn can_get_last_commit_for_special_filenames() { let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap(); let backslash_commit_id = repo - .last_commit(Path::new(r"special/faux\\path"), oid) + .last_commit(Path::new(r"special/faux\\path"), &oid) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(backslash_commit_id, Some(expected_commit_id)); let ogre_commit_id = repo - .last_commit(Path::new("special/👹👹👹"), oid) + .last_commit(Path::new("special/👹👹👹"), &oid) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(ogre_commit_id, Some(expected_commit_id)); diff --git a/radicle-surf/t/src/git/namespace.rs b/radicle-surf/t/src/git/namespace.rs index 4bc7a29..2fead75 100644 --- a/radicle-surf/t/src/git/namespace.rs +++ b/radicle-surf/t/src/git/namespace.rs @@ -8,7 +8,7 @@ use super::GIT_PLATINUM; fn switch_to_banana() -> Result<(), Error> { let repo = Repository::open(GIT_PLATINUM)?; let history_master = repo.history(&Branch::local(refname!("master")))?; - repo.switch_namespace("golden")?; + repo.switch_namespace(&refname!("golden"))?; let history_banana = repo.history(&Branch::local(refname!("banana")))?; assert_ne!(history_master.head(), history_banana.head()); @@ -23,7 +23,7 @@ fn me_namespace() -> Result<(), Error> { assert_eq!(repo.which_namespace().unwrap(), None); - repo.switch_namespace("me")?; + repo.switch_namespace(&refname!("me"))?; assert_eq!(repo.which_namespace().unwrap(), Some("me".parse()?)); let history_feature = repo.history(&Branch::local(refname!("feature/#1194")))?; @@ -58,7 +58,7 @@ fn golden_namespace() -> Result<(), Error> { assert_eq!(repo.which_namespace().unwrap(), None); - repo.switch_namespace("golden")?; + repo.switch_namespace(&refname!("golden"))?; assert_eq!(repo.which_namespace().unwrap(), Some("golden".parse()?)); @@ -101,7 +101,7 @@ fn silver_namespace() -> Result<(), Error> { assert_eq!(repo.which_namespace().unwrap(), None); - repo.switch_namespace("golden/silver")?; + repo.switch_namespace(&refname!("golden/silver"))?; assert_eq!( repo.which_namespace().unwrap(), Some("golden/silver".parse()?) diff --git a/radicle-surf/t/src/git/rev.rs b/radicle-surf/t/src/git/rev.rs index 2b7d1b8..ec25d33 100644 --- a/radicle-surf/t/src/git/rev.rs +++ b/radicle-surf/t/src/git/rev.rs @@ -44,7 +44,7 @@ fn _master() -> Result<(), Error> { fn commit() -> Result<(), Error> { let repo = Repository::open(GIT_PLATINUM)?; let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; - let mut history = repo.history(rev)?; + let mut history = repo.history(&rev)?; let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; assert!(history.any(|commit| commit.unwrap().id == commit1)); @@ -56,7 +56,7 @@ fn commit() -> Result<(), Error> { fn commit_parents() -> Result<(), Error> { let repo = Repository::open(GIT_PLATINUM)?; let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; - let history = repo.history(rev)?; + let history = repo.history(&rev)?; let commit = history.head(); assert_eq!( @@ -71,7 +71,7 @@ fn commit_parents() -> Result<(), Error> { fn commit_short() -> Result<(), Error> { let repo = Repository::open(GIT_PLATINUM)?; let rev = repo.oid("3873745c8")?; - let mut history = repo.history(rev)?; + let mut history = repo.history(&rev)?; let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; assert!(history.any(|commit| commit.unwrap().id == commit1)); diff --git a/test/Cargo.toml b/test/Cargo.toml index 5e21e10..14761cb 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -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"