diff --git a/radicle-crypto/src/lib.rs b/radicle-crypto/src/lib.rs index abe51c57..c6ff9b0a 100644 --- a/radicle-crypto/src/lib.rs +++ b/radicle-crypto/src/lib.rs @@ -19,7 +19,7 @@ mod cyphernet; pub use self::cyphernet::Ed25519; /// Verified (used as type witness). -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] pub struct Verified; /// Unverified (used as type witness). #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index f3a96e70..7c405af7 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -67,6 +67,10 @@ pub enum Error { /// Git2 error. #[error(transparent)] Git2(#[from] radicle::git::raw::Error), + + /// Storage refs error. + #[error(transparent)] + StorageRef(#[from] radicle::storage::refs::Error), } impl Error { diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 62f22a06..11665f4a 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -13,8 +13,10 @@ use tower_http::set_header::SetResponseHeaderLayer; use radicle::cob::issue::Issues; use radicle::git::raw::BranchType; use radicle::identity::{Doc, Id}; +use radicle::node::NodeId; use radicle::storage::{Oid, ReadRepository, WriteRepository, WriteStorage}; use radicle_surf::git::History; +use radicle_surf::Revision::Sha; use crate::api::axum_extra::{Path, Query}; use crate::api::error::Error; @@ -39,6 +41,10 @@ pub fn router(ctx: Context) -> Router { ), ) .route("/projects/:project/tree/:sha/*path", get(tree_handler)) + .route("/projects/:project/remotes", get(remotes_handler)) + .route("/projects/:project/remotes/:peer", get(remote_handler)) + .route("/projects/:project/blob/:sha/*path", get(blob_handler)) + .route("/projects/:project/readme/:sha", get(readme_handler)) .layer(Extension(ctx)) } @@ -183,7 +189,7 @@ async fn commit_handler( let repo = storage.repository(project)?; let commit = radicle_surf::commit(&repo.raw().into(), sha)?; - Ok::<_, Error>(Json(json!(commit))) + Ok::<_, Error>(Json(commit)) } /// Get project activity for the past year. @@ -221,11 +227,7 @@ async fn tree_handler( let path = path.strip_prefix('/').ok_or(Error::NotFound)?.to_string(); let storage = &ctx.profile.storage; let repo = storage.repository(project)?; - let tree = radicle_surf::object::tree( - &repo.raw().into(), - Some(radicle_surf::Revision::Sha { sha }), - Some(path), - )?; + let tree = radicle_surf::object::tree(&repo.raw().into(), Some(Sha { sha }), Some(path))?; let response = json!({ "path": &tree.path, "entries": &tree.entries, @@ -236,6 +238,77 @@ async fn tree_handler( Ok::<_, Error>(Json(response)) } +/// Get all project remotes. +/// `GET /projects/:project/remotes` +async fn remotes_handler( + Extension(ctx): Extension, + Path(project): Path, +) -> impl IntoResponse { + let storage = &ctx.profile.storage; + let repo = storage.repository(project)?; + let remotes = repo + .remotes()? + .filter_map(|r| r.map(|r| r.1).ok()) + .collect::>(); + + Ok::<_, Error>(Json(remotes)) +} + +/// Get project remote. +/// `GET /projects/:project/remotes/:peer` +async fn remote_handler( + Extension(ctx): Extension, + Path((project, node_id)): Path<(Id, NodeId)>, +) -> impl IntoResponse { + let storage = &ctx.profile.storage; + let repo = storage.repository(project)?; + let remote = repo.remote(&node_id)?; + + Ok::<_, Error>(Json(remote)) +} + +/// Get project source file. +/// `GET /projects/:project/blob/:sha/*path` +async fn blob_handler( + Extension(ctx): Extension, + Path((project, sha, path)): Path<(Id, Oid, String)>, +) -> impl IntoResponse { + let path = path.strip_prefix('/').ok_or(Error::NotFound)?; + let storage = &ctx.profile.storage; + let repo = storage.repository(project)?; + let blob = radicle_surf::blob::blob(&repo.raw().into(), Some(Sha { sha }), path)?; + + Ok::<_, Error>(Json(blob)) +} + +/// Get project readme. +/// `GET /projects/:project/readme/:sha` +async fn readme_handler( + Extension(ctx): Extension, + Path((project, sha)): Path<(Id, Oid)>, +) -> impl IntoResponse { + let storage = &ctx.profile.storage; + let repo = storage.repository(project)?; + let paths = &[ + "README", + "README.md", + "README.markdown", + "README.txt", + "README.rst", + "Readme.md", + ]; + + for path in paths { + if let Ok(blob) = radicle_surf::blob::blob(&repo.raw().into(), Some(Sha { sha }), path) { + return Ok::<_, Error>(Json(blob)); + } + } + + Err(radicle_surf::object::Error::PathNotFound( + radicle_surf::file_system::Path::try_from("README").unwrap(), + ))? +} + #[derive(Serialize)] struct Stats { branches: usize, diff --git a/radicle/src/storage.rs b/radicle/src/storage.rs index 059af5a9..a8bd6a75 100644 --- a/radicle/src/storage.rs +++ b/radicle/src/storage.rs @@ -6,6 +6,7 @@ use std::ops::Deref; use std::path::Path; use std::{fmt, io}; +use serde::Serialize; use thiserror::Error; use crypto::{PublicKey, Signer, Unverified, Verified}; @@ -184,11 +185,12 @@ impl From> for HashMap { } /// A project remote. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Remote { /// ID of remote. pub id: PublicKey, /// Git references published under this remote, and their hashes. + #[serde(flatten)] pub refs: SignedRefs, /// Whether this remote is a delegate for the project. pub delegate: bool, diff --git a/radicle/src/storage/refs.rs b/radicle/src/storage/refs.rs index ee5aa54b..13150c16 100644 --- a/radicle/src/storage/refs.rs +++ b/radicle/src/storage/refs.rs @@ -8,6 +8,7 @@ use std::path::Path; use std::str::FromStr; use crypto::{PublicKey, Signature, Signer, SignerError, Unverified, Verified}; +use serde::Serialize; use thiserror::Error; use crate::git; @@ -62,7 +63,7 @@ impl Error { } /// The published state of a local repository. -#[derive(Default, Clone, Debug, PartialEq, Eq)] +#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] pub struct Refs(BTreeMap); impl Refs { @@ -197,11 +198,12 @@ impl DerefMut for Refs { /// /// The type parameter keeps track of whether the signature was [`Verified`] or /// [`Unverified`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SignedRefs { pub refs: Refs, + #[serde(skip)] pub signature: Signature, - + #[serde(skip)] _verified: PhantomData, }