Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
radicle-surf: source module
Fintan Halpenny committed 3 years ago
commit 02321aba7cf280c9ff3d6572488bb4d54cf61c57
parent 5b6b447
14 files changed +992 -977
deleted radicle-surf/src/commit.rs
@@ -1,259 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! Represents a commit.
-

-
use std::path::PathBuf;
-

-
use git_ref_format::RefString;
-
#[cfg(feature = "serialize")]
-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use crate::{
-
    diff,
-
    git::{self, glob, Glob, Repository},
-
    person::Person,
-
    revision::Revision,
-
};
-

-
use radicle_git_ext::Oid;
-

-
/// Commit statistics.
-
#[cfg_attr(feature = "serialize", derive(Serialize))]
-
#[derive(Clone)]
-
pub struct Stats {
-
    /// Additions.
-
    pub additions: u64,
-
    /// Deletions.
-
    pub deletions: u64,
-
}
-

-
/// Representation of a changeset between two revs.
-
#[cfg_attr(feature = "serialize", derive(Serialize))]
-
#[derive(Clone)]
-
pub struct Commit {
-
    /// The commit header.
-
    pub header: Header,
-
    /// The change statistics for this commit.
-
    pub stats: Stats,
-
    /// The changeset introduced by this commit.
-
    pub diff: diff::Diff,
-
    /// The list of branches this commit belongs to.
-
    pub branches: Vec<RefString>,
-
}
-

-
/// Representation of a code commit.
-
#[derive(Clone)]
-
pub struct Header {
-
    /// Identifier of the commit in the form of a sha1 hash. Often referred to
-
    /// as oid or object id.
-
    pub sha1: Oid,
-
    /// The author of the commit.
-
    pub author: Person,
-
    /// The summary of the commit message body.
-
    pub summary: String,
-
    /// The entire commit message body.
-
    pub message: String,
-
    /// The committer of the commit.
-
    pub committer: Person,
-
    /// The recorded time of the committer signature. This is a convenience
-
    /// alias until we expose the actual author and commiter signatures.
-
    pub committer_time: git2::Time,
-
}
-

-
impl Header {
-
    /// Returns the commit description text. This is the text after the one-line
-
    /// summary.
-
    #[must_use]
-
    pub fn description(&self) -> &str {
-
        self.message
-
            .strip_prefix(&self.summary)
-
            .unwrap_or(&self.message)
-
            .trim()
-
    }
-
}
-

-
impl From<&git::Commit> for Header {
-
    fn from(commit: &git::Commit) -> Self {
-
        Self {
-
            sha1: commit.id,
-
            author: Person {
-
                name: commit.author.name.clone(),
-
                email: commit.author.email.clone(),
-
            },
-
            summary: commit.summary.clone(),
-
            message: commit.message.clone(),
-
            committer: Person {
-
                name: commit.committer.name.clone(),
-
                email: commit.committer.email.clone(),
-
            },
-
            committer_time: commit.committer.time,
-
        }
-
    }
-
}
-

-
impl From<git::Commit> for Header {
-
    fn from(commit: git::Commit) -> Self {
-
        Self::from(&commit)
-
    }
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for Header {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Header", 6)?;
-
        state.serialize_field("sha1", &self.sha1.to_string())?;
-
        state.serialize_field("author", &self.author)?;
-
        state.serialize_field("summary", &self.summary)?;
-
        state.serialize_field("description", &self.description())?;
-
        state.serialize_field("committer", &self.committer)?;
-
        state.serialize_field("committerTime", &self.committer_time.seconds())?;
-
        state.end()
-
    }
-
}
-

-
/// A selection of commit headers and their statistics.
-
#[cfg_attr(feature = "serialize", derive(Serialize))]
-
pub struct Commits {
-
    /// The commit headers
-
    pub headers: Vec<Header>,
-
    /// The statistics for the commit headers
-
    pub stats: git::Stats,
-
}
-

-
/// Retrieves a [`Commit`].
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn commit<R: git::Revision>(repo: &Repository, rev: R) -> Result<Commit, Error> {
-
    let commit = repo.commit(rev)?;
-
    let sha1 = commit.id;
-
    let header = Header::from(&commit);
-
    let diff = repo.diff_from_parent(commit)?;
-

-
    let mut deletions = 0;
-
    let mut additions = 0;
-

-
    for file in &diff.modified {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    match line {
-
                        diff::LineDiff::Addition { .. } => additions += 1,
-
                        diff::LineDiff::Deletion { .. } => deletions += 1,
-
                        _ => {},
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    for file in &diff.created {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    if let diff::LineDiff::Addition { .. } = line {
-
                        additions += 1
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    for file in &diff.deleted {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    if let diff::LineDiff::Deletion { .. } = line {
-
                        deletions += 1
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    let branches = repo
-
        .revision_branches(&sha1, Glob::all_heads().branches().and(Glob::all_remotes()))?
-
        .into_iter()
-
        .map(|b| b.refname().into())
-
        .collect();
-

-
    Ok(Commit {
-
        header,
-
        stats: Stats {
-
            additions,
-
            deletions,
-
        },
-
        diff,
-
        branches,
-
    })
-
}
-

-
/// Retrieves the [`Header`] for the given `sha1`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn header(repo: &Repository, sha1: Oid) -> Result<Header, Error> {
-
    let commit = repo.commit(sha1)?;
-
    Ok(Header::from(&commit))
-
}
-

-
/// Retrieves the [`Commit`] history for the given `revision`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or the surf interaction
-
/// fails.
-
pub fn commits(repo: &Repository, maybe_revision: Option<Revision>) -> Result<Commits, Error> {
-
    let rev = match maybe_revision {
-
        Some(revision) => revision,
-
        None => Revision::Sha {
-
            sha: repo.head_oid()?,
-
        },
-
    };
-

-
    let stats = repo.get_commit_stats(&rev)?;
-
    let commits: Result<Vec<git::Commit>, git::Error> = repo.history(&rev)?.collect();
-
    let headers = commits?.iter().map(Header::from).collect();
-

-
    Ok(Commits { headers, stats })
-
}
-

-
/// An error reported by commit API.
-
#[derive(Debug, thiserror::Error)]
-
pub enum Error {
-
    /// An error occurred during a git operation.
-
    #[error(transparent)]
-
    Git(#[from] git::Error),
-

-
    #[error(transparent)]
-
    Glob(#[from] glob::Error),
-

-
    /// Trying to find a file path which could not be found.
-
    #[error("the path '{0}' was not found")]
-
    PathNotFound(PathBuf),
-
}
modified radicle-surf/src/lib.rs
@@ -82,18 +82,7 @@ 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;
+
pub mod source;

#[cfg(feature = "syntax")]
pub mod syntax;
deleted radicle-surf/src/object.rs
@@ -1,99 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! Common definitions for git objects (blob and tree).
-
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
-

-
use std::path::PathBuf;
-

-
#[cfg(feature = "serialize")]
-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
pub mod blob;
-
pub use blob::{Blob, BlobContent};
-

-
pub mod tree;
-
pub use tree::{tree, Tree, TreeEntry};
-

-
use crate::{commit, file_system::directory, git};
-

-
/// Git object types.
-
///
-
/// `shafiul.github.io/gitbook/1_the_git_object_model.html`
-
#[derive(Debug, Eq, Ord, PartialOrd, PartialEq)]
-
pub enum ObjectType {
-
    /// References a list of other trees and blobs.
-
    Tree,
-
    /// Used to store file data.
-
    Blob,
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for ObjectType {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        match self {
-
            Self::Blob => serializer.serialize_unit_variant("ObjectType", 0, "BLOB"),
-
            Self::Tree => serializer.serialize_unit_variant("ObjectType", 1, "TREE"),
-
        }
-
    }
-
}
-

-
/// Set of extra information we carry for blob and tree objects returned from
-
/// the API.
-
pub struct Info {
-
    /// Name part of an object.
-
    pub name: String,
-
    /// The type of the object.
-
    pub object_type: ObjectType,
-
    /// The last commmit that touched this object.
-
    pub last_commit: Option<commit::Header>,
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for Info {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Info", 3)?;
-
        state.serialize_field("name", &self.name)?;
-
        state.serialize_field("objectType", &self.object_type)?;
-
        state.serialize_field("lastCommit", &self.last_commit)?;
-
        state.end()
-
    }
-
}
-

-
/// An error reported by object types.
-
#[derive(Debug, thiserror::Error)]
-
pub enum Error {
-
    #[error(transparent)]
-
    Directory(#[from] directory::error::Directory),
-

-
    /// An error occurred during a git operation.
-
    #[error(transparent)]
-
    Git(#[from] git::Error),
-

-
    /// Trying to find a file path which could not be found.
-
    #[error("the path '{0}' was not found")]
-
    PathNotFound(PathBuf),
-
}
deleted radicle-surf/src/object/blob.rs
@@ -1,216 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! Represents git object type 'blob', i.e. actual file contents.
-
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
-

-
use std::{
-
    path::{Path, PathBuf},
-
    str,
-
};
-

-
#[cfg(feature = "serialize")]
-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use crate::{
-
    commit,
-
    git::Repository,
-
    object::{Error, Info, ObjectType},
-
    revision::Revision,
-
};
-

-
#[cfg(feature = "syntax")]
-
use crate::syntax;
-

-
/// File data abstraction.
-
pub struct Blob {
-
    /// Actual content of the file, if the content is ASCII.
-
    pub content: BlobContent,
-
    /// Extra info for the file.
-
    pub info: Info,
-
    /// Absolute path to the object from the root of the repo.
-
    pub path: PathBuf,
-
}
-

-
impl Blob {
-
    /// Indicates if the content of the [`Blob`] is binary.
-
    #[must_use]
-
    pub fn is_binary(&self) -> bool {
-
        matches!(self.content, BlobContent::Binary(_))
-
    }
-

-
    /// Indicates if the content of the [`Blob`] is HTML.
-
    #[must_use]
-
    pub const fn is_html(&self) -> bool {
-
        matches!(self.content, BlobContent::Html(_))
-
    }
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for Blob {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Blob", 5)?;
-
        state.serialize_field("binary", &self.is_binary())?;
-
        state.serialize_field("html", &self.is_html())?;
-
        state.serialize_field("content", &self.content)?;
-
        state.serialize_field("info", &self.info)?;
-
        state.serialize_field("path", &self.path)?;
-
        state.end()
-
    }
-
}
-

-
/// Variants of blob content.
-
#[derive(PartialEq, Eq)]
-
pub enum BlobContent {
-
    /// Content is plain text and can be passed as a string.
-
    Plain(String),
-
    /// Content is syntax-highlighted HTML.
-
    ///
-
    /// Note that is necessary to enable the `syntax` feature flag for this
-
    /// variant to be constructed. Use `highlighting::blob`, instead of
-
    /// [`blob`] to get highlighted content.
-
    Html(String),
-
    /// Content is binary and needs special treatment.
-
    Binary(Vec<u8>),
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for BlobContent {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        match self {
-
            Self::Plain(content) | Self::Html(content) => serializer.serialize_str(content),
-
            Self::Binary(bytes) => {
-
                let encoded = base64::encode(bytes);
-
                serializer.serialize_str(&encoded)
-
            },
-
        }
-
    }
-
}
-

-
/// Returns the [`Blob`] for a file at `revision` under `path`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if the project doesn't exist or a surf interaction
-
/// fails.
-
pub fn blob<P>(repo: &Repository, maybe_revision: Option<Revision>, path: &P) -> Result<Blob, Error>
-
where
-
    P: AsRef<Path>,
-
{
-
    make_blob(repo, maybe_revision, path, content)
-
}
-

-
fn make_blob<P, C>(
-
    repo: &Repository,
-
    maybe_revision: Option<Revision>,
-
    path: &P,
-
    content: C,
-
) -> Result<Blob, Error>
-
where
-
    P: AsRef<Path>,
-
    C: FnOnce(&[u8]) -> BlobContent,
-
{
-
    let path = path.as_ref();
-
    let revision = maybe_revision.unwrap();
-
    let root = repo.root_dir(&revision)?;
-

-
    let file = root
-
        .find_file(&path, repo)?
-
        .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?;
-

-
    let last_commit = repo
-
        .last_commit(path, &revision)?
-
        .map(|c| commit::Header::from(&c));
-
    // TODO: fuck this
-
    let name = path
-
        .file_name()
-
        .unwrap()
-
        .to_os_string()
-
        .into_string()
-
        .ok()
-
        .unwrap();
-

-
    let file_content = repo.file_content(file)?;
-
    let content = content(file_content.as_bytes());
-

-
    Ok(Blob {
-
        content,
-
        info: Info {
-
            name,
-
            object_type: ObjectType::Blob,
-
            last_commit,
-
        },
-
        path: path.to_path_buf(),
-
    })
-
}
-

-
/// Return a [`BlobContent`] given a byte slice.
-
fn content(content: &[u8]) -> BlobContent {
-
    match str::from_utf8(content) {
-
        Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
-
        Err(_) => BlobContent::Binary(content.to_owned()),
-
    }
-
}
-

-
#[cfg(feature = "syntax")]
-
pub mod highlighting {
-
    use super::*;
-

-
    /// Returns the [`Blob`] for a file at `revision` under `path`.
-
    ///
-
    /// # Errors
-
    ///
-
    /// Will return [`Error`] if the project doesn't exist or a surf interaction
-
    /// fails.
-
    pub fn blob<P>(
-
        browser: &mut Browser,
-
        maybe_revision: Option<Revision<P>>,
-
        path: &str,
-
        theme: Option<&str>,
-
    ) -> Result<Blob, Error>
-
    where
-
        P: ToString,
-
    {
-
        make_blob(browser, maybe_revision, path, |contents| {
-
            content(path, contents, theme)
-
        })
-
    }
-

-
    /// Return a [`BlobContent`] given a file path, content and theme. Attempts
-
    /// to perform syntax highlighting when the theme is `Some`.
-
    fn content(path: &str, content: &[u8], theme_name: Option<&str>) -> BlobContent {
-
        let content = match str::from_utf8(content) {
-
            Ok(content) => content,
-
            Err(_) => return BlobContent::Binary(content.to_owned()),
-
        };
-

-
        match theme_name {
-
            None => BlobContent::Plain(content.to_owned()),
-
            Some(theme) => syntax::highlight(path, content, theme)
-
                .map_or_else(|| BlobContent::Plain(content.to_owned()), BlobContent::Html),
-
        }
-
    }
-
}
deleted radicle-surf/src/object/tree.rs
@@ -1,170 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! Represents git object type 'tree', i.e. like directory entries in Unix.
-
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
-

-
use std::path::{Path, PathBuf};
-

-
use git_ref_format::refname;
-
#[cfg(feature = "serialize")]
-
use serde::{
-
    ser::{SerializeStruct as _, Serializer},
-
    Serialize,
-
};
-

-
use crate::{
-
    commit,
-
    file_system::directory,
-
    git::Repository,
-
    object::{Error, Info, ObjectType},
-
    revision::Revision,
-
};
-

-
/// Result of a directory listing, carries other trees and blobs.
-
pub struct Tree {
-
    /// Absolute path to the tree object from the repo root.
-
    pub path: PathBuf,
-
    /// Entries listed in that tree result.
-
    pub entries: Vec<TreeEntry>,
-
    /// Extra info for the tree object.
-
    pub info: Info,
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for Tree {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Tree", 3)?;
-
        state.serialize_field("path", &self.path)?;
-
        state.serialize_field("entries", &self.entries)?;
-
        state.serialize_field("info", &self.info)?;
-
        state.end()
-
    }
-
}
-

-
// TODO(xla): Ensure correct by construction.
-
/// Entry in a Tree result.
-
pub struct TreeEntry {
-
    /// Extra info for the entry.
-
    pub info: Info,
-
    /// Absolute path to the object from the root of the repo.
-
    pub path: PathBuf,
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for TreeEntry {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        let mut state = serializer.serialize_struct("Tree", 2)?;
-
        state.serialize_field("path", &self.path)?;
-
        state.serialize_field("info", &self.info)?;
-
        state.end()
-
    }
-
}
-

-
/// Retrieve the [`Tree`] for the given `revision` and directory `prefix`.
-
///
-
/// # Errors
-
///
-
/// Will return [`Error`] if any of the surf interactions fail.
-
pub fn tree<P>(
-
    repo: &Repository,
-
    maybe_revision: Option<Revision>,
-
    maybe_prefix: Option<&P>,
-
) -> Result<Tree, Error>
-
where
-
    P: AsRef<Path>,
-
{
-
    let maybe_prefix = maybe_prefix.map(|p| p.as_ref());
-
    let rev = match maybe_revision {
-
        Some(r) => r,
-
        None => Revision::Branch {
-
            name: refname!("main"),
-
            remote: None,
-
        },
-
    };
-

-
    let prefix_dir = match maybe_prefix {
-
        None => repo.root_dir(&rev)?,
-
        Some(path) => repo
-
            .root_dir(&rev)?
-
            .find_directory(path, repo)?
-
            .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
-
    };
-

-
    let mut entries = prefix_dir
-
        .entries(repo)?
-
        .entries()
-
        .fold(Vec::new(), |mut entries, entry| {
-
            let entry_path = match maybe_prefix {
-
                Some(path) => {
-
                    let mut path = path.to_path_buf();
-
                    path.push(entry.name());
-
                    path
-
                },
-
                None => PathBuf::new(),
-
            };
-

-
            let info = Info {
-
                name: entry.name().clone(),
-
                object_type: match entry {
-
                    directory::Entry::Directory(_) => ObjectType::Tree,
-
                    directory::Entry::File { .. } => ObjectType::Blob,
-
                },
-
                last_commit: None,
-
            };
-

-
            entries.push(TreeEntry {
-
                info,
-
                path: entry_path,
-
            });
-
            entries
-
        });
-

-
    // We want to ensure that in the response Tree entries come first. `Ord` being
-
    // derived on the enum ensures Variant declaration order.
-
    //
-
    // https://doc.rust-lang.org/std/cmp/trait.Ord.html#derivable
-
    entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
-

-
    let last_commit = if maybe_prefix.is_none() {
-
        let history = repo.history(&rev)?;
-
        Some(commit::Header::from(history.head()))
-
    } else {
-
        None
-
    };
-
    let name = match maybe_prefix {
-
        None => "".into(),
-
        Some(path) => path.file_name().unwrap().to_str().unwrap().to_string(),
-
    };
-
    let info = Info {
-
        name,
-
        object_type: ObjectType::Tree,
-
        last_commit,
-
    };
-

-
    Ok(Tree {
-
        path: maybe_prefix.map_or(PathBuf::new(), |path| path.to_path_buf()),
-
        entries,
-
        info,
-
    })
-
}
deleted radicle-surf/src/person.rs
@@ -1,32 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! Represents 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))]
-
#[derive(Clone, Debug)]
-
pub struct Person {
-
    /// Name part of the commit signature.
-
    pub name: String,
-
    /// Email part of the commit signature.
-
    pub email: String,
-
}
deleted radicle-surf/src/revision.rs
@@ -1,189 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! Represents revisions
-

-
use std::collections::BTreeSet;
-

-
use git_ref_format::{lit, refspec, Qualified, RefString};
-

-
#[cfg(feature = "serialize")]
-
use serde::{Deserialize, Serialize};
-

-
use radicle_git_ext::Oid;
-

-
use crate::git::{self, Error, Glob, Repository};
-

-
/// Types of a peer.
-
pub enum Category<P, U> {
-
    /// Local peer.
-
    Local {
-
        /// Peer Id
-
        peer_id: P,
-
        /// User name
-
        user: U,
-
    },
-
    /// Remote peer.
-
    Remote {
-
        /// Peer Id
-
        peer_id: P,
-
        /// User name
-
        user: U,
-
    },
-
}
-

-
/// A revision selector for a `Browser`.
-
#[cfg_attr(
-
    feature = "serialize",
-
    derive(Deserialize, Serialize),
-
    serde(rename_all = "camelCase", tag = "type")
-
)]
-
#[derive(Debug, Clone)]
-
pub enum Revision {
-
    /// Select a tag under the name provided.
-
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
-
    Tag {
-
        /// Name of the tag.
-
        name: RefString,
-
    },
-
    /// Select a branch under the name provided.
-
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
-
    Branch {
-
        /// Name of the branch.
-
        name: RefString,
-
        /// The remote peer, if specified.
-
        remote: Option<RefString>,
-
    },
-
    /// Select a SHA1 under the name provided.
-
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
-
    Sha {
-
        /// The SHA1 value.
-
        sha: Oid,
-
    },
-
}
-

-
impl git::Revision for &Revision {
-
    type Error = git2::Error;
-

-
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
-
        match self {
-
            Revision::Tag { name } => match name.qualified() {
-
                None => Qualified::from(lit::refs_tags(name)).object_id(repo),
-
                Some(name) => name.object_id(repo),
-
            },
-
            Revision::Branch { name, remote } => match remote {
-
                Some(remote) => {
-
                    Qualified::from(lit::refs_remotes(remote.join(name))).object_id(repo)
-
                },
-
                None => git::Branch::local(name).refname().object_id(repo),
-
            },
-
            Revision::Sha { sha } => Ok(*sha),
-
        }
-
    }
-
}
-

-
/// Bundled response to retrieve both branches and tags for
-
/// a user's repo.
-
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct Revisions<P, U> {
-
    /// The peer peer_id for the user.
-
    pub peer_id: P,
-
    /// The user who owns these revisions.
-
    pub user: U,
-
    /// List of branch reference names.
-
    pub branches: BTreeSet<RefString>,
-
    /// List of tag reference names.
-
    pub tags: BTreeSet<RefString>,
-
}
-

-
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
-
/// remote branches.
-
///
-
/// If there are no branches then this returns `None`.
-
///
-
/// # Errors
-
///
-
///   * If we cannot get the branches from the `Browser`
-
pub fn remote<U>(
-
    repo: &Repository,
-
    peer_id: RefString,
-
    user: U,
-
) -> Result<Revisions<RefString, U>, Error> {
-
    let pattern = peer_id.to_pattern(refspec::pattern!("*"));
-
    let branches = repo
-
        .branch_names(Glob::remotes(pattern))?
-
        .map(|name| name.map(RefString::from))
-
        .collect::<Result<_, _>>()?;
-
    Ok(Revisions {
-
        peer_id,
-
        user,
-
        branches,
-
        // TODO(rudolfs): implement remote peer tags once we decide how
-
        // https://radicle.community/t/git-tags/214
-
        tags: BTreeSet::new(),
-
    })
-
}
-

-
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
-
/// local branches.
-
///
-
/// If there are no branches then this returns `None`.
-
///
-
/// # Errors
-
///
-
///   * If we cannot get the branches from the `Browser`
-
pub fn local<U>(
-
    repo: &Repository,
-
    peer_id: RefString,
-
    user: U,
-
) -> Result<Revisions<RefString, U>, Error> {
-
    let branches = repo
-
        .branch_names(Glob::all_heads())?
-
        .map(|name| name.map(RefString::from))
-
        .collect::<Result<_, _>>()?;
-
    let tags = repo
-
        .tag_names()?
-
        .map(|name| name.map(RefString::from))
-
        .collect::<Result<_, _>>()?;
-
    Ok(Revisions {
-
        peer_id,
-
        user,
-
        branches,
-
        tags,
-
    })
-
}
-

-
/// Provide the [`Revisions`] of a peer.
-
///
-
/// If the peer is [`Category::Local`], meaning that is the current person doing
-
/// the browsing and no remote is set for the reference.
-
///
-
/// Othewise, the peer is [`Category::Remote`], meaning that we are looking into
-
/// a remote part of a reference.
-
///
-
/// # Errors
-
///
-
///   * If we cannot get the branches from the `Browser`
-
pub fn revisions<U>(
-
    repo: &Repository,
-
    peer: Category<RefString, U>,
-
) -> Result<Revisions<RefString, U>, Error> {
-
    match peer {
-
        Category::Local { peer_id, user } => local(repo, peer_id, user),
-
        Category::Remote { peer_id, user } => remote(repo, peer_id, user),
-
    }
-
}
added radicle-surf/src/source.rs
@@ -0,0 +1,23 @@
+
//! 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, Info, ObjectType, Tree};
+

+
pub mod commit;
+
pub use commit::{commit, commits, Commit};
+

+
pub mod person;
+
pub use person::Person;
+

+
pub mod revision;
+
pub use revision::Revision;
added radicle-surf/src/source/commit.rs
@@ -0,0 +1,258 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
//! Represents a commit.
+

+
use std::path::PathBuf;
+

+
use git_ref_format::RefString;
+
#[cfg(feature = "serialize")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::{
+
    diff,
+
    git::{self, glob, Glob, Repository},
+
    source::{person::Person, revision::Revision},
+
};
+

+
use radicle_git_ext::Oid;
+

+
/// Commit statistics.
+
#[cfg_attr(feature = "serialize", derive(Serialize))]
+
#[derive(Clone)]
+
pub struct Stats {
+
    /// Additions.
+
    pub additions: u64,
+
    /// Deletions.
+
    pub deletions: u64,
+
}
+

+
/// Representation of a changeset between two revs.
+
#[cfg_attr(feature = "serialize", derive(Serialize))]
+
#[derive(Clone)]
+
pub struct Commit {
+
    /// The commit header.
+
    pub header: Header,
+
    /// The change statistics for this commit.
+
    pub stats: Stats,
+
    /// The changeset introduced by this commit.
+
    pub diff: diff::Diff,
+
    /// The list of branches this commit belongs to.
+
    pub branches: Vec<RefString>,
+
}
+

+
/// Representation of a code commit.
+
#[derive(Clone)]
+
pub struct Header {
+
    /// Identifier of the commit in the form of a sha1 hash. Often referred to
+
    /// as oid or object id.
+
    pub sha1: Oid,
+
    /// The author of the commit.
+
    pub author: Person,
+
    /// The summary of the commit message body.
+
    pub summary: String,
+
    /// The entire commit message body.
+
    pub message: String,
+
    /// The committer of the commit.
+
    pub committer: Person,
+
    /// The recorded time of the committer signature. This is a convenience
+
    /// alias until we expose the actual author and commiter signatures.
+
    pub committer_time: git2::Time,
+
}
+

+
impl Header {
+
    /// Returns the commit description text. This is the text after the one-line
+
    /// summary.
+
    #[must_use]
+
    pub fn description(&self) -> &str {
+
        self.message
+
            .strip_prefix(&self.summary)
+
            .unwrap_or(&self.message)
+
            .trim()
+
    }
+
}
+

+
impl From<&git::Commit> for Header {
+
    fn from(commit: &git::Commit) -> Self {
+
        Self {
+
            sha1: commit.id,
+
            author: Person {
+
                name: commit.author.name.clone(),
+
                email: commit.author.email.clone(),
+
            },
+
            summary: commit.summary.clone(),
+
            message: commit.message.clone(),
+
            committer: Person {
+
                name: commit.committer.name.clone(),
+
                email: commit.committer.email.clone(),
+
            },
+
            committer_time: commit.committer.time,
+
        }
+
    }
+
}
+

+
impl From<git::Commit> for Header {
+
    fn from(commit: git::Commit) -> Self {
+
        Self::from(&commit)
+
    }
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for Header {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Header", 6)?;
+
        state.serialize_field("sha1", &self.sha1.to_string())?;
+
        state.serialize_field("author", &self.author)?;
+
        state.serialize_field("summary", &self.summary)?;
+
        state.serialize_field("description", &self.description())?;
+
        state.serialize_field("committer", &self.committer)?;
+
        state.serialize_field("committerTime", &self.committer_time.seconds())?;
+
        state.end()
+
    }
+
}
+

+
/// A selection of commit headers and their statistics.
+
#[cfg_attr(feature = "serialize", derive(Serialize))]
+
pub struct Commits {
+
    /// The commit headers
+
    pub headers: Vec<Header>,
+
    /// The statistics for the commit headers
+
    pub stats: git::Stats,
+
}
+

+
/// Retrieves a [`Commit`].
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn commit<R: git::Revision>(repo: &Repository, rev: R) -> Result<Commit, Error> {
+
    let commit = repo.commit(rev)?;
+
    let sha1 = commit.id;
+
    let header = Header::from(&commit);
+
    let diff = repo.diff_from_parent(commit)?;
+

+
    let mut deletions = 0;
+
    let mut additions = 0;
+

+
    for file in &diff.modified {
+
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
+
            for hunk in hunks.iter() {
+
                for line in &hunk.lines {
+
                    match line {
+
                        diff::LineDiff::Addition { .. } => additions += 1,
+
                        diff::LineDiff::Deletion { .. } => deletions += 1,
+
                        _ => {},
+
                    }
+
                }
+
            }
+
        }
+
    }
+

+
    for file in &diff.created {
+
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
+
            for hunk in hunks.iter() {
+
                for line in &hunk.lines {
+
                    if let diff::LineDiff::Addition { .. } = line {
+
                        additions += 1
+
                    }
+
                }
+
            }
+
        }
+
    }
+

+
    for file in &diff.deleted {
+
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
+
            for hunk in hunks.iter() {
+
                for line in &hunk.lines {
+
                    if let diff::LineDiff::Deletion { .. } = line {
+
                        deletions += 1
+
                    }
+
                }
+
            }
+
        }
+
    }
+

+
    let branches = repo
+
        .revision_branches(&sha1, Glob::all_heads().branches().and(Glob::all_remotes()))?
+
        .into_iter()
+
        .map(|b| b.refname().into())
+
        .collect();
+

+
    Ok(Commit {
+
        header,
+
        stats: Stats {
+
            additions,
+
            deletions,
+
        },
+
        diff,
+
        branches,
+
    })
+
}
+

+
/// Retrieves the [`Header`] for the given `sha1`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn header(repo: &Repository, sha1: Oid) -> Result<Header, Error> {
+
    let commit = repo.commit(sha1)?;
+
    Ok(Header::from(&commit))
+
}
+

+
/// Retrieves the [`Commit`] history for the given `revision`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or the surf interaction
+
/// fails.
+
pub fn commits(repo: &Repository, maybe_revision: Option<Revision>) -> Result<Commits, Error> {
+
    let rev = match maybe_revision {
+
        Some(revision) => revision,
+
        None => Revision::Sha {
+
            sha: repo.head_oid()?,
+
        },
+
    };
+

+
    let stats = repo.get_commit_stats(&rev)?;
+
    let commits: Result<Vec<git::Commit>, git::Error> = repo.history(&rev)?.collect();
+
    let headers = commits?.iter().map(Header::from).collect();
+

+
    Ok(Commits { headers, stats })
+
}
+

+
/// An error reported by commit API.
+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    /// An error occurred during a git operation.
+
    #[error(transparent)]
+
    Git(#[from] git::Error),
+

+
    #[error(transparent)]
+
    Glob(#[from] glob::Error),
+

+
    /// Trying to find a file path which could not be found.
+
    #[error("the path '{0}' was not found")]
+
    PathNotFound(PathBuf),
+
}
added radicle-surf/src/source/object.rs
@@ -0,0 +1,99 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
//! Common definitions for git objects (blob and tree).
+
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+

+
use std::path::PathBuf;
+

+
#[cfg(feature = "serialize")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
pub mod blob;
+
pub use blob::{Blob, BlobContent};
+

+
pub mod tree;
+
pub use tree::{tree, Tree, TreeEntry};
+

+
use crate::{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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        match self {
+
            Self::Blob => serializer.serialize_unit_variant("ObjectType", 0, "BLOB"),
+
            Self::Tree => serializer.serialize_unit_variant("ObjectType", 1, "TREE"),
+
        }
+
    }
+
}
+

+
/// Set of extra information we carry for blob and tree objects returned from
+
/// the API.
+
pub struct Info {
+
    /// Name part of an object.
+
    pub name: String,
+
    /// The type of the object.
+
    pub object_type: ObjectType,
+
    /// The last commmit that touched this object.
+
    pub last_commit: Option<commit::Header>,
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for Info {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Info", 3)?;
+
        state.serialize_field("name", &self.name)?;
+
        state.serialize_field("objectType", &self.object_type)?;
+
        state.serialize_field("lastCommit", &self.last_commit)?;
+
        state.end()
+
    }
+
}
+

+
/// An error reported by object types.
+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    #[error(transparent)]
+
    Directory(#[from] directory::error::Directory),
+

+
    /// An error occurred during a git operation.
+
    #[error(transparent)]
+
    Git(#[from] git::Error),
+

+
    /// Trying to find a file path which could not be found.
+
    #[error("the path '{0}' was not found")]
+
    PathNotFound(PathBuf),
+
}
added radicle-surf/src/source/object/blob.rs
@@ -0,0 +1,218 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
//! 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::{
+
    git::Repository,
+
    source::{
+
        commit,
+
        object::{Error, Info, ObjectType},
+
        revision::Revision,
+
    },
+
};
+

+
#[cfg(feature = "syntax")]
+
use crate::syntax;
+

+
/// File data abstraction.
+
pub struct Blob {
+
    /// Actual content of the file, if the content is ASCII.
+
    pub content: BlobContent,
+
    /// Extra info for the file.
+
    pub info: Info,
+
    /// Absolute path to the object from the root of the repo.
+
    pub path: PathBuf,
+
}
+

+
impl Blob {
+
    /// Indicates if the content of the [`Blob`] is binary.
+
    #[must_use]
+
    pub fn is_binary(&self) -> bool {
+
        matches!(self.content, BlobContent::Binary(_))
+
    }
+

+
    /// Indicates if the content of the [`Blob`] is HTML.
+
    #[must_use]
+
    pub const fn is_html(&self) -> bool {
+
        matches!(self.content, BlobContent::Html(_))
+
    }
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for Blob {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Blob", 5)?;
+
        state.serialize_field("binary", &self.is_binary())?;
+
        state.serialize_field("html", &self.is_html())?;
+
        state.serialize_field("content", &self.content)?;
+
        state.serialize_field("info", &self.info)?;
+
        state.serialize_field("path", &self.path)?;
+
        state.end()
+
    }
+
}
+

+
/// Variants of blob content.
+
#[derive(PartialEq, Eq)]
+
pub enum BlobContent {
+
    /// Content is plain text and can be passed as a string.
+
    Plain(String),
+
    /// Content is syntax-highlighted HTML.
+
    ///
+
    /// Note that is necessary to enable the `syntax` feature flag for this
+
    /// variant to be constructed. Use `highlighting::blob`, instead of
+
    /// [`blob`] to get highlighted content.
+
    Html(String),
+
    /// Content is binary and needs special treatment.
+
    Binary(Vec<u8>),
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for BlobContent {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        match self {
+
            Self::Plain(content) | Self::Html(content) => serializer.serialize_str(content),
+
            Self::Binary(bytes) => {
+
                let encoded = base64::encode(bytes);
+
                serializer.serialize_str(&encoded)
+
            },
+
        }
+
    }
+
}
+

+
/// Returns the [`Blob`] for a file at `revision` under `path`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if the project doesn't exist or a surf interaction
+
/// fails.
+
pub fn blob<P>(repo: &Repository, maybe_revision: Option<Revision>, path: &P) -> Result<Blob, Error>
+
where
+
    P: AsRef<Path>,
+
{
+
    make_blob(repo, maybe_revision, path, content)
+
}
+

+
fn make_blob<P, C>(
+
    repo: &Repository,
+
    maybe_revision: Option<Revision>,
+
    path: &P,
+
    content: C,
+
) -> Result<Blob, Error>
+
where
+
    P: AsRef<Path>,
+
    C: FnOnce(&[u8]) -> BlobContent,
+
{
+
    let path = path.as_ref();
+
    let revision = maybe_revision.unwrap();
+
    let root = repo.root_dir(&revision)?;
+

+
    let file = root
+
        .find_file(&path, repo)?
+
        .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?;
+

+
    let last_commit = repo
+
        .last_commit(path, &revision)?
+
        .map(|c| commit::Header::from(&c));
+
    // TODO: fuck this
+
    let name = path
+
        .file_name()
+
        .unwrap()
+
        .to_os_string()
+
        .into_string()
+
        .ok()
+
        .unwrap();
+

+
    let file_content = repo.file_content(file)?;
+
    let content = content(file_content.as_bytes());
+

+
    Ok(Blob {
+
        content,
+
        info: Info {
+
            name,
+
            object_type: ObjectType::Blob,
+
            last_commit,
+
        },
+
        path: path.to_path_buf(),
+
    })
+
}
+

+
/// Return a [`BlobContent`] given a byte slice.
+
fn content(content: &[u8]) -> BlobContent {
+
    match str::from_utf8(content) {
+
        Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
+
        Err(_) => BlobContent::Binary(content.to_owned()),
+
    }
+
}
+

+
#[cfg(feature = "syntax")]
+
pub mod highlighting {
+
    use super::*;
+

+
    /// Returns the [`Blob`] for a file at `revision` under `path`.
+
    ///
+
    /// # Errors
+
    ///
+
    /// Will return [`Error`] if the project doesn't exist or a surf interaction
+
    /// fails.
+
    pub fn blob<P>(
+
        browser: &mut Browser,
+
        maybe_revision: Option<Revision<P>>,
+
        path: &str,
+
        theme: Option<&str>,
+
    ) -> Result<Blob, Error>
+
    where
+
        P: ToString,
+
    {
+
        make_blob(browser, maybe_revision, path, |contents| {
+
            content(path, contents, theme)
+
        })
+
    }
+

+
    /// Return a [`BlobContent`] given a file path, content and theme. Attempts
+
    /// to perform syntax highlighting when the theme is `Some`.
+
    fn content(path: &str, content: &[u8], theme_name: Option<&str>) -> BlobContent {
+
        let content = match str::from_utf8(content) {
+
            Ok(content) => content,
+
            Err(_) => return BlobContent::Binary(content.to_owned()),
+
        };
+

+
        match theme_name {
+
            None => BlobContent::Plain(content.to_owned()),
+
            Some(theme) => syntax::highlight(path, content, theme)
+
                .map_or_else(|| BlobContent::Plain(content.to_owned()), BlobContent::Html),
+
        }
+
    }
+
}
added radicle-surf/src/source/object/tree.rs
@@ -0,0 +1,172 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
//! Represents git object type 'tree', i.e. like directory entries in Unix.
+
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+

+
use std::path::{Path, PathBuf};
+

+
use git_ref_format::refname;
+
#[cfg(feature = "serialize")]
+
use serde::{
+
    ser::{SerializeStruct as _, Serializer},
+
    Serialize,
+
};
+

+
use crate::{
+
    file_system::directory,
+
    git::Repository,
+
    source::{
+
        commit,
+
        object::{Error, Info, ObjectType},
+
        revision::Revision,
+
    },
+
};
+

+
/// Result of a directory listing, carries other trees and blobs.
+
pub struct Tree {
+
    /// Absolute path to the tree object from the repo root.
+
    pub path: PathBuf,
+
    /// Entries listed in that tree result.
+
    pub entries: Vec<TreeEntry>,
+
    /// Extra info for the tree object.
+
    pub info: Info,
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for Tree {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Tree", 3)?;
+
        state.serialize_field("path", &self.path)?;
+
        state.serialize_field("entries", &self.entries)?;
+
        state.serialize_field("info", &self.info)?;
+
        state.end()
+
    }
+
}
+

+
// TODO(xla): Ensure correct by construction.
+
/// Entry in a Tree result.
+
pub struct TreeEntry {
+
    /// Extra info for the entry.
+
    pub info: Info,
+
    /// Absolute path to the object from the root of the repo.
+
    pub path: PathBuf,
+
}
+

+
#[cfg(feature = "serialize")]
+
impl Serialize for TreeEntry {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Tree", 2)?;
+
        state.serialize_field("path", &self.path)?;
+
        state.serialize_field("info", &self.info)?;
+
        state.end()
+
    }
+
}
+

+
/// Retrieve the [`Tree`] for the given `revision` and directory `prefix`.
+
///
+
/// # Errors
+
///
+
/// Will return [`Error`] if any of the surf interactions fail.
+
pub fn tree<P>(
+
    repo: &Repository,
+
    maybe_revision: Option<Revision>,
+
    maybe_prefix: Option<&P>,
+
) -> Result<Tree, Error>
+
where
+
    P: AsRef<Path>,
+
{
+
    let maybe_prefix = maybe_prefix.map(|p| p.as_ref());
+
    let rev = match maybe_revision {
+
        Some(r) => r,
+
        None => Revision::Branch {
+
            name: refname!("main"),
+
            remote: None,
+
        },
+
    };
+

+
    let prefix_dir = match maybe_prefix {
+
        None => repo.root_dir(&rev)?,
+
        Some(path) => repo
+
            .root_dir(&rev)?
+
            .find_directory(path, repo)?
+
            .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
+
    };
+

+
    let mut entries = prefix_dir
+
        .entries(repo)?
+
        .entries()
+
        .fold(Vec::new(), |mut entries, entry| {
+
            let entry_path = match maybe_prefix {
+
                Some(path) => {
+
                    let mut path = path.to_path_buf();
+
                    path.push(entry.name());
+
                    path
+
                },
+
                None => PathBuf::new(),
+
            };
+

+
            let info = Info {
+
                name: entry.name().clone(),
+
                object_type: match entry {
+
                    directory::Entry::Directory(_) => ObjectType::Tree,
+
                    directory::Entry::File { .. } => ObjectType::Blob,
+
                },
+
                last_commit: None,
+
            };
+

+
            entries.push(TreeEntry {
+
                info,
+
                path: entry_path,
+
            });
+
            entries
+
        });
+

+
    // We want to ensure that in the response Tree entries come first. `Ord` being
+
    // derived on the enum ensures Variant declaration order.
+
    //
+
    // https://doc.rust-lang.org/std/cmp/trait.Ord.html#derivable
+
    entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
+

+
    let last_commit = if maybe_prefix.is_none() {
+
        let history = repo.history(&rev)?;
+
        Some(commit::Header::from(history.head()))
+
    } else {
+
        None
+
    };
+
    let name = match maybe_prefix {
+
        None => "".into(),
+
        Some(path) => path.file_name().unwrap().to_str().unwrap().to_string(),
+
    };
+
    let info = Info {
+
        name,
+
        object_type: ObjectType::Tree,
+
        last_commit,
+
    };
+

+
    Ok(Tree {
+
        path: maybe_prefix.map_or(PathBuf::new(), |path| path.to_path_buf()),
+
        entries,
+
        info,
+
    })
+
}
added radicle-surf/src/source/person.rs
@@ -0,0 +1,32 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
//! Represents 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))]
+
#[derive(Clone, Debug)]
+
pub struct Person {
+
    /// Name part of the commit signature.
+
    pub name: String,
+
    /// Email part of the commit signature.
+
    pub email: String,
+
}
added radicle-surf/src/source/revision.rs
@@ -0,0 +1,189 @@
+
// This file is part of radicle-surf
+
// <https://github.com/radicle-dev/radicle-surf>
+
//
+
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
+
//
+
// This program is free software: you can redistribute it and/or modify
+
// it under the terms of the GNU General Public License version 3 or
+
// later as published by the Free Software Foundation.
+
//
+
// This program is distributed in the hope that it will be useful,
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+
// GNU General Public License for more details.
+
//
+
// You should have received a copy of the GNU General Public License
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
+

+
//! Represents revisions
+

+
use std::collections::BTreeSet;
+

+
use git_ref_format::{lit, refspec, Qualified, RefString};
+

+
#[cfg(feature = "serialize")]
+
use serde::{Deserialize, Serialize};
+

+
use radicle_git_ext::Oid;
+

+
use crate::git::{self, Error, Glob, Repository};
+

+
/// Types of a peer.
+
pub enum Category<P, U> {
+
    /// Local peer.
+
    Local {
+
        /// Peer Id
+
        peer_id: P,
+
        /// User name
+
        user: U,
+
    },
+
    /// Remote peer.
+
    Remote {
+
        /// Peer Id
+
        peer_id: P,
+
        /// User name
+
        user: U,
+
    },
+
}
+

+
/// A revision selector for a `Browser`.
+
#[cfg_attr(
+
    feature = "serialize",
+
    derive(Deserialize, Serialize),
+
    serde(rename_all = "camelCase", tag = "type")
+
)]
+
#[derive(Debug, Clone)]
+
pub enum Revision {
+
    /// Select a tag under the name provided.
+
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
+
    Tag {
+
        /// Name of the tag.
+
        name: RefString,
+
    },
+
    /// Select a branch under the name provided.
+
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
+
    Branch {
+
        /// Name of the branch.
+
        name: RefString,
+
        /// The remote peer, if specified.
+
        remote: Option<RefString>,
+
    },
+
    /// Select a SHA1 under the name provided.
+
    #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
+
    Sha {
+
        /// The SHA1 value.
+
        sha: Oid,
+
    },
+
}
+

+
impl git::Revision for &Revision {
+
    type Error = git2::Error;
+

+
    fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+
        match self {
+
            Revision::Tag { name } => match name.qualified() {
+
                None => Qualified::from(lit::refs_tags(name)).object_id(repo),
+
                Some(name) => name.object_id(repo),
+
            },
+
            Revision::Branch { name, remote } => match remote {
+
                Some(remote) => {
+
                    Qualified::from(lit::refs_remotes(remote.join(name))).object_id(repo)
+
                },
+
                None => git::Branch::local(name).refname().object_id(repo),
+
            },
+
            Revision::Sha { sha } => Ok(*sha),
+
        }
+
    }
+
}
+

+
/// Bundled response to retrieve both branches and tags for
+
/// a user's repo.
+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub struct Revisions<P, U> {
+
    /// The peer peer_id for the user.
+
    pub peer_id: P,
+
    /// The user who owns these revisions.
+
    pub user: U,
+
    /// List of branch reference names.
+
    pub branches: BTreeSet<RefString>,
+
    /// List of tag reference names.
+
    pub tags: BTreeSet<RefString>,
+
}
+

+
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
+
/// remote branches.
+
///
+
/// If there are no branches then this returns `None`.
+
///
+
/// # Errors
+
///
+
///   * If we cannot get the branches from the `Browser`
+
pub fn remote<U>(
+
    repo: &Repository,
+
    peer_id: RefString,
+
    user: U,
+
) -> Result<Revisions<RefString, U>, Error> {
+
    let pattern = peer_id.to_pattern(refspec::pattern!("*"));
+
    let branches = repo
+
        .branch_names(Glob::remotes(pattern))?
+
        .map(|name| name.map(RefString::from))
+
        .collect::<Result<_, _>>()?;
+
    Ok(Revisions {
+
        peer_id,
+
        user,
+
        branches,
+
        // TODO(rudolfs): implement remote peer tags once we decide how
+
        // https://radicle.community/t/git-tags/214
+
        tags: BTreeSet::new(),
+
    })
+
}
+

+
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
+
/// local branches.
+
///
+
/// If there are no branches then this returns `None`.
+
///
+
/// # Errors
+
///
+
///   * If we cannot get the branches from the `Browser`
+
pub fn local<U>(
+
    repo: &Repository,
+
    peer_id: RefString,
+
    user: U,
+
) -> Result<Revisions<RefString, U>, Error> {
+
    let branches = repo
+
        .branch_names(Glob::all_heads())?
+
        .map(|name| name.map(RefString::from))
+
        .collect::<Result<_, _>>()?;
+
    let tags = repo
+
        .tag_names()?
+
        .map(|name| name.map(RefString::from))
+
        .collect::<Result<_, _>>()?;
+
    Ok(Revisions {
+
        peer_id,
+
        user,
+
        branches,
+
        tags,
+
    })
+
}
+

+
/// Provide the [`Revisions`] of a peer.
+
///
+
/// If the peer is [`Category::Local`], meaning that is the current person doing
+
/// the browsing and no remote is set for the reference.
+
///
+
/// Othewise, the peer is [`Category::Remote`], meaning that we are looking into
+
/// a remote part of a reference.
+
///
+
/// # Errors
+
///
+
///   * If we cannot get the branches from the `Browser`
+
pub fn revisions<U>(
+
    repo: &Repository,
+
    peer: Category<RefString, U>,
+
) -> Result<Revisions<RefString, U>, Error> {
+
    match peer {
+
        Category::Local { peer_id, user } => local(repo, peer_id, user),
+
        Category::Remote { peer_id, user } => remote(repo, peer_id, user),
+
    }
+
}