httpd: Add routes for patch ops

Signed-off-by: xphoniex <dj.2dixx@gmail.com>
This commit is contained in:
xphoniex 2023-02-04 13:22:09 +00:00
parent 88ffc02c5e
commit 4552c1f296
No known key found for this signature in database
GPG Key ID: 9FACFECE1E3B3994
4 changed files with 167 additions and 0 deletions

View File

@ -4,6 +4,7 @@ use std::path::Path;
use serde_json::{json, Value};
use radicle::cob::patch::{Patch, PatchId};
use radicle_surf::blob::Blob;
use radicle_surf::tree::Tree;
use radicle_surf::{Commit, Stats};
@ -61,6 +62,26 @@ pub(crate) fn tree(tree: &Tree, path: &str, stats: &Stats) -> Value {
})
}
/// Returns JSON for a `patch`.
pub(crate) fn patch(id: PatchId, patch: Patch) -> Value {
json!({
"id": id.to_string(),
"author": patch.author(),
"title": patch.title(),
"description": patch.description(),
"state": patch.state(),
"target": patch.target(),
"tags": patch.tags().collect::<Vec<_>>(),
"revisions": patch.revisions().map(|(id, rev)| {
json!({
"id": id,
"description": rev.description(),
"reviews": rev.reviews().collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
})
}
/// Returns the name part of a path string.
fn name_in_path(path: &str) -> &str {
match path.rsplit('/').next() {

View File

@ -1,4 +1,5 @@
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::{env, fs};
@ -9,6 +10,7 @@ use serde_json::Value;
use tower::ServiceExt;
use radicle::cob::issue::Issues;
use radicle::cob::patch::{MergeTarget, Patches};
use radicle::git::raw as git2;
use radicle::storage::WriteStorage;
use radicle_cli::commands::rad_init;
@ -110,6 +112,22 @@ pub fn seed(dir: &Path) -> Context {
)
.unwrap();
// eq. rad patch open
let mut patches = Patches::open(*signer.public_key(), &repo).unwrap();
let oid = radicle::git::Oid::from_str(HEAD).unwrap();
let base = radicle::git::Oid::from_str(HEAD_1).unwrap();
patches
.create(
"A new `hello word`",
"change `hello world` in README to something else",
MergeTarget::Delegates,
base,
oid,
&[],
&signer,
)
.unwrap();
Context {
profile: Arc::new(profile),
sessions: Default::default(),

View File

@ -12,6 +12,7 @@ use serde_json::json;
use tower_http::set_header::SetResponseHeaderLayer;
use radicle::cob::issue::Issues;
use radicle::cob::patch::Patches;
use radicle::cob::thread::{self, CommentId};
use radicle::cob::Timestamp;
use radicle::identity::{Id, PublicKey};
@ -49,6 +50,8 @@ pub fn router(ctx: Context) -> Router {
.route("/projects/:project/readme/:sha", get(readme_handler))
.route("/projects/:project/issues", get(issues_handler))
.route("/projects/:project/issues/:id", get(issue_handler))
.route("/projects/:project/patches", get(patches_handler))
.route("/projects/:project/patches/:id", get(patch_handler))
.with_state(ctx)
}
@ -422,6 +425,46 @@ async fn issue_handler(
Ok::<_, Error>(Json(issue))
}
/// Get project patches list.
/// `GET /projects/:project/patches`
async fn patches_handler(
State(ctx): State<Context>,
Path(project): Path<Id>,
Query(qs): Query<PaginationQuery>,
) -> impl IntoResponse {
let PaginationQuery { page, per_page } = qs;
let page = page.unwrap_or(0);
let per_page = per_page.unwrap_or(10);
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let patches = Patches::open(ctx.profile.public_key, &repo)?;
let patches = patches
.all()?
.into_iter()
.filter_map(|r| r.ok())
.map(|(id, patch, _)| api::json::patch(id, patch))
.skip(page * per_page)
.take(per_page)
.collect::<Vec<_>>();
Ok::<_, Error>(Json(patches))
}
/// Get project patch.
/// `GET /projects/:project/patches/:id`
async fn patch_handler(
State(ctx): State<Context>,
Path((project, patch_id)): Path<(Id, Oid)>,
) -> impl IntoResponse {
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let patch = Patches::open(ctx.profile.public_key, &repo)?
.get(&patch_id.into())?
.ok_or(Error::NotFound)?;
Ok::<_, Error>(Json(api::json::patch(patch_id.into(), patch)))
}
#[derive(Serialize)]
struct Author {
id: PublicKey,
@ -939,4 +982,67 @@ mod routes {
])
);
}
#[tokio::test]
async fn test_projects_patches() {
let tmp = tempfile::tempdir().unwrap();
let app = super::router(test::seed(tmp.path()));
let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/patches").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!([
{
"id": "5de9f17ca5326258412ab02f9a5339b6482198ce",
"author": {
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
},
"title": "A new `hello word`",
"description": "change `hello world` in README to something else",
"state": "proposed",
"target": "delegates",
"tags": [],
"revisions": [
{
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
"description": "",
"reviews": [],
}
],
}
])
);
let response = get(
&app,
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/patches/5de9f17ca5326258412ab02f9a5339b6482198ce",
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!(
{
"id": "5de9f17ca5326258412ab02f9a5339b6482198ce",
"author": {
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
},
"title": "A new `hello word`",
"description": "change `hello world` in README to something else",
"state": "proposed",
"target": "delegates",
"tags": [],
"revisions": [
{
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
"description": "",
"reviews": [],
}
],
}
)
);
}
}

View File

@ -5,6 +5,7 @@ use std::ops::Range;
use std::str::FromStr;
use once_cell::sync::Lazy;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -180,6 +181,10 @@ impl Patch {
.timestamp
}
pub fn tags(&self) -> impl Iterator<Item = &Tag> {
self.tags.iter()
}
pub fn description(&self) -> Option<&str> {
Some(self.description.get().get())
}
@ -375,6 +380,10 @@ impl Revision {
pub fn description(&self) -> &str {
self.description.get()
}
pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
self.reviews.iter()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@ -479,6 +488,19 @@ pub struct Review {
pub timestamp: Max<Timestamp>,
}
impl Serialize for Review {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let mut state = serializer.serialize_struct("Review", 3)?;
state.serialize_field("verdict", &self.verdict())?;
state.serialize_field("comment", &self.comment())?;
state.serialize_field("timestamp", &self.timestamp())?;
state.end()
}
}
impl Semilattice for Review {
fn merge(&mut self, other: Self) {
self.verdict.merge(other.verdict);