httpd: Dogfood the new radicle-surf API

Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
Han Xu 2022-12-06 08:23:39 -08:00 committed by Alexis Sellier
parent f33e79f14b
commit 4ea67250ea
No known key found for this signature in database
7 changed files with 183 additions and 80 deletions

12
Cargo.lock generated
View File

@ -1067,7 +1067,7 @@ dependencies = [
[[package]] [[package]]
name = "git-ref-format" name = "git-ref-format"
version = "0.1.0" 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 = [ dependencies = [
"git-ref-format-core", "git-ref-format-core",
"git-ref-format-macro", "git-ref-format-macro",
@ -1076,7 +1076,7 @@ dependencies = [
[[package]] [[package]]
name = "git-ref-format-core" name = "git-ref-format-core"
version = "0.1.0" 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 = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@ -1085,7 +1085,7 @@ dependencies = [
[[package]] [[package]]
name = "git-ref-format-macro" name = "git-ref-format-macro"
version = "0.1.0" 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 = [ dependencies = [
"git-ref-format-core", "git-ref-format-core",
"proc-macro-error", "proc-macro-error",
@ -2229,7 +2229,7 @@ dependencies = [
[[package]] [[package]]
name = "radicle-git-ext" name = "radicle-git-ext"
version = "0.2.0" 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 = [ dependencies = [
"git-ref-format", "git-ref-format",
"git2", "git2",
@ -2325,12 +2325,12 @@ dependencies = [
[[package]] [[package]]
name = "radicle-std-ext" name = "radicle-std-ext"
version = "0.1.0" 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]] [[package]]
name = "radicle-surf" name = "radicle-surf"
version = "0.8.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

@ -43,8 +43,8 @@ version = "0.1.0"
[patch.crates-io.radicle-git-ext] [patch.crates-io.radicle-git-ext]
git = "https://github.com/radicle-dev/radicle-git" git = "https://github.com/radicle-dev/radicle-git"
rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09" rev = "016ece66474edf45d2d304141515152e6f7b37a3"
[patch.crates-io.git-ref-format] [patch.crates-io.git-ref-format]
git = "https://github.com/radicle-dev/radicle-git" git = "https://github.com/radicle-dev/radicle-git"
rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09" rev = "016ece66474edf45d2d304141515152e6f7b37a3"

View File

@ -42,7 +42,7 @@ version = "0.2.0"
[dependencies.radicle-surf] [dependencies.radicle-surf]
git = "https://github.com/radicle-dev/radicle-git" git = "https://github.com/radicle-dev/radicle-git"
features = ["serde"] features = ["serde"]
rev = "79a94721366490053e2d8ac1c1afa14fb0c25f09" rev = "016ece66474edf45d2d304141515152e6f7b37a3"
[dev-dependencies] [dev-dependencies]
hyper = { version = "0.14.17", default-features = false, features = ["client"] } hyper = { version = "0.14.17", default-features = false, features = ["client"] }

View File

@ -20,6 +20,7 @@ use radicle::Profile;
mod auth; mod auth;
mod axum_extra; mod axum_extra;
mod error; mod error;
mod json;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
mod v1; mod v1;

View File

@ -38,17 +38,13 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
GitProject(#[from] radicle::storage::git::ProjectError), GitProject(#[from] radicle::storage::git::ProjectError),
/// Surf commit error. /// Surf directory error.
#[error(transparent)] #[error(transparent)]
SurfCommit(#[from] radicle_surf::source::commit::Error), SurfDir(#[from] radicle_surf::fs::error::Directory),
/// Surf object error. /// Surf error.
#[error(transparent)] #[error(transparent)]
SurfObject(#[from] radicle_surf::source::object::Error), Surf(#[from] radicle_surf::Error),
/// Surf git error.
#[error(transparent)]
SurfGit(#[from] radicle_surf::git::Error),
/// Git2 error. /// Git2 error.
#[error(transparent)] #[error(transparent)]

View File

@ -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::<Vec<_>>();
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,
}
}

View File

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashSet}; use std::collections::BTreeMap;
use axum::handler::Handler; use axum::handler::Handler;
use axum::http::{header, HeaderValue}; use axum::http::{header, HeaderValue};
@ -13,20 +13,15 @@ use tower_http::set_header::SetResponseHeaderLayer;
use radicle::cob::issue::Issues; use radicle::cob::issue::Issues;
use radicle::cob::thread::{self, CommentId}; use radicle::cob::thread::{self, CommentId};
use radicle::cob::Timestamp; use radicle::cob::Timestamp;
use radicle::git::Oid;
use radicle::identity::{Id, PublicKey}; use radicle::identity::{Id, PublicKey};
use radicle::node::NodeId; use radicle::node::NodeId;
use radicle::storage::git::paths; use radicle::storage::{git::paths, ReadRepository, WriteStorage};
use radicle::storage::{ReadRepository, WriteStorage}; use radicle_surf::{Glob, Oid, Repository};
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 crate::api::axum_extra::{Path, Query}; use crate::api::axum_extra::{Path, Query};
use crate::api::error::Error; use crate::api::error::Error;
use crate::api::project::Info; 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"; const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";
@ -147,7 +142,7 @@ async fn history_handler(
}; };
let headers = repo let headers = repo
.history(sha)? .history(&sha)?
.filter(|q| { .filter(|q| {
if let Ok(q) = q { if let Ok(q) = q {
if let (Some(since), Some(until)) = (since, until) { if let (Some(since), Some(until)) = (since, until) {
@ -166,14 +161,23 @@ async fn history_handler(
}) })
.skip(page * per_page) .skip(page * per_page)
.take(per_page) .take(per_page)
.filter_map(|commit| { .map(|r| {
if let Ok(commit) = commit { r.and_then(|c| {
source::commit(&repo, commit.id).ok() let glob = Glob::all_heads().branches().and(Glob::all_remotes());
} else { let branches: Vec<String> = repo
None .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::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let response = json!({ let response = json!({
"headers": headers, "headers": headers,
@ -195,9 +199,22 @@ async fn commit_handler(
) -> impl IntoResponse { ) -> impl IntoResponse {
let storage = &ctx.profile.storage; let storage = &ctx.profile.storage;
let repo = Repository::open(paths::repository(storage, &project))?; 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<String> = 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. /// 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 path = path.strip_prefix('/').ok_or(Error::NotFound)?.to_string();
let storage = &ctx.profile.storage; let storage = &ctx.profile.storage;
let repo = Repository::open(paths::repository(storage, &project))?; let repo = Repository::open(paths::repository(storage, &project))?;
let path = if path.is_empty() { None } else { Some(&path) }; let tree = repo.tree(sha, &path)?;
let tree = Tree::new(&repo, &sha, path)?; let stats = repo.stats_from(&sha)?;
let mut response = json!(tree); let response = api::json::tree(&tree, &path, &stats);
response["stats"] = json!(stats(&repo, sha)?);
Ok::<_, Error>(Json(response)) Ok::<_, Error>(Json(response))
} }
@ -313,9 +329,10 @@ async fn blob_handler(
let path = path.strip_prefix('/').ok_or(Error::NotFound)?; let path = path.strip_prefix('/').ok_or(Error::NotFound)?;
let storage = &ctx.profile.storage; let storage = &ctx.profile.storage;
let repo = Repository::open(paths::repository(storage, &project))?; 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. /// Get project readme.
@ -336,8 +353,9 @@ async fn readme_handler(
]; ];
for path in paths { for path in paths {
if let Ok(blob) = Blob::new(&repo, &sha, &path) { if let Ok(blob) = repo.blob(sha, path) {
return Ok::<_, Error>(Json(blob)); 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<Stats, Error> {
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::<HashSet<_>>();
Ok(Stats {
branches,
commits,
contributors: contributors.len(),
})
}
#[cfg(test)] #[cfg(test)]
mod routes { mod routes {
use axum::http::StatusCode; use axum::http::StatusCode;
@ -722,17 +711,17 @@ mod routes {
response.json().await, response.json().await,
json!({ json!({
"entries": [ "entries": [
{
"path": "README",
"name": "README",
"lastCommit": null,
"kind": "blob"
},
{ {
"path": "dir1", "path": "dir1",
"name": "dir1", "name": "dir1",
"lastCommit": null, "lastCommit": null,
"kind": "tree" "kind": "tree"
},
{
"path": "README",
"name": "README",
"lastCommit": null,
"kind": "blob"
} }
], ],
"lastCommit": { "lastCommit": {
@ -778,7 +767,20 @@ mod routes {
"kind": "blob" "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", "name": "dir1",
"path": "dir1", "path": "dir1",
"stats": { "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] #[tokio::test]
async fn test_projects_issues_root() { async fn test_projects_issues_root() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();