diff --git a/radicle-surf/Cargo.toml b/radicle-surf/Cargo.toml index fc8eece..a720eed 100644 --- a/radicle-surf/Cargo.toml +++ b/radicle-surf/Cargo.toml @@ -21,6 +21,7 @@ doctest = false [features] serialize = ["serde"] +syntax = ["syntect", "lazy_static"] # 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,6 +30,7 @@ gh-actions = [] [dependencies] base64 = "0.13" +log = "0.4" nonempty = "0.5" serde = { features = ["serde_derive"], optional = true, version = "1" } thiserror = "1.0" @@ -43,6 +45,10 @@ version = "0.1.0" path = "../git-ref-format" features = ["macro", "serde"] +[dependencies.lazy_static] +version = "1" +optional = true + [dependencies.radicle-git-ext] version = "0.2.0" path = "../radicle-git-ext" @@ -52,6 +58,10 @@ features = ["serde"] version = "0.1.0" path = "../radicle-std-ext" +[dependencies.syntect] +version = "5" +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/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/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/lib.rs b/radicle-surf/src/lib.rs index 3113878..53e50e8 100644 --- a/radicle-surf/src/lib.rs +++ b/radicle-surf/src/lib.rs @@ -83,8 +83,3 @@ pub mod diff; pub mod file_system; pub mod git; pub mod source; - -#[cfg(feature = "syntax")] -pub mod syntax; -#[cfg(feature = "syntax")] -pub use syntax::SYNTAX_SET; diff --git a/radicle-surf/src/source.rs b/radicle-surf/src/source.rs index 457c9cb..396cc80 100644 --- a/radicle-surf/src/source.rs +++ b/radicle-surf/src/source.rs @@ -11,13 +11,18 @@ //! [git-objects]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects pub mod object; -pub use object::{blob, tree, Blob, BlobContent, Info, ObjectType, Tree}; +pub use object::{blob, tree, Blob, BlobContent, Tree, TreeEntry}; pub mod commit; pub use commit::{commit, commits, Commit}; -pub mod person; +mod person; pub use person::Person; -pub mod revision; -pub use revision::Revision; +mod view; +pub use view::{view, View}; + +#[cfg(feature = "syntax")] +pub mod syntax; +#[cfg(feature = "syntax")] +pub use syntax::SYNTAX_SET; diff --git a/radicle-surf/src/source/commit.rs b/radicle-surf/src/source/commit.rs index 2b1483f..109f285 100644 --- a/radicle-surf/src/source/commit.rs +++ b/radicle-surf/src/source/commit.rs @@ -29,7 +29,7 @@ use serde::{ use crate::{ diff, git::{self, glob, Glob, Repository}, - source::{person::Person, revision::Revision}, + source::person::Person, }; use radicle_git_ext::Oid; @@ -227,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 index 39105ab..6c943b5 100644 --- a/radicle-surf/src/source/object.rs +++ b/radicle-surf/src/source/object.rs @@ -20,68 +20,13 @@ 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}; +pub use tree::{Tree, TreeEntry}; -use crate::{file_system::directory, git, source::commit}; - -/// Git object types. -/// -/// `shafiul.github.io/gitbook/1_the_git_object_model.html` -#[derive(Debug, Eq, Ord, PartialOrd, PartialEq)] -pub enum ObjectType { - /// References a list of other trees and blobs. - Tree, - /// Used to store file data. - Blob, -} - -#[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() - } -} +use crate::{file_system::directory, git}; /// An error reported by object types. #[derive(Debug, thiserror::Error)] @@ -89,6 +34,9 @@ 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), diff --git a/radicle-surf/src/source/object/blob.rs b/radicle-surf/src/source/object/blob.rs index 65d8161..24b165f 100644 --- a/radicle-surf/src/source/object/blob.rs +++ b/radicle-surf/src/source/object/blob.rs @@ -18,10 +18,7 @@ //! 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, -}; +use std::{path::Path, str}; #[cfg(feature = "serialize")] use serde::{ @@ -30,28 +27,63 @@ use serde::{ }; use crate::{ - git::Repository, - source::{ - commit, - object::{Error, Info, ObjectType}, - revision::Revision, - }, + file_system::{File, FileContent}, + git::{self, Repository}, + source::{commit, object::Error}, }; -#[cfg(feature = "syntax")] -use crate::syntax; - /// File data abstraction. pub struct Blob { - /// Actual content of the file, if the content is ASCII. + pub file: File, 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, + 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 { @@ -71,12 +103,14 @@ impl Serialize for Blob { where S: Serializer, { - let mut state = serializer.serialize_struct("Blob", 5)?; + const FIELDS: usize = 6; + let mut state = serializer.serialize_struct("Blob", FIELDS)?; 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.serialize_field("lastCommit", &self.commit)?; + state.serialize_field("name", &self.file.name())?; + state.serialize_field("path", &self.file.location())?; state.end() } } @@ -88,9 +122,9 @@ pub enum BlobContent { 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. + /// Note that it is necessary to enable the `syntax` feature flag + /// for this variant to be constructed. Use `Blob::highlighted`, + /// instead of `Blob::new` to get highlighted content. Html(String), /// Content is binary and needs special treatment. Binary(Vec), @@ -112,98 +146,54 @@ impl Serialize for BlobContent { } } -/// 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()), +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()), + } } } #[cfg(feature = "syntax")] pub mod highlighting { + use crate::source::syntax; + 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) - }) + impl Blob { + /// Returns the [`Blob`] for a file at `revision` under `path`. + /// + /// The content of the [`Blob`] will be highlighted, if possible. + /// + /// # Errors + /// + /// Will return [`Error`] if the project doesn't exist or a surf + /// interaction fails. + pub fn highlighted( + browser: &Repository, + revision: &R, + path: &P, + theme: Option<&str>, + ) -> Result + where + P: AsRef, + R: git::Revision, + { + Self::make_blob(browser, 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 { + fn content

(path: P, content: &FileContent, theme_name: Option<&str>) -> BlobContent + where + P: AsRef, + { + let content = content.as_bytes(); let content = match str::from_utf8(content) { Ok(content) => content, Err(_) => return BlobContent::Binary(content.to_owned()), diff --git a/radicle-surf/src/source/object/tree.rs b/radicle-surf/src/source/object/tree.rs index 03b8e0f..3c83670 100644 --- a/radicle-surf/src/source/object/tree.rs +++ b/radicle-surf/src/source/object/tree.rs @@ -18,9 +18,8 @@ //! 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 std::path::Path; -use git_ref_format::refname; #[cfg(feature = "serialize")] use serde::{ ser::{SerializeStruct as _, Serializer}, @@ -28,46 +27,95 @@ use serde::{ }; use crate::{ - file_system::directory, - git::Repository, - source::{ - commit, - object::{Error, Info, ObjectType}, - revision::Revision, - }, + file_system::{directory, Directory}, + git::{self, Repository}, + source::{commit, object::Error}, }; /// 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, + pub directory: Directory, + pub commit: Option, /// Entries listed in that tree result. pub entries: Vec, - /// Extra info for the tree object. - pub info: Info, } -#[cfg(feature = "serialize")] +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, { - let mut state = serializer.serialize_struct("Tree", 3)?; - state.serialize_field("path", &self.path)?; + const FIELDS: usize = 4; + let mut state = serializer.serialize_struct("Tree", FIELDS)?; state.serialize_field("entries", &self.entries)?; - state.serialize_field("info", &self.info)?; + state.serialize_field("lastCommit", &self.commit)?; + state.serialize_field("name", &self.directory.name())?; + state.serialize_field("path", &self.directory.location())?; state.end() } } -// TODO(xla): Ensure correct by construction. /// Entry in a Tree result. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 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, + 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 = "serialize")] @@ -76,97 +124,18 @@ impl Serialize for TreeEntry { where S: Serializer, { - let mut state = serializer.serialize_struct("Tree", 2)?; - state.serialize_field("path", &self.path)?; - state.serialize_field("info", &self.info)?; + 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() } } - -/// 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/source/revision.rs b/radicle-surf/src/source/revision.rs deleted file mode 100644 index 9651cf7..0000000 --- a/radicle-surf/src/source/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/syntax.rs b/radicle-surf/src/source/syntax.rs similarity index 89% rename from radicle-surf/src/syntax.rs rename to radicle-surf/src/source/syntax.rs index b572a35..b2f6c7a 100644 --- a/radicle-surf/src/syntax.rs +++ b/radicle-surf/src/source/syntax.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::path; +use std::path::Path; use syntect::{ easy::HighlightLines, @@ -56,10 +56,14 @@ lazy_static::lazy_static! { }; } -/// 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) +/// Return highlighted text given a file path, the original text, and +/// a theme. +pub fn highlight

(path: P, content: &str, theme_name: &str) -> Option +where + P: AsRef, +{ + let syntax = path + .as_ref() .extension() .and_then(std::ffi::OsStr::to_str) .and_then(|ext| SYNTAX_SET.find_syntax_by_extension(ext)); @@ -73,12 +77,13 @@ pub fn highlight(path: &str, content: &str, theme_name: &str) -> Option let mut html = String::with_capacity(content.len()); for line in LinesWithEndings::from(content) { - let regions = highlighter.highlight(line, &SYNTAX_SET); + let regions = highlighter.highlight_line(line, &SYNTAX_SET).ok()?; syntect::html::append_highlighted_html_for_styled_line( ®ions[..], syntect::html::IncludeBackground::No, &mut html, - ); + ) + .ok(); } Some(html) }, diff --git a/radicle-surf/src/source/view.rs b/radicle-surf/src/source/view.rs new file mode 100644 index 0000000..e8554f9 --- /dev/null +++ b/radicle-surf/src/source/view.rs @@ -0,0 +1,96 @@ +// 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::collections::{BTreeMap, BTreeSet}; + +use git_ref_format::{RefStr, RefString}; + +use crate::git::{Error, Glob, Repository}; + +/// A `View` represents a logical view of peer's repository in the +/// [Heartwood protocol][heartwood]. +/// +/// The `Id` parameter allows the specification of what the identifier +/// for a peer is in Heartwood. +/// +/// The `P` parameter allows the specification of what a person looks +/// like in Heartwood, whether that be a DID document, website, etc. +/// +/// [heartwood]: https://github.com/radicle-dev/heartwood +pub struct View { + /// Identifier for the peer that this `View` is associated to. + pub identifier: Id, + /// Personal information for the peer that this `View` is associated to. + pub person: Option

, + /// All `refs/heads` and `refs/remotes` reference names. + pub branches: BTreeSet, + /// All `refs/tags` reference names. + pub tags: BTreeSet, + /// Any `refs/` reference names, where each key in the map is a + /// `` and the set is the references under that category. + pub references: BTreeMap>, +} + +/// Construct a [`View`] for the `identifier` and `person`. +/// +/// `identifier` is assumed to act as the namespace in the Heartwood storage. +/// +/// `categories` is the set of git non-standard reference categories, +/// i.e. `refs/heads`, `refs/remotes`, `refs/tags`, and `refs/notes`. +pub fn view( + repo: &Repository, + identifier: Id, + person: Option

, + categories: impl IntoIterator, +) -> Result, Error> +where + Id: Clone + Into, + R: AsRef, +{ + let namespace = identifier.clone().into(); + repo.with_namespace(&namespace.as_ref().to_ref_string(), move || { + let branches = repo + .branch_names(Glob::all_heads())? + .map(|name| name.map(RefString::from)) + .collect::>()?; + let tags = repo + .tag_names(&Glob::all_tags())? + .map(|name| name.map(RefString::from)) + .collect::>()?; + let categories = categories.into_iter().fold(Glob::default(), |globs, cat| { + globs.and(Glob::all_category(cat)) + }); + let references = + repo.categories(&categories)? + .try_fold(BTreeMap::new(), |mut map, r| { + let (cat, name) = r?; + map.entry(cat) + .and_modify(|names: &mut BTreeSet| { + names.insert(name.clone()); + }) + .or_insert_with(|| BTreeSet::from_iter(Some(name))); + Ok::<_, Error>(map) + })?; + Ok(View { + identifier, + person, + branches, + tags, + references, + }) + }) +} 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));