Migrate `/projects`: `history` and `tree` handlers
Signed-off-by: xphoniex <dj.2dixx@gmail.com>
This commit is contained in:
parent
dea7082a6f
commit
84c3cf4cf1
|
|
@ -52,9 +52,21 @@ pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
GitProject(#[from] radicle::storage::git::ProjectError),
|
GitProject(#[from] radicle::storage::git::ProjectError),
|
||||||
|
|
||||||
/// Surf error.
|
/// Surf commit error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SurfCommit(#[from] radicle_surf::commit::Error),
|
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 {
|
impl Error {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use axum::handler::Handler;
|
use axum::handler::Handler;
|
||||||
use axum::http::{header, HeaderValue};
|
use axum::http::{header, HeaderValue};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{Extension, Json, Router};
|
use axum::{Extension, Json, Router};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tower_http::set_header::SetResponseHeaderLayer;
|
use tower_http::set_header::SetResponseHeaderLayer;
|
||||||
|
|
||||||
use radicle::cob::issue::Issues;
|
use radicle::cob::issue::Issues;
|
||||||
|
use radicle::git::raw::BranchType;
|
||||||
use radicle::identity::{Doc, Id};
|
use radicle::identity::{Doc, Id};
|
||||||
use radicle::storage::{Oid, ReadRepository, WriteRepository, WriteStorage};
|
use radicle::storage::{Oid, ReadRepository, WriteRepository, WriteStorage};
|
||||||
use radicle_surf::git::History;
|
use radicle_surf::git::History;
|
||||||
|
|
@ -23,6 +27,7 @@ pub fn router(ctx: Context) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/projects", get(project_root_handler))
|
.route("/projects", get(project_root_handler))
|
||||||
.route("/projects/:project", get(project_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/commits/:sha", get(commit_handler))
|
||||||
.route(
|
.route(
|
||||||
"/projects/:project/activity",
|
"/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))
|
.layer(Extension(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +88,91 @@ async fn project_handler(
|
||||||
Ok::<_, Error>(Json(info))
|
Ok::<_, Error>(Json(info))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct CommitsQueryString {
|
||||||
|
pub parent: Option<String>,
|
||||||
|
pub since: Option<i64>,
|
||||||
|
pub until: Option<i64>,
|
||||||
|
pub page: Option<usize>,
|
||||||
|
pub per_page: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get project commit range.
|
||||||
|
/// `GET /projects/:project/commits?since=<sha>`
|
||||||
|
async fn history_handler(
|
||||||
|
Extension(ctx): Extension<Context>,
|
||||||
|
Path(project): Path<Id>,
|
||||||
|
Query(qs): Query<CommitsQueryString>,
|
||||||
|
) -> 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::<Vec<_>>();
|
||||||
|
|
||||||
|
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 project commit.
|
||||||
/// `GET /projects/:project/commits/:sha`
|
/// `GET /projects/:project/commits/:sha`
|
||||||
async fn commit_handler(
|
async fn commit_handler(
|
||||||
|
|
@ -106,8 +197,7 @@ async fn activity_handler(
|
||||||
let storage = &ctx.profile.storage;
|
let storage = &ctx.profile.storage;
|
||||||
let repo = storage.repository(project)?;
|
let repo = storage.repository(project)?;
|
||||||
let (_, head) = repo.head()?;
|
let (_, head) = repo.head()?;
|
||||||
let timestamps = History::new(repo.raw().into(), head)
|
let timestamps = History::new(repo.raw().into(), head)?
|
||||||
.unwrap()
|
|
||||||
.filter_map(|a| {
|
.filter_map(|a| {
|
||||||
if let Ok(a) = a {
|
if let Ok(a) = a {
|
||||||
let seconds = a.committer.time.seconds();
|
let seconds = a.committer.time.seconds();
|
||||||
|
|
@ -121,3 +211,56 @@ async fn activity_handler(
|
||||||
|
|
||||||
Ok::<_, Error>((StatusCode::OK, Json(json!({ "activity": timestamps }))))
|
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<Context>,
|
||||||
|
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<R: WriteRepository>(repo: &R) -> Result<Stats, Error> {
|
||||||
|
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::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
Ok(Stats {
|
||||||
|
branches,
|
||||||
|
commits,
|
||||||
|
contributors: contributors.len(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue