From 78e68336726e390dd549302e321e8d301118ad97 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Mon, 25 Sep 2023 22:55:07 +0200 Subject: [PATCH] httpd: Add missing COB actions Handles missing issue and patch actions. Adds embeds to patch comments. Signed-off-by: Sebastian Martinez --- radicle-cli/src/commands/patch/comment.rs | 2 +- radicle-httpd/src/api/json.rs | 2 + radicle-httpd/src/api/v1/projects.rs | 385 ++++++++++++++++++---- radicle-httpd/src/lib.rs | 1 + radicle-httpd/src/test.rs | 6 +- radicle/src/cob/issue.rs | 13 + radicle/src/cob/patch.rs | 130 +++++++- radicle/src/cob/thread.rs | 11 +- 8 files changed, 479 insertions(+), 71 deletions(-) diff --git a/radicle-cli/src/commands/patch/comment.rs b/radicle-cli/src/commands/patch/comment.rs index 688bff32..99d77dbf 100644 --- a/radicle-cli/src/commands/patch/comment.rs +++ b/radicle-cli/src/commands/patch/comment.rs @@ -27,7 +27,7 @@ pub fn run( .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); let (body, reply_to) = prompt(message, reply_to, &revision, repo)?; - let comment_id = patch.comment(revision_id, body, reply_to, &signer)?; + let comment_id = patch.comment(revision_id, body, reply_to, None, vec![], &signer)?; let comment = patch .revision(&revision_id) .ok_or(anyhow!("error retrieving revision `{revision_id}`"))? diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs index 078befe8..cde680a1 100644 --- a/radicle-httpd/src/api/json.rs +++ b/radicle-httpd/src/api/json.rs @@ -247,6 +247,7 @@ struct Comment<'a> { #[serde(with = "radicle::serde_ext::localtime::time")] timestamp: Timestamp, reply_to: Option, + resolved: bool, } impl<'a> Comment<'a> { @@ -260,6 +261,7 @@ impl<'a> Comment<'a> { reactions: comment.reactions().collect::>(), timestamp: comment.timestamp(), reply_to: comment.reply_to(), + resolved: comment.resolved(), } } } diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 7549d0c3..f9ee9d7d 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -732,11 +732,21 @@ async fn issue_update_handler( } => { issue.react(id, reaction, active, &signer)?; } - issue::Action::CommentEdit { .. } => { - todo!(); + issue::Action::CommentEdit { id, body, embeds } => { + let embeds: Vec = embeds + .into_iter() + .filter_map(|embed| { + let content = TryInto::::try_into(&embed.content).ok()?; + Some(Embed { + name: embed.name, + content: content.into(), + }) + }) + .collect(); + issue.edit_comment(id, body, embeds, &signer)?; } - issue::Action::CommentRedact { .. } => { - todo!(); + issue::Action::CommentRedact { id } => { + issue.redact_comment(id, &signer)?; } }; @@ -824,11 +834,26 @@ async fn patch_update_handler( patch::Action::Edit { title, target } => { patch.edit(title, target, &signer)?; } - patch::Action::RevisionEdit { + patch::Action::Label { labels } => { + patch.label(labels, &signer)?; + } + patch::Action::Lifecycle { state } => { + patch.lifecycle(state, &signer)?; + } + patch::Action::Assign { assignees } => { + patch.assign(assignees, &signer)?; + } + patch::Action::Merge { revision, commit } => { + // TODO: We should cleanup the stored copy at least. + let _ = patch.merge(revision, commit, &signer)?; + } + patch::Action::Review { revision, - description, + summary, + verdict, + labels, } => { - patch.edit_revision(revision, description, &signer)?; + patch.review(revision, verdict, summary, labels, &signer)?; } patch::Action::ReviewEdit { review, @@ -837,20 +862,45 @@ async fn patch_update_handler( } => { patch.edit_review(review, summary, verdict, &signer)?; } + patch::Action::ReviewRedact { review } => { + patch.redact_review(review, &signer)?; + } patch::Action::ReviewComment { review, body, reply_to, - .. + location, + embeds, } => { - patch.review_comment(review, body, None, reply_to, &signer)?; + let embeds: Vec = embeds + .into_iter() + .filter_map(|embed| { + let content = TryInto::::try_into(&embed.content).ok()?; + Some(Embed { + name: embed.name, + content: content.into(), + }) + }) + .collect(); + patch.review_comment(review, body, location, reply_to, embeds, &signer)?; } patch::Action::ReviewCommentEdit { review, comment, body, + embeds, } => { - patch.edit_review_comment(review, comment, body, &signer)?; + let embeds: Vec = embeds + .into_iter() + .filter_map(|embed| { + let content = TryInto::::try_into(&embed.content).ok()?; + Some(Embed { + name: embed.name, + content: content.into(), + }) + }) + .collect(); + patch.edit_review_comment(review, comment, body, embeds, &signer)?; } patch::Action::ReviewCommentReact { review, @@ -863,8 +913,11 @@ async fn patch_update_handler( patch::Action::ReviewCommentRedact { review, comment } => { patch.redact_review_comment(review, comment, &signer)?; } - patch::Action::Label { labels } => { - patch.label(labels, &signer)?; + patch::Action::ReviewCommentResolve { review, comment } => { + patch.resolve_review_comment(review, comment, &signer)?; + } + patch::Action::ReviewCommentUnresolve { review, comment } => { + patch.unresolve_review_comment(review, comment, &signer)?; } patch::Action::Revision { description, @@ -874,35 +927,51 @@ async fn patch_update_handler( } => { patch.update(description, base, oid, &signer)?; } - patch::Action::Lifecycle { state } => { - patch.lifecycle(state, &signer)?; - } - patch::Action::Review { + patch::Action::RevisionEdit { revision, - summary, - verdict, - labels, + description, } => { - patch.review(revision, verdict, summary, labels, &signer)?; + patch.edit_revision(revision, description, &signer)?; } - patch::Action::Merge { revision, commit } => { - // TODO: We should cleanup the stored copy at least. - let _ = patch.merge(revision, commit, &signer)?; + patch::Action::RevisionRedact { revision } => { + patch.redact(revision, &signer)?; } patch::Action::RevisionComment { revision, body, reply_to, - .. + location, + embeds, } => { - patch.comment(revision, body, reply_to, &signer)?; + let embeds: Vec = embeds + .into_iter() + .filter_map(|embed| { + let content = TryInto::::try_into(&embed.content).ok()?; + Some(Embed { + name: embed.name, + content: content.into(), + }) + }) + .collect(); + patch.comment(revision, body, reply_to, location, embeds, &signer)?; } patch::Action::RevisionCommentEdit { revision, comment, body, + embeds, } => { - patch.comment_edit(revision, comment, body, &signer)?; + let embeds: Vec = embeds + .into_iter() + .filter_map(|embed| { + let content = TryInto::::try_into(&embed.content).ok()?; + Some(Embed { + name: embed.name, + content: content.into(), + }) + }) + .collect(); + patch.comment_edit(revision, comment, body, embeds, &signer)?; } patch::Action::RevisionCommentReact { revision, @@ -1937,7 +2006,8 @@ mod routes { "embeds": [], "reactions": [], "timestamp": TIMESTAMP, - "replyTo": null + "replyTo": null, + "resolved": false, } ], "labels": [] @@ -2017,6 +2087,7 @@ mod routes { "reactions": [], "timestamp": TIMESTAMP, "replyTo": null, + "resolved": false, }], "labels": [ "bug", @@ -2059,7 +2130,7 @@ mod routes { let body = serde_json::to_vec(&json!({ "type": "comment.react", - "id": "6fe9fdec2ec9f6436f2875dbcbedb95dd215b863", + "id": ISSUE_DISCUSSION_ID, "reaction": "🚀", "active": true, })) @@ -2072,6 +2143,40 @@ mod routes { ) .await; + let body = serde_json::to_vec(&json!({ + "type": "comment.edit", + "id": ISSUE_DISCUSSION_ID, + "body": "EDIT: Change 'hello world' to 'hello anyone'", + "embeds": [] + })) + .unwrap(); + + let response = patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/issues/{CONTRIBUTOR_ISSUE_ID}"), + Some(Body::from(body)), + Some(SESSION_ID.to_string()), + ) + .await; + + assert_eq!(response.json().await, json!({ "success": true })); + + let body = serde_json::to_vec(&json!({ + "type": "comment.redact", + "id": "6fe9fdec2ec9f6436f2875dbcbedb95dd215b863", + })) + .unwrap(); + + let response = patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/issues/{CONTRIBUTOR_ISSUE_ID}"), + Some(Body::from(body)), + Some(SESSION_ID.to_string()), + ) + .await; + + assert_eq!(response.json().await, json!({ "success": true })); + let response = get( &app, format!("/projects/{CONTRIBUTOR_RID}/issues/{CONTRIBUTOR_ISSUE_ID}"), @@ -2096,32 +2201,17 @@ mod routes { "author": { "id": CONTRIBUTOR_DID, }, - "body": "Change 'hello world' to 'hello everyone'", + "body": "EDIT: Change 'hello world' to 'hello anyone'", "embeds": [], - "reactions": [], - "timestamp": TIMESTAMP, - "replyTo": null, - }, - { - "id": "6fe9fdec2ec9f6436f2875dbcbedb95dd215b863", - "author": { - "id": CONTRIBUTOR_DID, - }, - "body": "This is first-level comment", - "embeds": [ - { - "name": "image.jpg", - "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", - }, - ], "reactions": [ [ - "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", - "🚀", + "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", + "🚀", ], ], "timestamp": TIMESTAMP, - "replyTo": CONTRIBUTOR_ISSUE_ID, + "replyTo": null, + "resolved": false, }, ], "labels": [], @@ -2185,6 +2275,7 @@ mod routes { "reactions": [], "timestamp": TIMESTAMP, "replyTo": null, + "resolved": false, }, { "id": ISSUE_COMMENT_ID, @@ -2196,6 +2287,7 @@ mod routes { "reactions": [], "timestamp": TIMESTAMP, "replyTo": ISSUE_DISCUSSION_ID, + "resolved": false, }, ], "labels": [], @@ -2371,7 +2463,68 @@ mod routes { } #[tokio::test] - async fn test_projects_patches_tag() { + async fn test_projects_patches_assign() { + let tmp = tempfile::tempdir().unwrap(); + let ctx = contributor(tmp.path()); + let app = super::router(ctx.to_owned()); + create_session(ctx).await; + let body = serde_json::to_vec(&json!({ + "type": "assign", + "assignees": [CONTRIBUTOR_DID] + })) + .unwrap(); + let response = patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(body)), + Some(SESSION_ID.to_string()), + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + + let response = get( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + ) + .await; + + assert_eq!( + response.json().await, + json!({ + "id": CONTRIBUTOR_PATCH_ID, + "author": { + "id": CONTRIBUTOR_DID, + }, + "title": "A new `hello world`", + "state": { "status": "open" }, + "target": "delegates", + "labels": [], + "merges": [], + "assignees": [CONTRIBUTOR_DID], + "revisions": [ + { + "id": CONTRIBUTOR_PATCH_ID, + "author": { + "id": CONTRIBUTOR_DID, + }, + "description": "change `hello world` in README to something else", + "base": PARENT, + "oid": HEAD, + "refs": [ + "refs/heads/master", + ], + "discussions": [], + "timestamp": TIMESTAMP, + "reviews": [], + }, + ], + }) + ); + } + + #[tokio::test] + async fn test_projects_patches_label() { let tmp = tempfile::tempdir().unwrap(); let ctx = contributor(tmp.path()); let app = super::router(ctx.to_owned()); @@ -2575,6 +2728,68 @@ mod routes { ); } + #[tokio::test] + async fn test_projects_patches_revisions_edit() { + let tmp = tempfile::tempdir().unwrap(); + let ctx = contributor(tmp.path()); + let app = super::router(ctx.to_owned()); + create_session(ctx).await; + let body = serde_json::to_vec(&json!({ + "type": "revision.edit", + "revision": CONTRIBUTOR_PATCH_ID, + "description": "Let's change the description a bit", + })) + .unwrap(); + let response = patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(body)), + Some(SESSION_ID.to_string()), + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + + let response = get( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + ) + .await; + + assert_eq!( + response.json().await, + json!({ + "id": CONTRIBUTOR_PATCH_ID, + "author": { + "id": CONTRIBUTOR_DID, + }, + "title": "A new `hello world`", + "state": { "status": "open" }, + "target": "delegates", + "labels": [], + "merges": [], + "assignees": [], + "revisions": [ + { + "id": CONTRIBUTOR_PATCH_ID, + "author": { + "id": CONTRIBUTOR_DID, + }, + "description": "Let's change the description a bit", + "base": PARENT, + "oid": HEAD, + "refs": [ + "refs/heads/master", + ], + "discussions": [], + "timestamp": TIMESTAMP, + "reviews": [], + }, + ], + }) + ); + } + #[tokio::test] async fn test_projects_patches_discussions() { let tmp = tempfile::tempdir().unwrap(); @@ -2584,7 +2799,13 @@ mod routes { let thread_body = serde_json::to_vec(&json!({ "type": "revision.comment", "revision": CONTRIBUTOR_PATCH_ID, - "body": "This is a root level comment" + "body": "This is a root level comment", + "embeds": [ + { + "name": "image.jpg", + "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC" + } + ], })) .unwrap(); let response = patch( @@ -2616,7 +2837,13 @@ mod routes { "type": "revision.comment.edit", "revision": CONTRIBUTOR_PATCH_ID, "comment": CONTRIBUTOR_COMMENT_1, - "body": "EDIT: This is a root level comment" + "body": "EDIT: This is a root level comment", + "embeds": [ + { + "name": "image.jpg", + "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC" + } + ], })) .unwrap(); let response = patch( @@ -2633,6 +2860,7 @@ mod routes { "revision": CONTRIBUTOR_PATCH_ID, "body": "This is a root level comment", "replyTo": CONTRIBUTOR_COMMENT_1, + "embeds": [], })) .unwrap(); let response = patch( @@ -2683,10 +2911,16 @@ mod routes { "id": CONTRIBUTOR_DID, }, "body": "EDIT: This is a root level comment", - "embeds": [], + "embeds": [ + { + "name": "image.jpg", + "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", + } + ], "reactions": [["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8","🚀"]], "timestamp": TIMESTAMP, "replyTo": null, + "resolved": false, }, { "id": CONTRIBUTOR_COMMENT_2, @@ -2698,6 +2932,7 @@ mod routes { "reactions": [], "timestamp": TIMESTAMP, "replyTo": CONTRIBUTOR_COMMENT_1, + "resolved": false, }, ], "timestamp": TIMESTAMP, @@ -2734,7 +2969,18 @@ mod routes { let review_comment_body = serde_json::to_vec(&json!({ "type": "review.comment", "review": CONTRIBUTOR_PATCH_REVIEW, - "body": "This is a comment on a review" + "body": "This is a comment on a review", + "embeds": [], + "location": { + "path": "README.md", + "new": { + "type": "lines", + "range": { + "start": 2, + "end": 4 + } + } + } })) .unwrap(); patch( @@ -2749,7 +2995,8 @@ mod routes { "type": "review.comment.edit", "review": CONTRIBUTOR_PATCH_REVIEW, "comment": CONTRIBUTOR_COMMENT_3, - "body": "EDIT: This is a comment on a review" + "embeds": [], + "body": "EDIT: This is a comment on a review", })) .unwrap(); patch( @@ -2776,6 +3023,20 @@ mod routes { ) .await; + let review_resolve_body = serde_json::to_vec(&json!({ + "type": "review.comment.resolve", + "review": CONTRIBUTOR_PATCH_REVIEW, + "comment": CONTRIBUTOR_COMMENT_3, + })) + .unwrap(); + patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(review_resolve_body)), + Some(SESSION_ID.to_string()), + ) + .await; + let response = get( &app, format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), @@ -2817,15 +3078,27 @@ mod routes { "verdict": "accept", "summary": "A small review", "comments": [[ - "dd9743bb964ba22399548c86a3c1765020d58f48", + CONTRIBUTOR_COMMENT_3, { - "author": "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", + "author": CONTRIBUTOR_NID, + "location": { + "path": "README.md", + "old": null, + "new": { + "type": "lines", + "range": { + "start": 2, + "end": 4, + } + } + }, "reactions": [ [ "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", "🚀", ], ], + "resolved": true, "body": "EDIT: This is a comment on a review", }, ]], diff --git a/radicle-httpd/src/lib.rs b/radicle-httpd/src/lib.rs index 0fc159c2..83b138c8 100644 --- a/radicle-httpd/src/lib.rs +++ b/radicle-httpd/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::type_complexity)] #![allow(clippy::too_many_arguments)] +#![recursion_limit = "256"] pub mod error; use std::collections::HashMap; diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index c875aadd..c95ec271 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -45,9 +45,9 @@ pub const CONTRIBUTOR_NID: &str = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJm pub const CONTRIBUTOR_ISSUE_ID: &str = "7466975f0bef37b459887824a9655f3e78262522"; pub const CONTRIBUTOR_PATCH_ID: &str = "e651ae5869a2c1ac8ad4f6deae4cc835656ffa25"; pub const CONTRIBUTOR_PATCH_REVIEW: &str = "ee3eeba95f4ec418b3d0714e18e0d1ff605dc0e6"; -pub const CONTRIBUTOR_COMMENT_1: &str = "d0bb75b2c72ab8b5486d39f6cf5f41f104b63cb1"; -pub const CONTRIBUTOR_COMMENT_2: &str = "227947fa41420e363752357b2edd54d3ccadf7b3"; -pub const CONTRIBUTOR_COMMENT_3: &str = "dd9743bb964ba22399548c86a3c1765020d58f48"; +pub const CONTRIBUTOR_COMMENT_1: &str = "d8ff07edbc8d2229e54e70f2f5bc31614287f0dc"; +pub const CONTRIBUTOR_COMMENT_2: &str = "f3fca0add53f85bc51a85198efed3273fe13b88e"; +pub const CONTRIBUTOR_COMMENT_3: &str = "06990ff59faa12463f693dae7a98eb33d75afd2e"; /// Create a new profile. pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile { diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs index 5137cfbc..d56c9801 100644 --- a/radicle/src/cob/issue.rs +++ b/radicle/src/cob/issue.rs @@ -563,6 +563,19 @@ where }) } + /// Edit a comment. + pub fn edit_comment( + &mut self, + id: CommentId, + body: S, + embeds: impl IntoIterator, + signer: &G, + ) -> Result { + self.transaction("Edit comment", signer, |tx| { + tx.edit_comment(id, body, embeds.into_iter().collect()) + }) + } + /// Redact a comment. pub fn redact_comment( &mut self, diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index 7c93db35..d9b12891 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -19,7 +19,7 @@ use crate::cob::store::{Cob, CobAction}; use crate::cob::thread; use crate::cob::thread::Thread; use crate::cob::thread::{Comment, CommentId, Reactions}; -use crate::cob::{op, store, ActorId, EntryId, ObjectId, TypeName}; +use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName, Uri}; use crate::crypto::{PublicKey, Signer}; use crate::git; use crate::identity; @@ -176,12 +176,16 @@ pub enum Action { /// Should be [`Some`] otherwise. #[serde(default, skip_serializing_if = "Option::is_none")] reply_to: Option, + /// Embeded content. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + embeds: Vec>, }, #[serde(rename = "review.comment.edit")] ReviewCommentEdit { review: ReviewId, comment: EntryId, body: String, + embeds: Vec>, }, #[serde(rename = "review.comment.redact")] ReviewCommentRedact { review: ReviewId, comment: EntryId }, @@ -240,6 +244,9 @@ pub enum Action { /// Should be the root [`CommentId`] if it's a top-level comment. #[serde(default, skip_serializing_if = "Option::is_none")] reply_to: Option, + /// Embeded content. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + embeds: Vec>, }, /// Edit a revision comment. #[serde(rename = "revision.comment.edit")] @@ -247,6 +254,7 @@ pub enum Action { revision: RevisionId, comment: CommentId, body: String, + embeds: Vec>, }, /// Redact a revision comment. #[serde(rename = "revision.comment.redact")] @@ -856,6 +864,7 @@ impl Patch { review, comment, body, + embeds, } => { if let Some(review) = lookup::review_mut(self, &review)? { thread::edit( @@ -864,7 +873,7 @@ impl Patch { comment, timestamp, body, - vec![], + embeds, )?; } } @@ -883,6 +892,7 @@ impl Patch { body, location, reply_to, + embeds, } => { if let Some(review) = lookup::review_mut(self, &review)? { thread::comment( @@ -893,7 +903,7 @@ impl Patch { body, reply_to, location, - vec![], + embeds, )?; } } @@ -1008,6 +1018,7 @@ impl Patch { revision, comment, body, + embeds, } => { if let Some(revision) = lookup::revision_mut(self, &revision)? { thread::edit( @@ -1016,7 +1027,7 @@ impl Patch { comment, timestamp, body, - vec![], + embeds, )?; } } @@ -1554,6 +1565,7 @@ impl store::Transaction { body: body.to_string(), reply_to: None, location: None, + embeds: vec![], }) } @@ -1563,12 +1575,18 @@ impl store::Transaction { revision: RevisionId, body: S, reply_to: Option, + location: Option, + embeds: Vec, ) -> Result<(), store::Error> { + let hashed = embeds.iter().map(|e| e.hashed()).collect(); + + self.embed(embeds)?; self.push(Action::RevisionComment { revision, body: body.to_string(), reply_to, - location: None, + location, + embeds: hashed, }) } @@ -1578,11 +1596,16 @@ impl store::Transaction { revision: RevisionId, comment: CommentId, body: S, + embeds: Vec, ) -> Result<(), store::Error> { + let hashed = embeds.iter().map(|e| e.hashed()).collect(); + + self.embed(embeds)?; self.push(Action::RevisionCommentEdit { revision, comment, body: body.to_string(), + embeds: hashed, }) } @@ -1618,26 +1641,54 @@ impl store::Transaction { body: S, location: Option, reply_to: Option, + embeds: Vec, ) -> Result<(), store::Error> { + let hashed = embeds.iter().map(|e| e.hashed()).collect(); + self.embed(embeds)?; + self.push(Action::ReviewComment { review, body: body.to_string(), location, reply_to, + embeds: hashed, }) } + /// Resolve a review comment. + pub fn review_comment_resolve( + &mut self, + review: ReviewId, + comment: CommentId, + ) -> Result<(), store::Error> { + self.push(Action::ReviewCommentResolve { review, comment }) + } + + /// Unresolve a review comment. + pub fn review_comment_unresolve( + &mut self, + review: ReviewId, + comment: CommentId, + ) -> Result<(), store::Error> { + self.push(Action::ReviewCommentUnresolve { review, comment }) + } + /// Edit review comment. pub fn edit_review_comment( &mut self, review: ReviewId, comment: EntryId, body: S, + embeds: Vec, ) -> Result<(), store::Error> { + let hashed = embeds.iter().map(|e| e.hashed()).collect(); + + self.embed(embeds)?; self.push(Action::ReviewCommentEdit { review, comment, body: body.to_string(), + embeds: hashed, }) } @@ -1712,6 +1763,11 @@ impl store::Transaction { self.push(Action::Lifecycle { state }) } + /// Assign a patch. + pub fn assign(&mut self, assignees: BTreeSet) -> Result<(), store::Error> { + self.push(Action::Assign { assignees }) + } + /// Label a patch. pub fn label(&mut self, labels: impl IntoIterator) -> Result<(), store::Error> { self.push(Action::Label { @@ -1828,9 +1884,19 @@ where revision: RevisionId, body: S, reply_to: Option, + location: Option, + embeds: impl IntoIterator, signer: &G, ) -> Result { - self.transaction("Comment", signer, |tx| tx.comment(revision, body, reply_to)) + self.transaction("Comment", signer, |tx| { + tx.comment( + revision, + body, + reply_to, + location, + embeds.into_iter().collect(), + ) + }) } /// Edit a comment on a patch revision. @@ -1839,10 +1905,11 @@ where revision: RevisionId, comment: CommentId, body: S, + embeds: impl IntoIterator, signer: &G, ) -> Result { self.transaction("Edit comment", signer, |tx| { - tx.comment_edit(revision, comment, body) + tx.comment_edit(revision, comment, body, embeds.into_iter().collect()) }) } @@ -1879,10 +1946,17 @@ where body: S, location: Option, reply_to: Option, + embeds: impl IntoIterator, signer: &G, ) -> Result { self.transaction("Review comment", signer, |tx| { - tx.review_comment(review, body, location, reply_to) + tx.review_comment( + review, + body, + location, + reply_to, + embeds.into_iter().collect(), + ) }) } @@ -1892,10 +1966,11 @@ where review: ReviewId, comment: EntryId, body: S, + embeds: impl IntoIterator, signer: &G, ) -> Result { self.transaction("Edit review comment", signer, |tx| { - tx.edit_review_comment(review, comment, body) + tx.edit_review_comment(review, comment, body, embeds.into_iter().collect()) }) } @@ -1949,6 +2024,30 @@ where self.transaction("Redact review", signer, |tx| tx.redact_review(review)) } + /// Resolve a patch review comment. + pub fn resolve_review_comment( + &mut self, + review: ReviewId, + comment: CommentId, + signer: &G, + ) -> Result { + self.transaction("Resolve review comment", signer, |tx| { + tx.review_comment_resolve(review, comment) + }) + } + + /// Unresolve a patch review comment. + pub fn unresolve_review_comment( + &mut self, + review: ReviewId, + comment: CommentId, + signer: &G, + ) -> Result { + self.transaction("Unresolve review comment", signer, |tx| { + tx.review_comment_unresolve(review, comment) + }) + } + /// Merge a patch revision. pub fn merge( &mut self, @@ -1985,6 +2084,15 @@ where self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state)) } + /// Assign a patch. + pub fn assign( + &mut self, + assignees: BTreeSet, + signer: &G, + ) -> Result { + self.transaction("Assign", signer, |tx| tx.assign(assignees)) + } + /// Archive a patch. pub fn archive(&mut self, signer: &G) -> Result { self.lifecycle(Lifecycle::Archived, signer) @@ -2223,6 +2331,7 @@ where #[allow(clippy::unwrap_used)] mod test { use std::str::FromStr; + use std::vec; use pretty_assertions::assert_eq; @@ -2310,7 +2419,7 @@ mod test { let (revision_id, _) = patch.revisions().last().unwrap(); assert!( patch - .comment(revision_id, "patch comment", None, &alice.signer) + .comment(revision_id, "patch comment", None, None, [], &alice.signer) .is_ok(), "can comment on patch" ); @@ -2666,6 +2775,7 @@ mod test { "I like these lines of code", Some(location.clone()), None, + [], &alice.signer, ) .unwrap(); diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index dd7f4b75..97adf4bd 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -83,7 +83,7 @@ impl Serialize for Comment { where S: serde::ser::Serializer, { - let mut state = serializer.serialize_struct("Comment", 6)?; + let mut state = serializer.serialize_struct("Comment", 8)?; state.serialize_field("author", &self.author())?; if let Some(loc) = &self.location { state.serialize_field("location", loc)?; @@ -92,7 +92,11 @@ impl Serialize for Comment { state.serialize_field("replyTo", &to)?; } state.serialize_field("reactions", &self.reactions)?; + state.serialize_field("resolved", &self.resolved)?; state.serialize_field("body", self.body())?; + if let Some(location) = self.location() { + state.serialize_field("location", &location)?; + } let embeds = self.embeds(); if !embeds.is_empty() { @@ -179,6 +183,11 @@ impl Comment { self.location.as_ref() } + /// Get comment resolution status. + pub fn resolved(&self) -> bool { + self.resolved + } + /// Return the embedded media. pub fn embeds(&self) -> &[Embed] { // SAFETY: There is always at least one edit. This is guaranteed by the [`Comment`]