diff --git a/Cargo.lock b/Cargo.lock index 9683b657..c024c800 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1067,7 +1067,7 @@ dependencies = [ [[package]] name = "git-ref-format" version = "0.1.0" -source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09" +source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3" dependencies = [ "git-ref-format-core", "git-ref-format-macro", @@ -1076,7 +1076,7 @@ dependencies = [ [[package]] name = "git-ref-format-core" version = "0.1.0" -source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09" +source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3" dependencies = [ "serde", "thiserror", @@ -1085,7 +1085,7 @@ dependencies = [ [[package]] name = "git-ref-format-macro" version = "0.1.0" -source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09" +source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3" dependencies = [ "git-ref-format-core", "proc-macro-error", @@ -2229,7 +2229,7 @@ dependencies = [ [[package]] name = "radicle-git-ext" version = "0.2.0" -source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09" +source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3" dependencies = [ "git-ref-format", "git2", @@ -2325,12 +2325,12 @@ dependencies = [ [[package]] name = "radicle-std-ext" version = "0.1.0" -source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09" +source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3" [[package]] name = "radicle-surf" version = "0.8.0" -source = "git+https://github.com/radicle-dev/radicle-git?rev=79a94721366490053e2d8ac1c1afa14fb0c25f09#79a94721366490053e2d8ac1c1afa14fb0c25f09" +source = "git+https://github.com/radicle-dev/radicle-git?rev=016ece66474edf45d2d304141515152e6f7b37a3#016ece66474edf45d2d304141515152e6f7b37a3" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index e832a785..ab52e5de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,8 +43,8 @@ version = "0.1.0" [patch.crates-io.radicle-git-ext] git = "https://github.com/radicle-dev/radicle-git" -rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09" +rev = "016ece66474edf45d2d304141515152e6f7b37a3" [patch.crates-io.git-ref-format] git = "https://github.com/radicle-dev/radicle-git" -rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09" +rev = "016ece66474edf45d2d304141515152e6f7b37a3" diff --git a/radicle-httpd/Cargo.toml b/radicle-httpd/Cargo.toml index e0b0d58b..4171aace 100644 --- a/radicle-httpd/Cargo.toml +++ b/radicle-httpd/Cargo.toml @@ -42,7 +42,7 @@ version = "0.2.0" [dependencies.radicle-surf] git = "https://github.com/radicle-dev/radicle-git" features = ["serde"] -rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09" +rev = "016ece66474edf45d2d304141515152e6f7b37a3" [dev-dependencies] hyper = { version = "0.14.17", default-features = false, features = ["client"] } diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index 573d2776..3bb9a668 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -20,6 +20,7 @@ use radicle::Profile; mod auth; mod axum_extra; mod error; +mod json; #[cfg(test)] mod test; mod v1; diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index d969fc5b..d2b1dd08 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -38,17 +38,13 @@ pub enum Error { #[error(transparent)] GitProject(#[from] radicle::storage::git::ProjectError), - /// Surf commit error. + /// Surf directory error. #[error(transparent)] - SurfCommit(#[from] radicle_surf::source::commit::Error), + SurfDir(#[from] radicle_surf::fs::error::Directory), - /// Surf object error. + /// Surf error. #[error(transparent)] - SurfObject(#[from] radicle_surf::source::object::Error), - - /// Surf git error. - #[error(transparent)] - SurfGit(#[from] radicle_surf::git::Error), + Surf(#[from] radicle_surf::Error), /// Git2 error. #[error(transparent)] diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs new file mode 100644 index 00000000..d503d3e4 --- /dev/null +++ b/radicle-httpd/src/api/json.rs @@ -0,0 +1,68 @@ +//! Utilities for building JSON responses of our API. + +use radicle_surf::{ + object::{Blob, Tree}, + Commit, Stats, +}; +use serde_json::json; + +/// Returns JSON of a commit. +pub(crate) fn commit(commit: &Commit) -> serde_json::Value { + json!({ + "sha1": commit.id, + "author": { + "name": commit.author.name, + "email": commit.author.email + }, + "summary": commit.summary, + "description": commit.description(), + "committer": { + "name": commit.committer.name, + "email": commit.committer.email + }, + "committerTime": commit.committer.time.seconds() + }) +} + +/// Returns JSON for a blob with a given `path`. +pub(crate) fn blob(blob: &Blob, path: &str) -> serde_json::Value { + json!({ + "binary": blob.is_binary(), + "content": blob.content(), + "name": name_in_path(path), + "path": path, + "lastCommit": commit(blob.commit()) + }) +} + +/// Returns JSON for a tree with a given `path` and `stats`. +pub(crate) fn tree(tree: &Tree, path: &str, stats: &Stats) -> serde_json::Value { + let prefix = std::path::Path::new(path); + let entries = tree + .entries() + .iter() + .map(|entry| { + json!({ + "path": prefix.join(entry.name()), + "name": entry.name(), + "lastCommit": serde_json::Value::Null, + "kind": if entry.is_tree() { "tree" } else { "blob" }, + }) + }) + .collect::>(); + json!({ + "entries": &entries, + "lastCommit": commit(tree.commit()), + "name": name_in_path(path), + "path": path, + "stats": stats, + }) +} + +/// Returns the name part of a path string. +fn name_in_path(path: &str) -> &str { + match path.rsplit('/').next() { + Some(name) => name, + None => path, + } +} diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 176acf10..36cc7f91 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashSet}; +use std::collections::BTreeMap; use axum::handler::Handler; use axum::http::{header, HeaderValue}; @@ -13,20 +13,15 @@ use tower_http::set_header::SetResponseHeaderLayer; use radicle::cob::issue::Issues; use radicle::cob::thread::{self, CommentId}; use radicle::cob::Timestamp; -use radicle::git::Oid; use radicle::identity::{Id, PublicKey}; use radicle::node::NodeId; -use radicle::storage::git::paths; -use radicle::storage::{ReadRepository, WriteStorage}; -use radicle_surf::git::{Glob, History, Repository}; -use radicle_surf::source; -use radicle_surf::source::object::blob::Blob; -use radicle_surf::source::object::tree::Tree; +use radicle::storage::{git::paths, ReadRepository, WriteStorage}; +use radicle_surf::{Glob, Oid, Repository}; use crate::api::axum_extra::{Path, Query}; use crate::api::error::Error; use crate::api::project::Info; -use crate::api::{Context, PaginationQuery}; +use crate::api::{self, Context, PaginationQuery}; const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate"; @@ -147,7 +142,7 @@ async fn history_handler( }; let headers = repo - .history(sha)? + .history(&sha)? .filter(|q| { if let Ok(q) = q { if let (Some(since), Some(until)) = (since, until) { @@ -166,14 +161,23 @@ async fn history_handler( }) .skip(page * per_page) .take(per_page) - .filter_map(|commit| { - if let Ok(commit) = commit { - source::commit(&repo, commit.id).ok() - } else { - None - } + .map(|r| { + r.and_then(|c| { + let glob = Glob::all_heads().branches().and(Glob::all_remotes()); + let branches: Vec = repo + .revision_branches(c.id, glob)? + .iter() + .map(|b| b.refname().to_string()) + .collect(); + let diff = repo.diff_commit(c.id)?; + Ok(json!({ + "header": api::json::commit(&c), + "diff": diff, + "branches": branches + })) + }) }) - .collect::>(); + .collect::, _>>()?; let response = json!({ "headers": headers, @@ -195,9 +199,22 @@ async fn commit_handler( ) -> impl IntoResponse { let storage = &ctx.profile.storage; let repo = Repository::open(paths::repository(storage, &project))?; - let commit = source::commit(&repo, sha)?; + let commit = repo.commit(sha)?; - Ok::<_, Error>(Json(commit)) + let diff = repo.diff_commit(commit.id)?; + let glob = Glob::all_heads().branches().and(Glob::all_remotes()); + let branches: Vec = repo + .revision_branches(commit.id, glob)? + .iter() + .map(|b| b.refname().to_string()) + .collect(); + + let response = json!({ + "header": api::json::commit(&commit), + "diff": diff, + "branches": branches + }); + Ok::<_, Error>(Json(response)) } /// Get project activity for the past year. @@ -236,10 +253,9 @@ async fn tree_handler( let path = path.strip_prefix('/').ok_or(Error::NotFound)?.to_string(); let storage = &ctx.profile.storage; let repo = Repository::open(paths::repository(storage, &project))?; - let path = if path.is_empty() { None } else { Some(&path) }; - let tree = Tree::new(&repo, &sha, path)?; - let mut response = json!(tree); - response["stats"] = json!(stats(&repo, sha)?); + let tree = repo.tree(sha, &path)?; + let stats = repo.stats_from(&sha)?; + let response = api::json::tree(&tree, &path, &stats); Ok::<_, Error>(Json(response)) } @@ -313,9 +329,10 @@ async fn blob_handler( let path = path.strip_prefix('/').ok_or(Error::NotFound)?; let storage = &ctx.profile.storage; let repo = Repository::open(paths::repository(storage, &project))?; - let blob = Blob::new(&repo, &sha, &path)?; + let blob = repo.blob(sha, &path)?; + let response = api::json::blob(&blob, path); - Ok::<_, Error>(Json(blob)) + Ok::<_, Error>(Json(response)) } /// Get project readme. @@ -336,8 +353,9 @@ async fn readme_handler( ]; for path in paths { - if let Ok(blob) = Blob::new(&repo, &sha, &path) { - return Ok::<_, Error>(Json(blob)); + if let Ok(blob) = repo.blob(sha, path) { + let response = api::json::blob(&blob, path); + return Ok::<_, Error>(Json(response)); } } @@ -439,35 +457,6 @@ impl<'a> FromIterator<(&'a CommentId, &'a thread::Comment)> for Comments { } } -#[derive(Serialize)] -struct Stats { - branches: usize, - commits: usize, - contributors: usize, -} - -fn stats(repo: &Repository, head: Oid) -> Result { - let branches = repo.branches(Glob::all_heads())?.count(); - - let mut commits = 0; - let contributors = History::new(repo, head)? - .filter_map(|commit| { - commits += 1; - if let Ok(commit) = commit { - Some((commit.author.name, commit.author.email)) - } else { - None - } - }) - .collect::>(); - - Ok(Stats { - branches, - commits, - contributors: contributors.len(), - }) -} - #[cfg(test)] mod routes { use axum::http::StatusCode; @@ -722,17 +711,17 @@ mod routes { response.json().await, json!({ "entries": [ - { - "path": "README", - "name": "README", - "lastCommit": null, - "kind": "blob" - }, { "path": "dir1", "name": "dir1", "lastCommit": null, "kind": "tree" + }, + { + "path": "README", + "name": "README", + "lastCommit": null, + "kind": "blob" } ], "lastCommit": { @@ -778,7 +767,20 @@ mod routes { "kind": "blob" } ], - "lastCommit": null, + "lastCommit": { + "sha1": HEAD, + "author": { + "name": "Alice Liddell", + "email": "alice@radicle.xyz" + }, + "summary": "Add another folder", + "description": "", + "committer": { + "name": "Alice Liddell", + "email": "alice@radicle.xyz" + }, + "committerTime": 1673001014 + }, "name": "dir1", "path": "dir1", "stats": { @@ -866,6 +868,42 @@ mod routes { ); } + #[tokio::test] + async fn test_projects_readme() { + let tmp = tempfile::tempdir().unwrap(); + let app = super::router(test::seed(tmp.path())); + let response = request( + &app, + format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/readme/{HEAD}"), + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.json().await, + json!({ + "binary": false, + "content": "Hello World!\n", + "lastCommit": { + "sha1": HEAD_1, + "author": { + "name": "Alice Liddell", + "email": "alice@radicle.xyz" + }, + "summary": "Initial commit", + "description": "", + "committer": { + "name": "Alice Liddell", + "email": "alice@radicle.xyz" + }, + "committerTime": 1673001014 + }, + "name": "README", + "path": "README" + }) + ); + } + #[tokio::test] async fn test_projects_issues_root() { let tmp = tempfile::tempdir().unwrap();