From 84c3cf4cf13915508bbcfa333ccec30adda2b8a8 Mon Sep 17 00:00:00 2001 From: xphoniex Date: Tue, 22 Nov 2022 13:16:43 +0000 Subject: [PATCH] Migrate `/projects`: `history` and `tree` handlers Signed-off-by: xphoniex --- radicle-httpd/src/api/error.rs | 14 ++- radicle-httpd/src/api/v1/projects.rs | 147 ++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 3 deletions(-) diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index 7100b1a5..f3a96e70 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -52,9 +52,21 @@ pub enum Error { #[error(transparent)] GitProject(#[from] radicle::storage::git::ProjectError), - /// Surf error. + /// Surf commit error. #[error(transparent)] SurfCommit(#[from] radicle_surf::commit::Error), + + /// Surf object error. + #[error(transparent)] + SurfObject(#[from] radicle_surf::object::Error), + + /// Surf git error. + #[error(transparent)] + SurfGit(#[from] radicle_surf::git::Error), + + /// Git2 error. + #[error(transparent)] + Git2(#[from] radicle::git::raw::Error), } impl Error { diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 1ab12125..62f22a06 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -1,13 +1,17 @@ +use std::collections::BTreeSet; + use axum::handler::Handler; use axum::http::{header, HeaderValue}; use axum::response::IntoResponse; use axum::routing::get; use axum::{Extension, Json, Router}; use hyper::StatusCode; +use serde::{Deserialize, Serialize}; use serde_json::json; use tower_http::set_header::SetResponseHeaderLayer; use radicle::cob::issue::Issues; +use radicle::git::raw::BranchType; use radicle::identity::{Doc, Id}; use radicle::storage::{Oid, ReadRepository, WriteRepository, WriteStorage}; use radicle_surf::git::History; @@ -23,6 +27,7 @@ pub fn router(ctx: Context) -> Router { Router::new() .route("/projects", get(project_root_handler)) .route("/projects/:project", get(project_handler)) + .route("/projects/:project/commits", get(history_handler)) .route("/projects/:project/commits/:sha", get(commit_handler)) .route( "/projects/:project/activity", @@ -33,6 +38,7 @@ pub fn router(ctx: Context) -> Router { )), ), ) + .route("/projects/:project/tree/:sha/*path", get(tree_handler)) .layer(Extension(ctx)) } @@ -82,6 +88,91 @@ async fn project_handler( Ok::<_, Error>(Json(info)) } +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct CommitsQueryString { + pub parent: Option, + pub since: Option, + pub until: Option, + pub page: Option, + pub per_page: Option, +} + +/// Get project commit range. +/// `GET /projects/:project/commits?since=` +async fn history_handler( + Extension(ctx): Extension, + Path(project): Path, + Query(qs): Query, +) -> impl IntoResponse { + let CommitsQueryString { + since, + until, + parent, + page, + per_page, + } = qs; + + let (sha, fallback_to_head) = match parent { + Some(commit) => (commit, false), + None => { + let info = ctx.project_info(project)?; + + (info.head.to_string(), true) + } + }; + + let storage = &ctx.profile.storage; + let repo = storage.repository(project)?; + + // If a pagination is defined, we do not want to paginate the commits, and we return all of them on the first page. + let page = page.unwrap_or(0); + let per_page = if per_page.is_none() && (since.is_some() || until.is_some()) { + usize::MAX + } else { + per_page.unwrap_or(30) + }; + + let headers = History::new(repo.raw().into(), sha.as_str())? + .filter(|q| { + if let Ok(q) = q { + if let (Some(since), Some(until)) = (since, until) { + q.committer.time.seconds() >= since && q.committer.time.seconds() < until + } else if let Some(since) = since { + q.committer.time.seconds() >= since + } else if let Some(until) = until { + q.committer.time.seconds() < until + } else { + // If neither `since` nor `until` are specified, we include the commit. + true + } + } else { + false + } + }) + .skip(page * per_page) + .take(per_page) + .filter_map(|commit| { + if let Ok(commit) = commit { + radicle_surf::commit(&repo.raw().into(), commit.id).ok() + } else { + None + } + }) + .collect::>(); + + let response = json!({ + "headers": headers, + "stats": stats(&repo)?, + }); + + if fallback_to_head { + return Ok::<_, Error>((StatusCode::FOUND, Json(response))); + } + + Ok::<_, Error>((StatusCode::OK, Json(response))) +} + /// Get project commit. /// `GET /projects/:project/commits/:sha` async fn commit_handler( @@ -106,8 +197,7 @@ async fn activity_handler( let storage = &ctx.profile.storage; let repo = storage.repository(project)?; let (_, head) = repo.head()?; - let timestamps = History::new(repo.raw().into(), head) - .unwrap() + let timestamps = History::new(repo.raw().into(), head)? .filter_map(|a| { if let Ok(a) = a { let seconds = a.committer.time.seconds(); @@ -121,3 +211,56 @@ async fn activity_handler( Ok::<_, Error>((StatusCode::OK, Json(json!({ "activity": timestamps })))) } + +/// Get project source tree. +/// `GET /projects/:project/tree/:sha/*path` +async fn tree_handler( + Extension(ctx): Extension, + Path((project, sha, path)): Path<(Id, Oid, String)>, +) -> impl IntoResponse { + 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 response = json!({ + "path": &tree.path, + "entries": &tree.entries, + "info": &tree.info, + "stats": stats(&repo)?, + }); + + Ok::<_, Error>(Json(response)) +} + +#[derive(Serialize)] +struct Stats { + branches: usize, + commits: usize, + contributors: usize, +} + +fn stats(repo: &R) -> Result { + let branches = repo.raw().branches(Some(BranchType::Local))?.count(); + let (_, head) = repo.head()?; + let mut commits = 0; + let contributors = History::new(repo.raw().into(), 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(), + }) +}