cli: add remaining patch actions

Add the remaining `Patch` actions to the CLI interface. These include:

- React to a revision
- Edit a comment
- React to a comment
- Redact a comment

The `react` subcommand is added in a straight-forward manner.

The comment commands are slightly less straight-forward. In order to
keep to one layer of subcommands, these variants are done through the
`rad patch comment` subcommand. They are accessed via options:
`--edit`, `--react`, and `--redact`, respectively. Each of these
options takes the `CommentId` for which the action is affecting. The
`message`, `react` (introduced for `rad patch react`), and `undo`
variables repurposed for this subset of actions.
This commit is contained in:
Fintan Halpenny 2024-09-16 13:40:39 +01:00 committed by cloudhead
parent 7d28d1e648
commit 4cced3ddb8
No known key found for this signature in database
8 changed files with 353 additions and 4 deletions

View File

@ -143,6 +143,23 @@ $ rad patch comment aa45913 --message 'My favorite decade!' --reply-to 686ec1c -
f4336e42daf76342f787d574b5ee779d89d05c7a
```
If we realize we made a mistake in the comment, we can go back and edit it:
```
$ rad patch comment aa45913 --edit 686ec1c --message 'I cannot wait to get back to the 80s!' --no-announce
╭───────────────────────────────────────╮
│ alice (you) now 686ec1c │
│ I cannot wait to get back to the 80s! │
╰───────────────────────────────────────╯
```
And if we really made a mistake, then we can redact the comment entirely:
```
$ rad patch comment aa45913 --redact f4336e4 --no-announce
✓ Redacted comment f4336e42daf76342f787d574b5ee779d89d05c7a
```
Now, let's checkout the patch that we just created:
```

View File

@ -18,6 +18,8 @@ mod edit;
mod label;
#[path = "patch/list.rs"]
mod list;
#[path = "patch/react.rs"]
mod react;
#[path = "patch/ready.rs"]
mod ready;
#[path = "patch/redact.rs"]
@ -33,11 +35,12 @@ mod update;
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::str::FromStr as _;
use anyhow::anyhow;
use radicle::cob::patch::PatchId;
use radicle::cob::{patch, Label};
use radicle::cob::{patch, Label, Reaction};
use radicle::git::RefString;
use radicle::patch::cache::Patches as _;
use radicle::storage::git::transport;
@ -67,6 +70,7 @@ Usage
rad patch resolve <patch-id> [--review <review-id>] [--comment <comment-id>] [--unresolve] [<option>...]
rad patch delete <patch-id> [<option>...]
rad patch redact <revision-id> [<option>...]
rad patch react <patch-id | revision-id> [--react <emoji>] [<option>...]
rad patch assign <revision-id> [--add <did>] [--delete <did>] [<option>...]
rad patch label <revision-id> [--add <label>] [--delete <label>] [<option>...]
rad patch ready <patch-id> [--undo] [<option>...]
@ -89,6 +93,10 @@ Comment options
-m, --message <string> Provide a comment message via the command-line
--reply-to <comment> The comment to reply to
--edit <comment> The comment to edit (use --message to edit with the provided message)
--react <comment> The comment to react to
--emoji <char> The emoji to react with when --react is used
--redact <comment> The comment to redact
Edit options
@ -163,6 +171,10 @@ Set options
--remote <string> Provide the git remote to use as the upstream
React options
--emoji <char> The emoji to react to the patch or revision with
Other options
--repo <rid> Operate on the given repository (default: cwd)
@ -182,6 +194,7 @@ pub enum OperationName {
Delete,
Checkout,
Comment,
React,
Ready,
Review,
Resolve,
@ -194,6 +207,13 @@ pub enum OperationName {
Cache,
}
#[derive(Debug, PartialEq, Eq)]
pub enum CommentOperation {
Edit,
React,
Redact,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct AssignOptions {
pub add: BTreeSet<Did>,
@ -243,6 +263,26 @@ pub enum Operation {
message: Message,
reply_to: Option<Rev>,
},
CommentEdit {
revision_id: Rev,
comment_id: Rev,
message: Message,
},
CommentRedact {
revision_id: Rev,
comment_id: Rev,
},
CommentReact {
revision_id: Rev,
comment_id: Rev,
reaction: Reaction,
undo: bool,
},
React {
revision_id: Rev,
reaction: Reaction,
undo: bool,
},
Review {
patch_id: Rev,
revision_id: Option<Rev>,
@ -291,12 +331,16 @@ impl Operation {
| Operation::Ready { .. }
| Operation::Delete { .. }
| Operation::Comment { .. }
| Operation::CommentEdit { .. }
| Operation::CommentRedact { .. }
| Operation::CommentReact { .. }
| Operation::Review { .. }
| Operation::Resolve { .. }
| Operation::Assign { .. }
| Operation::Label { .. }
| Operation::Edit { .. }
| Operation::Redact { .. }
| Operation::React { .. }
| Operation::Set { .. } => true,
Operation::Show { .. }
| Operation::Diff { .. }
@ -338,7 +382,9 @@ impl Args for Options {
let mut diff = false;
let mut debug = false;
let mut undo = false;
let mut reaction: Option<Reaction> = None;
let mut reply_to: Option<Rev> = None;
let mut comment_op: Option<(CommentOperation, Rev)> = None;
let mut checkout_opts = checkout::Options::default();
let mut remote: Option<RefString> = None;
let mut assign_opts = AssignOptions::default();
@ -394,6 +440,17 @@ impl Args for Options {
base_id = Some(rev);
}
// React options.
Long("emoji") if op == Some(OperationName::React) => {
if let Some(emoji) = parser.value()?.to_str() {
reaction =
Some(Reaction::from_str(emoji).map_err(|_| anyhow!("invalid emoji"))?);
}
}
Long("undo") if op == Some(OperationName::React) => {
undo = true;
}
// Comment options.
Long("reply-to") if op == Some(OperationName::Comment) => {
let val = parser.value()?;
@ -402,6 +459,42 @@ impl Args for Options {
reply_to = Some(rev);
}
Long("edit") if op == Some(OperationName::Comment) => {
let val = parser.value()?;
let rev = term::args::rev(&val)?;
comment_op = Some((CommentOperation::Edit, rev));
}
Long("react") if op == Some(OperationName::Comment) => {
let val = parser.value()?;
let rev = term::args::rev(&val)?;
comment_op = Some((CommentOperation::React, rev));
}
Long("emoji")
if op == Some(OperationName::Comment)
&& matches!(comment_op, Some((CommentOperation::React, _))) =>
{
if let Some(emoji) = parser.value()?.to_str() {
reaction =
Some(Reaction::from_str(emoji).map_err(|_| anyhow!("invalid emoji"))?);
}
}
Long("undo")
if op == Some(OperationName::Comment)
&& matches!(comment_op, Some((CommentOperation::React, _))) =>
{
undo = true;
}
Long("redact") if op == Some(OperationName::Comment) => {
let val = parser.value()?;
let rev = term::args::rev(&val)?;
comment_op = Some((CommentOperation::Redact, rev));
}
// Edit options.
Long("revision") | Short('r') if op == Some(OperationName::Edit) => {
let val = parser.value()?;
@ -672,11 +765,38 @@ impl Args for Options {
revision_id,
opts: checkout_opts,
},
OperationName::Comment => Operation::Comment {
OperationName::Comment => match comment_op {
Some((CommentOperation::Edit, comment)) => Operation::CommentEdit {
revision_id: patch_id
.ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
comment_id: comment,
message,
},
Some((CommentOperation::React, comment)) => Operation::CommentReact {
revision_id: patch_id
.ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
comment_id: comment,
reaction: reaction
.ok_or_else(|| anyhow!("a reaction emoji must be provided"))?,
undo,
},
Some((CommentOperation::Redact, comment)) => Operation::CommentRedact {
revision_id: patch_id
.ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
comment_id: comment,
},
None => Operation::Comment {
revision_id: patch_id
.ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
message,
reply_to,
},
},
OperationName::React => Operation::React {
revision_id: patch_id
.ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
message,
reply_to,
reaction: reaction.ok_or_else(|| anyhow!("a reaction emoji must be provided"))?,
undo,
},
OperationName::Review => Operation::Review {
patch_id: patch_id
@ -963,6 +1083,52 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
};
cache::run(mode, &profile)?;
}
Operation::CommentEdit {
revision_id,
comment_id,
message,
} => {
let comment = comment_id.resolve(&repository.backend)?;
comment::edit::run(
revision_id,
comment,
message,
options.quiet,
&repository,
&profile,
)?;
}
Operation::CommentRedact {
revision_id,
comment_id,
} => {
let comment = comment_id.resolve(&repository.backend)?;
comment::redact::run(revision_id, comment, &repository, &profile)?;
}
Operation::CommentReact {
revision_id,
comment_id,
reaction,
undo,
} => {
let comment = comment_id.resolve(&repository.backend)?;
if undo {
comment::react::run(revision_id, comment, reaction, false, &repository, &profile)?;
} else {
comment::react::run(revision_id, comment, reaction, true, &repository, &profile)?;
}
}
Operation::React {
revision_id,
reaction,
undo,
} => {
if undo {
react::run(&revision_id, reaction, false, &repository, &profile)?;
} else {
react::run(&revision_id, reaction, true, &repository, &profile)?;
}
}
}
if announce {

View File

@ -1,3 +1,10 @@
#[path = "comment/edit.rs"]
pub mod edit;
#[path = "comment/react.rs"]
pub mod react;
#[path = "comment/redact.rs"]
pub mod redact;
use super::*;
use radicle::cob;

View File

@ -0,0 +1,49 @@
use anyhow::anyhow;
use radicle::cob;
use radicle::cob::patch;
use radicle::cob::thread;
use radicle::patch::cache::Patches as _;
use radicle::patch::ByRevision;
use radicle::storage::git::Repository;
use radicle::Profile;
use crate::git;
use crate::terminal as term;
use crate::terminal::Element as _;
pub fn run(
revision_id: git::Rev,
comment_id: thread::CommentId,
message: term::patch::Message,
quiet: bool,
repo: &Repository,
profile: &Profile,
) -> anyhow::Result<()> {
let signer = term::signer(profile)?;
let mut patches = profile.patches_mut(repo)?;
let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
let ByRevision {
id: patch_id,
patch,
revision_id,
revision,
} = patches
.find_by_revision(&patch::RevisionId::from(revision_id))?
.ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
let (body, _) = super::prompt(message, None, &revision, repo)?;
let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
patch.comment_edit(revision_id, comment_id, body, vec![], &signer)?;
if !quiet {
let comment = patch
.revision(&revision_id)
.ok_or(anyhow!("error retrieving revision `{revision_id}`"))?
.discussion()
.comment(&comment_id)
.ok_or(anyhow!("error retrieving comment `{comment_id}`"))?;
term::comment::widget(&comment_id, comment, profile).print();
}
Ok(())
}

View File

@ -0,0 +1,38 @@
use anyhow::anyhow;
use radicle::cob;
use radicle::cob::patch;
use radicle::cob::thread;
use radicle::cob::Reaction;
use radicle::patch::cache::Patches as _;
use radicle::patch::ByRevision;
use radicle::storage::git::Repository;
use radicle::Profile;
use crate::git;
use crate::terminal as term;
pub fn run(
revision_id: git::Rev,
comment: thread::CommentId,
reaction: Reaction,
active: bool,
repo: &Repository,
profile: &Profile,
) -> anyhow::Result<()> {
let signer = term::signer(profile)?;
let mut patches = profile.patches_mut(repo)?;
let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
let ByRevision {
id: patch_id,
patch,
revision_id,
..
} = patches
.find_by_revision(&patch::RevisionId::from(revision_id))?
.ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
patch.comment_react(revision_id, comment, reaction, active, &signer)?;
Ok(())
}

View File

@ -0,0 +1,36 @@
use anyhow::anyhow;
use radicle::cob;
use radicle::cob::patch;
use radicle::cob::thread;
use radicle::patch::cache::Patches as _;
use radicle::patch::ByRevision;
use radicle::storage::git::Repository;
use radicle::Profile;
use crate::git;
use crate::terminal as term;
pub fn run(
revision_id: git::Rev,
comment: thread::CommentId,
repo: &Repository,
profile: &Profile,
) -> anyhow::Result<()> {
let signer = term::signer(profile)?;
let mut patches = profile.patches_mut(repo)?;
let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
let ByRevision {
id: patch_id,
patch,
revision_id,
..
} = patches
.find_by_revision(&patch::RevisionId::from(revision_id))?
.ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
patch.comment_redact(revision_id, comment, &signer)?;
term::success!("Redacted comment {comment}");
Ok(())
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,35 @@
use anyhow::anyhow;
use radicle::cob;
use radicle::cob::{patch, Reaction};
use radicle::patch::cache::Patches as _;
use radicle::patch::ByRevision;
use radicle::storage::git::Repository;
use radicle::Profile;
use crate::git;
use crate::terminal as term;
pub fn run(
revision_id: &git::Rev,
reaction: Reaction,
active: bool,
repo: &Repository,
profile: &Profile,
) -> anyhow::Result<()> {
let signer = term::signer(profile)?;
let mut patches = profile.patches_mut(repo)?;
let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
let ByRevision {
id: patch_id,
patch,
revision_id,
..
} = patches
.find_by_revision(&patch::RevisionId::from(revision_id))?
.ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches);
patch.react(revision_id, reaction, None, active, &signer)?;
Ok(())
}