Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: add remaining patch actions
Merged fintohaps opened 1 year ago

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.

8 files changed +357 -5 7d28d1e6 4cced3dd
modified radicle-cli/examples/rad-patch.md
@@ -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:

```
modified radicle-cli/src/commands/patch.rs
@@ -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,12 +70,14 @@ 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>...]
    rad patch edit <patch-id> [<option>...]
    rad patch set <patch-id> [<option>...]
-
    rad patch comment <patch-id | revision-id> [<option>...]
+
    rad patch comment <patch-id | revision-id> [--edit <comment-id>] [--redact <comment-id>]
+
                                               [--react <comment-id> --emoji <char>] [<option>...]
    rad patch cache [<patch-id>] [<option>...]

Show options
@@ -89,6 +94,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. If --message is used, the comment will be edited 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 +172,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 +195,7 @@ pub enum OperationName {
    Delete,
    Checkout,
    Comment,
+
    React,
    Ready,
    Review,
    Resolve,
@@ -194,6 +208,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 +264,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>,
@@ -290,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 { .. }
@@ -337,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();
@@ -392,6 +439,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()?;
@@ -400,6 +458,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()?;
@@ -665,11 +759,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
@@ -940,6 +1061,52 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                .transpose()?;
            cache::run(patch_id, &repository, &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 {
modified radicle-cli/src/commands/patch/comment.rs
@@ -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;
added radicle-cli/src/commands/patch/comment/edit.rs
@@ -0,0 +1,51 @@
+
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 {
+
        term::success!("Updated {comment_id}");
+
    } else {
+
        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(())
+
}
added radicle-cli/src/commands/patch/comment/react.rs
@@ -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(())
+
}
added radicle-cli/src/commands/patch/comment/redact.rs
@@ -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(())
+
}
added radicle-cli/src/commands/patch/common.rs
@@ -0,0 +1 @@
+

added radicle-cli/src/commands/patch/react.rs
@@ -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(())
+
}