httpd: Dogfood the new radicle-surf API
Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
parent
f33e79f14b
commit
4ea67250ea
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use radicle::Profile;
|
|||
mod auth;
|
||||
mod axum_extra;
|
||||
mod error;
|
||||
mod json;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
mod v1;
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> = 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::<Vec<_>>();
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
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<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.
|
||||
|
|
@ -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<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)]
|
||||
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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue