Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
fixup! Continue
✗ CI failure Erik Kundt committed 6 months ago
commit 4f8776b772ffdc101d97de5728db3bc603e57f8a
parent c8a37c5333f80c95b1cec96fd71050e47430dd42
1 failed (1 total) View logs
7 files changed +579 -517
modified crates/radicle-cli/src/commands/help.rs
@@ -76,7 +76,10 @@ const COMMANDS: &[CommandItem] = &[
        about: crate::commands::ls::ABOUT,
    },
    CommandItem::Lexopt(crate::commands::node::HELP),
-
    CommandItem::Lexopt(crate::commands::patch::HELP),
+
    CommandItem::Clap {
+
        name: "patch",
+
        about: crate::commands::patch::ABOUT,
+
    },
    CommandItem::Clap {
        name: "path",
        about: crate::commands::path::ABOUT,
modified crates/radicle-cli/src/commands/patch.rs
@@ -1,4 +1,5 @@
mod archive;
+
mod args;
mod assign;
mod cache;
mod checkout;
@@ -16,11 +17,6 @@ mod review;
mod show;
mod update;

-
#[path = "patch/args.rs"]
-
mod args;
-

-
pub use self::args::Args;
-

use std::collections::BTreeSet;

use anyhow::anyhow;
@@ -31,143 +27,15 @@ use radicle::patch::cache::Patches as _;
use radicle::storage::git::transport;
use radicle::{prelude::*, Node};

-
use crate::commands::rad_patch::args::{Command, CommentSubcommand};
use crate::git::Rev;
use crate::node;
use crate::terminal as term;
-
use crate::terminal::args::{Error, Help};
use crate::terminal::patch::Message;

-
pub const HELP: Help = Help {
-
    name: "patch",
-
    description: "Manage patches",
-
    version: env!("RADICLE_VERSION"),
-
    usage: r#"
-
Usage
-

-
    rad patch [<option>...]
-
    rad patch list [--all|--merged|--open|--archived|--draft|--authored] [--author <did>]... [<option>...]
-
    rad patch show <patch-id> [<option>...]
-
    rad patch diff <patch-id> [<option>...]
-
    rad patch archive <patch-id> [--undo] [<option>...]
-
    rad patch update <patch-id> [<option>...]
-
    rad patch checkout <patch-id> [<option>...]
-
    rad patch review <patch-id> [--accept | --reject] [-m [<string>]] [-d | --delete] [<option>...]
-
    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 cache [<patch-id>] [--storage] [<option>...]
-

-
Show options
-

-
    -p, --patch                Show the actual patch diff
-
    -v, --verbose              Show additional information about the patch
-

-
Diff options
-

-
    -r, --revision <id>        The revision to diff (default: latest)
-

-
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
-

-
    -m, --message [<string>]   Provide a comment message to the patch or revision (default: prompt)
-

-
Review options
-

-
    -r, --revision <id>        Review the given revision of the patch
-
    -p, --patch                Review by patch hunks
-
        --hunk <index>         Only review a specific hunk
-
        --accept               Accept a patch or set of hunks
-
        --reject               Reject a patch or set of hunks
-
    -U, --unified <n>          Generate diffs with <n> lines of context instead of the usual three
-
    -d, --delete               Delete a review draft
-
    -m, --message [<string>]   Provide a comment with the review (default: prompt)
-

-
Resolve options
-

-
    --review <id>              The review id which the comment is under
-
    --comment <id>             The comment to (un)resolve
-
    --undo                     Unresolve the comment
-

-
Assign options
-

-
    -a, --add    <did>         Add an assignee to the patch (may be specified multiple times).
-
                               Note: --add will take precedence over --delete
-

-
    -d, --delete <did>         Delete an assignee from the patch (may be specified multiple times).
-
                               Note: --add will take precedence over --delete
-

-
Archive options
-

-
        --undo                 Unarchive a patch
-

-
Label options
-

-
    -a, --add    <label>       Add a label to the patch (may be specified multiple times).
-
                               Note: --add will take precedence over --delete
-

-
    -d, --delete <label>       Delete a label from the patch (may be specified multiple times).
-
                               Note: --add will take precedence over --delete
-

-
Update options
-

-
    -b, --base <revspec>       Provide a Git revision as the base commit
-
    -m, --message [<string>]   Provide a comment message to the patch or revision (default: prompt)
-
        --no-message           Leave the patch or revision comment message blank
-

-
List options
-

-
        --all                  Show all patches, including merged and archived patches
-
        --archived             Show only archived patches
-
        --merged               Show only merged patches
-
        --open                 Show only open patches (default)
-
        --draft                Show only draft patches
-
        --authored             Show only patches that you have authored
-
        --author <did>         Show only patched where the given user is an author
-
                               (may be specified multiple times)
-

-
Ready options
+
pub use args::Args;
+
pub(crate) use args::ABOUT;

-
        --undo                 Convert a patch back to a draft
-

-
Checkout options
-

-
        --revision <id>        Checkout the given revision of the patch
-
        --name <string>        Provide a name for the branch to checkout
-
        --remote <string>      Provide the git remote to use as the upstream
-
    -f, --force                Checkout the head of the revision, even if the branch already exists
-

-
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)
-
        --[no-]announce        Announce changes made to the network
-
    -q, --quiet                Quiet output
-
        --help                 Print help
-
"#,
-
};
+
use args::{Command, CommentAction};

pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    let (workdir, rid) = if let Some(rid) = args.repo {
@@ -180,25 +48,27 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {

    let profile = ctx.profile()?;
    let repository = profile.storage.repository(rid)?;
-
    let announce = !args.no_announce && args.command.is_some_and(|c| c.is_announce());
+
    let announce = !args.no_announce && args.command.as_ref().is_some_and(|c| c.is_announce());
+

+
    // Fallback to [`Command::List`] if no subcommand is provided.
+
    // Construct it using the [`EmptyArgs`] in `args.empty`.
+
    let command = args
+
        .command
+
        .unwrap_or_else(|| Command::List(args.empty.into()));

    transport::local::register(profile.storage.clone());

-
    match args.command {
-
        Some(Command::List { filter, options, authors }) =>  {
-
            let mut authors: BTreeSet<Did> = authors.iter().cloned().collect();
-
            if options.authored {
+
    match command {
+
        Command::List(list_args) => {
+
            let mut authors: BTreeSet<Did> = list_args.authors.iter().cloned().collect();
+
            if list_args.authored {
                authors.insert(profile.did());
            }
-
            list::run(filter.as_ref(), authors, &repository, &profile)?;
+
            list::run((&list_args.state).into(), authors, &repository, &profile)?;
        }

-
        Some(Command::Show {
-
            patch_id,
-
            patch,
-
            verbose,
-
        }) => {
-
            let patch_id = patch_id.resolve(&repository.backend)?;
+
        Command::Show { id, patch, verbose } => {
+
            let patch_id = id.resolve(&repository.backend)?;
            show::run(
                &patch_id,
                patch,
@@ -209,24 +79,25 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            )?;
        }

-
        Some(Command::Diff {
-
            patch_id,
-
            revision_id,
-
        }) => {
-
            let patch_id = patch_id.resolve(&repository.backend)?;
-
            let revision_id = revision_id
+
        Command::Diff { id, revision } => {
+
            let patch_id = id.resolve(&repository.backend)?;
+
            let revision_id = revision
                .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
                .transpose()?
                .map(patch::RevisionId::from);
            diff::run(&patch_id, revision_id, &repository, &profile)?;
        }

-
        Some(Command::Update {
-
            ref patch_id,
-
            ref base_id,
-
            message: crate::commands::rad_patch::args::UpdateMessageArg { message, no_message: _ },
-
        }) => {
-
            let patch_id = patch_id.resolve(&repository.backend)?;
+
        Command::Update {
+
            id,
+
            base_id,
+
            message:
+
                args::UpdateMessageArg {
+
                    message,
+
                    no_message: _,
+
                },
+
        } => {
+
            let patch_id = id.resolve(&repository.backend)?;
            let base_id = base_id
                .as_ref()
                .map(|base| base.resolve(&repository.backend))
@@ -245,13 +116,13 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            )?;
        }

-
        Some(Command::Archive { ref patch_id, undo }) => {
-
            let patch_id = patch_id.resolve::<PatchId>(&repository.backend)?;
+
        Command::Archive { id, undo } => {
+
            let patch_id = id.resolve::<PatchId>(&repository.backend)?;
            archive::run(&patch_id, undo, &profile, &repository)?;
        }

-
        Some(Command::Ready { ref patch_id, undo }) => {
-
            let patch_id = patch_id.resolve::<PatchId>(&repository.backend)?;
+
        Command::Ready { id, undo } => {
+
            let patch_id = id.resolve::<PatchId>(&repository.backend)?;

            if !ready::run(&patch_id, undo, &profile, &repository)? {
                if undo {
@@ -262,18 +133,14 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            }
        }

-
        Some(Command::Delete { patch_id }) => {
-
            let patch_id = patch_id.resolve::<PatchId>(&repository.backend)?;
+
        Command::Delete { id } => {
+
            let patch_id = id.resolve::<PatchId>(&repository.backend)?;
            delete::run(&patch_id, &profile, &repository)?;
        }

-
        Some(Command::Checkout {
-
            patch_id,
-
            revision_id,
-
            opts,
-
        }) => {
-
            let patch_id = patch_id.resolve::<radicle::git::Oid>(&repository.backend)?;
-
            let revision_id = revision_id
+
        Command::Checkout { id, revision, opts } => {
+
            let patch_id = id.resolve::<radicle::git::Oid>(&repository.backend)?;
+
            let revision_id = revision
                .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
                .transpose()?
                .map(patch::RevisionId::from);
@@ -290,70 +157,64 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            )?;
        }

-
        Some(Command::Review {
-
            patch_id,
-
            revision_id,
-
            opts,
-
        }) => {
-
            let patch_id = patch_id.resolve(&repository.backend)?;
-
            let revision_id = revision_id
+
        Command::Review {
+
            id,
+
            revision,
+
            options,
+
        } => {
+
            let patch_id = id.resolve(&repository.backend)?;
+
            let revision_id = revision
                .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
                .transpose()?
                .map(patch::RevisionId::from);
-
            review::run(patch_id, revision_id, opts, &profile, &repository)?;
+
            review::run(patch_id, revision_id, options.into(), &profile, &repository)?;
        }

-
        Some(Command::Resolve {
-
            ref patch_id,
-
            ref review_id,
-
            ref comment_id,
-
            undo,
-
        }) => {
-
            let patch = patch_id.resolve(&repository.backend)?;
+
        Command::Resolve {
+
            id,
+
            review,
+
            comment,
+
            unresolve,
+
        } => {
+
            let patch = id.resolve(&repository.backend)?;
            let review = patch::ReviewId::from(
-
                review_id.resolve::<radicle::cob::EntryId>(&repository.backend)?,
+
                review.resolve::<radicle::cob::EntryId>(&repository.backend)?,
            );
-
            let comment = comment_id.resolve(&repository.backend)?;
-
            if undo {
+
            let comment = comment.resolve(&repository.backend)?;
+
            if unresolve {
                resolve::unresolve(patch, review, comment, &repository, &profile)?;
-
                term::success!("Unresolved comment {comment_id}");
+
                term::success!("Unresolved comment {comment}");
            } else {
                resolve::resolve(patch, review, comment, &repository, &profile)?;
-
                term::success!("Resolved comment {comment_id}");
+
                term::success!("Resolved comment {comment}");
            }
        }
-
        Some(Command::Edit {
-
            patch_id,
-
            revision_id,
+
        Command::Edit {
+
            id,
+
            revision,
            message,
-
        }) => {
-
            let patch_id = patch_id.resolve(&repository.backend)?;
-
            let revision_id = revision_id
+
        } => {
+
            let patch_id = id.resolve(&repository.backend)?;
+
            let revision_id = revision
                .map(|id| id.resolve::<radicle::git::Oid>(&repository.backend))
                .transpose()?
                .map(patch::RevisionId::from);
            edit::run(&patch_id, revision_id, message, &profile, &repository)?;
        }
-
        Some(Command::Redact { revision_id }) => {
-
            redact::run(&revision_id, &profile, &repository)?;
+
        Command::Redact { id } => {
+
            redact::run(&id, &profile, &repository)?;
        }
-
        Some(Command::Assign {
-
            patch_id,
-
            opts: self::args::AssignArg { add, delete },
-
        }) => {
-
            let patch_id = patch_id.resolve(&repository.backend)?;
+
        Command::Assign { id, add, delete } => {
+
            let patch_id = id.resolve(&repository.backend)?;
            assign::run(&patch_id, add, delete, &profile, &repository)?;
        }
-
        Some(Command::Label {
-
            patch_id,
-
            opts: self::args::LabelArg { add, delete },
-
        }) => {
-
            let patch_id = patch_id.resolve(&repository.backend)?;
+
        Command::Label { id, add, delete } => {
+
            let patch_id = id.resolve(&repository.backend)?;
            label::run(&patch_id, add, delete, &profile, &repository)?;
        }
-
        Some(Command::Set { patch_id, remote }) => {
+
        Command::Set { id, remote } => {
            let patches = term::cob::patches(&profile, &repository)?;
-
            let patch_id = patch_id.resolve(&repository.backend)?;
+
            let patch_id = id.resolve(&repository.backend)?;
            let patch = patches
                .get(&patch_id)?
                .ok_or_else(|| anyhow!("patch {patch_id} not found"))?;
@@ -368,13 +229,11 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                true,
            )?;
        }
-
        Some(Command::Cache { patch_id, storage }) => {
+
        Command::Cache { id, storage } => {
            let mode = if storage {
                cache::CacheMode::Storage
            } else {
-
                let patch_id = patch_id
-
                    .map(|id| id.resolve(&repository.backend))
-
                    .transpose()?;
+
                let patch_id = id.map(|id| id.resolve(&repository.backend)).transpose()?;
                patch_id.map_or(
                    cache::CacheMode::Repository {
                        repository: &repository,
@@ -387,56 +246,61 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
            };
            cache::run(mode, &profile)?;
        }
-
        Some(Command::Comment { subcommand: CommentSubcommand::Edit {
-
            revision_id,
-
            comment_id,
-
            message,
-
        }}) => {
-
            let comment = comment_id.resolve(&repository.backend)?;
-
            comment::edit::run(
-
                revision_id,
+
        Command::Comment(c) => match CommentAction::from(c) {
+
            CommentAction::Comment {
+
                revision,
+
                message,
+
                reply_to,
+
            } => {
+
                comment::run(
+
                    revision,
+
                    message,
+
                    reply_to,
+
                    args.quiet,
+
                    &repository,
+
                    &profile,
+
                )?;
+
            }
+
            CommentAction::Edit {
+
                revision,
                comment,
                message,
-
                args.quiet,
-
                &repository,
-
                &profile,
-
            )?;
-
        }
-
        Some(Command::Comment { subcommand: CommentSubcommand::Redact {
-
            revision_id,
-
            comment_id,
-
        }}) => {
-
            let comment = comment_id.resolve(&repository.backend)?;
-
            comment::redact::run(revision_id, comment, &repository, &profile)?;
-
        }
-
        Some(Command::Comment { subcommand: CommentSubcommand::React {
-
            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)?;
+
            } => {
+
                let comment = comment.resolve(&repository.backend)?;
+
                comment::edit::run(
+
                    revision,
+
                    comment,
+
                    message,
+
                    args.quiet,
+
                    &repository,
+
                    &profile,
+
                )?;
            }
-
        }
-
        Some(Command::React {
-
            revision_id,
-
            reaction,
-
            undo,
-
        }) => {
+
            CommentAction::Redact { revision, comment } => {
+
                let comment = comment.resolve(&repository.backend)?;
+
                comment::redact::run(revision, comment, &repository, &profile)?;
+
            }
+
            CommentAction::React {
+
                revision,
+
                comment,
+
                emoji,
+
                undo,
+
            } => {
+
                let comment = comment.resolve(&repository.backend)?;
+
                if undo {
+
                    comment::react::run(revision, comment, emoji, false, &repository, &profile)?;
+
                } else {
+
                    comment::react::run(revision, comment, emoji, true, &repository, &profile)?;
+
                }
+
            }
+
        },
+
        Command::React { id, react, undo } => {
            if undo {
-
                react::run(&revision_id, reaction, false, &repository, &profile)?;
+
                react::run(&id, react, false, &repository, &profile)?;
            } else {
-
                react::run(&revision_id, reaction, true, &repository, &profile)?;
+
                react::run(&id, react, true, &repository, &profile)?;
            }
        }
-

-
        None => {
-
            unimplemented!(),
-
        }
    }

    if announce {
modified crates/radicle-cli/src/commands/patch/args.rs
@@ -1,327 +1,220 @@
use clap::{Parser, Subcommand};
-
use clap_complete::ArgValueCompleter;
+

+
use radicle::cob::Label;
+
use radicle::git;
+
use radicle::git::fmt::RefString;
+
use radicle::patch::Status;
+
use radicle::patch::Verdict;
use radicle::prelude::Did;

+
use crate::commands::patch::checkout;
+
use crate::commands::patch::review;
+

use crate::git::Rev;
+
use crate::terminal::patch::Message;

-
#[derive(Parser, Debug)]
+
pub const ABOUT: &str = "Manage patches";
+

+
#[derive(Debug, Parser)]
+
#[command(about = ABOUT, disable_version_flag = true)]
pub struct Args {
    /// Subcommand for `radicle issue`
    #[command(subcommand)]
    pub(crate) command: Option<Command>,

    /// Don't print anything
-
    #[arg(short, long)]
-
    #[clap(global = true)]
+
    #[arg(short, long, global = true)]
    pub(crate) quiet: bool,

    /// Don't announce issue to peers
-
    #[arg(long)]
-
    #[arg(value_name = "no-announce")]
-
    #[clap(global = true)]
+
    #[arg(long, global = true, value_name = "no-announce")]
    pub(crate) no_announce: bool,

    /// Show only the issue header, hiding the comments
-
    #[arg(long)]
-
    #[clap(global = true)]
+
    #[arg(long, global = true)]
    pub(crate) header: bool,

    /// Optionally specify the repository to manage issues for
-
    #[arg(value_name = "RID")]
-
    #[arg(long, short)]
-
    #[clap(global = true)]
+
    #[arg(long, short, global = true, value_name = "RID")]
    pub(crate) repo: Option<radicle::prelude::RepoId>,

    /// Verbose output
-
    #[arg(long, short)]
-
    #[clap(global = true)]
+
    #[arg(long, short, global = true)]
    pub(crate) verbose: bool,
-
}
-

-
#[derive(Debug, clap::Args)]
-
#[group(required = false, multiple = false)]
-
pub struct ListOptions {
-
    #[arg(long, group = "cmd-list")]
-
    pub all: bool,
-

-
    #[arg(long, group = "cmd-list")]
-
    pub merged: bool,
-

-
    #[arg(long, group = "cmd-list")]
-
    pub open: bool,
-

-
    #[arg(long, group = "cmd-list")]
-
    pub archived: bool,
-

-
    #[arg(long, group = "cmd-list")]
-
    pub draft: bool,
-

-
    #[arg(long, group = "cmd-list")]
-
    pub authored: bool,
-
}
-

-
#[derive(Debug, Parser)]
-
pub struct ReviewOptions {
-
    #[clap(long, group = "cmd-review")]
-
    pub accept: bool,
-

-
    #[clap(long, group = "cmd-review")]
-
    pub reject: bool,
-

-
    #[clap(long, short = 'm', group = "cmd-review")]
-
    pub message: String,
-

-
    #[clap(long, short = 'd', group = "cmd-review")]
-
    pub delete: bool,
-
}
-

-
impl From<ReviewOptions> for crate::commands::rad_patch::review::Options {
-
    fn from(value: ReviewOptions) -> Self {
-
        Self {
-
            message: value.message.into(),
-
            op: if value.delete {
-
                crate::commands::rad_patch::review::Operation::Delete
-
            } else {
-
                todo!()
-
            }
-
        }
-
    }
-
}
-

-
#[derive(Debug, clap::Args)]
-
#[group(required = false, multiple = false)]
-
pub struct UpdateMessageArg {
-
    #[clap(long = "message", short = 'm', group = "update-message-arg")]
-
    pub message: crate::terminal::patch::Message,

-
    #[clap(long = "no-message")]
-
    pub no_message: bool,
-
}
-

-
#[derive(Debug, clap::Args)]
-
#[group(required = false, multiple = false)]
-
pub struct LabelArg {
-
    #[clap(long, group = "label-arg")]
-
    pub add: Option<String>,
-

-
    #[clap(long, group = "label-arg")]
-
    pub delete: Option<String>,
-
}
-

-

-
#[derive(Debug, clap::Args)]
-
#[group(required = false, multiple = false)]
-
pub struct AssignArg {
-
    #[clap(long, group = "assign-arg")]
-
    pub add: Option<String>,
-

-
    #[clap(long, group = "assign-arg")]
-
    pub delete: Option<String>,
-
}
-

-
#[derive(Debug, clap::Args)]
-
pub struct CheckoutOptions {
-
    #[clap(long)]
-
    pub name: Option<git_ref_format::RefString>,
-

-
    #[clap(long)]
-
    pub remote: Option<git_ref_format::RefString>,
-

-
    #[clap(long)]
-
    pub force: bool,
-
}
-

-
impl From<CheckoutOptions> for crate::commands::rad_patch::checkout::Options {
-
    fn from(value: CheckoutOptions) -> Self {
-
        Self {
-
            name: value.name,
-
            remote: value.remote,
-
            force: value.force,
-
        }
-
    }
+
    /// Arguments for the empty subcommand.
+
    /// Will fall back to [`Command::List`].
+
    #[clap(flatten)]
+
    pub(crate) empty: EmptyArgs,
}

/// Commands to create, view, and edit Radicle issues
#[derive(Subcommand, Debug)]
-
pub enum Command {
-
    List {
-
        #[clap(flatten)]
-
        options: ListOptions,
-

-
        #[clap(long)]
-
        authors: Vec<Did>,
-

-
        #[clap(long)]
-
        filter: Option<radicle::patch::Status>,
-
    },
+
pub(crate) enum Command {
+
    ///
+
    #[command(alias = "l")]
+
    List(ListArgs),

+
    ///
+
    #[command(alias = "s")]
    Show {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        // Show the actual patch diff
+
        #[arg(long)]
        patch: bool,

-
        /// Show additional information about the patch
+
        #[arg(long, short)]
        verbose: bool,
    },

    Diff {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        /// The revision of diff (default: latest)
-
        #[clap(long = "revision", short = 'r')]
-
        revision_id: Option<Rev>,
+
        #[arg(long = "revision", short = 'r')]
+
        revision: Option<Rev>,
    },

+
    #[command(alias = "a")]
    Archive {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

        undo: bool,
    },

+
    #[command(alias = "u")]
    Update {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        #[clap(long = "base", short = 'n')]
+
        #[arg(long = "base", short = 'n')]
        base_id: Option<Rev>,

        #[clap(flatten)]
        message: UpdateMessageArg,
    },

+
    #[command(alias = "c")]
    Checkout {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        #[clap(long)]
-
        revision_id: Option<Rev>,
+
        #[arg(long)]
+
        revision: Option<Rev>,

        #[clap(flatten)]
        opts: CheckoutOptions,
    },

    Review {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        #[clap(long)]
-
        revision_id: Option<Rev>,
+
        #[arg(long)]
+
        revision: Option<Rev>,

        #[clap(flatten)]
-
        options: ReviewOptions,
+
        options: ReviewOptionsArgs,
    },

    Resolve {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        #[clap(long)]
-
        review_id: Option<Rev>,
+
        #[arg(long, value_name = "REVIEW_ID")]
+
        review: Rev,

-
        #[clap(long)]
-
        comment_id: Option<Rev>,
+
        #[arg(long, value_name = "COMMENT_ID")]
+
        comment: Rev,

-
        #[clap(long)]
-
        undo: bool,
+
        #[arg(long)]
+
        unresolve: bool,
    },

+
    #[command(alias = "d")]
    Delete {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,
    },

+
    #[command(alias = "r")]
    Redact {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID|REVISION_ID")]
+
        id: Rev,
    },

    React {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID|REVISION_ID")]
+
        id: Rev,

-
        #[clap(long)]
-
        react: Option<radicle::cob::Reaction>,
+
        #[arg(long, value_name = "EMOJI")]
+
        react: radicle::cob::Reaction,
+

+
        #[arg(long)]
+
        undo: bool,
    },

    Assign {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        #[clap(flatten)]
-
        opts: AssignArg,
+
        #[arg(long, short, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
+
        add: Vec<Did>,
+

+
        #[clap(long, short, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
+
        delete: Vec<Did>,
    },

    Label {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        #[clap(flatten)]
-
        opts: LabelArg,
+
        #[arg(long, short, value_name = "LABEL", num_args = 1.., action = clap::ArgAction::Append)]
+
        add: Vec<Label>,
+

+
        #[clap(long, short, value_name = "LABEL", num_args = 1.., action = clap::ArgAction::Append)]
+
        delete: Vec<Label>,
    },

+
    #[command(alias = "y")]
    Ready {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

+
        #[arg(long)]
        undo: bool,
    },

+
    #[command(alias = "e")]
    Edit {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
-
    },
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
    Set {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Rev,
-
    },
+
        #[arg(long, value_name = "REVISION_ID")]
+
        revision: Option<Rev>,

-
    Comment {
-
        #[command(subcommand)]
-
        subcommand: CommentSubcommand,
+
        #[arg(long, short)]
+
        message: Message,
    },

-
    Cache {
-
        #[arg(value_name = "PATCH_ID", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        patch_id: Option<Rev>,
+
    Set {
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Rev,

-
        #[arg(long)]
-
        storage: bool,
+
        #[arg(long, value_name = "REF", value_parser = parse_refstr)]
+
        remote: Option<RefString>,
    },
-
}

-
#[derive(Subcommand, Debug)]
-
pub enum CommentSubcommand {
-
    Edit {
-
        #[arg(value_name = "REVISION", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        revision_id: Rev,
-
        #[arg(value_name = "COMMENT", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        comment_id: Rev,
-

-
        #[arg(long, short)]
-
        message: crate::terminal::patch::Message,
-
    },
-
    Redact {
-
        #[arg(value_name = "REVISION", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        revision_id: Rev,
-
        #[arg(value_name = "COMMENT", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        comment_id: Rev,
-
    },
-
    React {
-
        #[arg(value_name = "REVISION", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        revision_id: Rev,
-
        #[arg(value_name = "COMMENT", add = ArgValueCompleter::new(crate::commands::hints::patch_ids_completer))]
-
        comment_id: Rev,
+
    Comment(CommentArgs),

-
        #[arg(long)]
-
        reaction: radicle::cob::Reaction,
+
    Cache {
+
        #[arg(value_name = "PATCH_ID")]
+
        id: Option<Rev>,

        #[arg(long)]
-
        undo: bool,
+
        storage: bool,
    },
}

@@ -332,18 +225,7 @@ impl Command {
            | Self::Archive { .. }
            | Self::Ready { .. }
            | Self::Delete { .. }
-
            | Self::Comment {
-
                subcommand: CommentSubcommand::Edit { .. },
-
                ..
-
            }
-
            | Self::Comment {
-
                subcommand: CommentSubcommand::Redact { .. },
-
                ..
-
            }
-
            | Self::Comment {
-
                subcommand: CommentSubcommand::React { .. },
-
                ..
-
            }
+
            | Self::Comment { .. }
            | Self::Review { .. }
            | Self::Resolve { .. }
            | Self::Assign { .. }
@@ -360,3 +242,309 @@ impl Command {
        }
    }
}
+

+
#[derive(Parser, Debug)]
+
pub struct CommentArgs {
+
    #[arg(value_name = "REVISION_ID")]
+
    revision: Rev,
+

+
    #[arg(long, short)]
+
    message: Message,
+

+
    #[arg(
+
        long,
+
        value_name = "COMMENT_ID",
+
        conflicts_with = "react",
+
        conflicts_with = "redact"
+
    )]
+
    edit: Option<Rev>,
+

+
    #[arg(
+
        long,
+
        value_name = "COMMENT_ID",
+
        conflicts_with = "edit",
+
        conflicts_with = "redact",
+
        requires = "emoji"
+
    )]
+
    react: Option<Rev>,
+

+
    #[arg(
+
        long,
+
        value_name = "COMMENT_ID",
+
        conflicts_with = "react",
+
        conflicts_with = "edit"
+
    )]
+
    redact: Option<Rev>,
+

+
    #[arg(long, value_name = "EMOJI")]
+
    emoji: Option<radicle::cob::Reaction>,
+

+
    #[arg(long, value_name = "COMMENT_ID")]
+
    reply_to: Option<Rev>,
+

+
    #[arg(long)]
+
    undo: bool,
+
}
+

+
#[derive(Debug)]
+
pub(crate) enum CommentAction {
+
    Comment {
+
        revision: Rev,
+
        message: Message,
+
        reply_to: Option<Rev>,
+
    },
+
    Edit {
+
        revision: Rev,
+
        comment: Rev,
+
        message: Message,
+
    },
+
    Redact {
+
        revision: Rev,
+
        comment: Rev,
+
    },
+
    React {
+
        revision: Rev,
+
        comment: Rev,
+
        emoji: radicle::cob::Reaction,
+
        undo: bool,
+
    },
+
}
+

+
impl From<CommentArgs> for CommentAction {
+
    fn from(
+
        CommentArgs {
+
            revision,
+
            message,
+
            edit,
+
            react,
+
            redact,
+
            reply_to,
+
            emoji,
+
            undo,
+
        }: CommentArgs,
+
    ) -> Self {
+
        match (edit, react, redact) {
+
            (Some(edit), None, None) => CommentAction::Edit {
+
                revision,
+
                comment: edit,
+
                message,
+
            },
+
            (None, Some(react), None) => CommentAction::React {
+
                revision,
+
                comment: react,
+
                emoji: emoji.unwrap(),
+
                undo,
+
            },
+
            (None, None, Some(redact)) => CommentAction::Redact {
+
                revision,
+
                comment: redact,
+
            },
+
            (None, None, None) => Self::Comment {
+
                revision,
+
                message,
+
                reply_to,
+
            },
+
            _ => unreachable!("the argument cannot be used together'"),
+
        }
+
    }
+
}
+

+
#[derive(Parser, Debug, Default)]
+
pub(crate) struct EmptyArgs {
+
    #[arg(long, hide = true, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
+
    pub(crate) authors: Vec<Did>,
+

+
    #[arg(long, hide = true)]
+
    pub(crate) authored: bool,
+

+
    #[clap(flatten)]
+
    pub(crate) state: EmptyStateArgs,
+
}
+

+
#[derive(Parser, Debug, Default)]
+
#[group(multiple = false)]
+
pub(crate) struct EmptyStateArgs {
+
    #[arg(long, hide = true)]
+
    pub(crate) all: bool,
+

+
    #[arg(long, hide = true)]
+
    pub(crate) draft: bool,
+

+
    #[arg(long, hide = true)]
+
    pub(crate) open: bool,
+

+
    #[arg(long, hide = true)]
+
    pub(crate) merged: bool,
+

+
    #[arg(long, hide = true)]
+
    pub(crate) archived: bool,
+
}
+

+
#[derive(Parser, Debug, Default)]
+
pub(crate) struct ListArgs {
+
    #[arg(long)]
+
    pub(crate) authors: Vec<Did>,
+

+
    #[arg(long)]
+
    pub(crate) authored: bool,
+

+
    #[clap(flatten)]
+
    pub(crate) state: ListStateArgs,
+
}
+

+
impl From<EmptyArgs> for ListArgs {
+
    fn from(args: EmptyArgs) -> Self {
+
        Self {
+
            authors: args.authors,
+
            authored: args.authored,
+
            state: ListStateArgs::from(args.state),
+
        }
+
    }
+
}
+

+
#[derive(Parser, Debug, Default)]
+
#[group(multiple = false)]
+
pub(crate) struct ListStateArgs {
+
    #[arg(long)]
+
    pub(crate) all: bool,
+

+
    #[arg(long)]
+
    pub(crate) draft: bool,
+

+
    #[arg(long)]
+
    pub(crate) open: bool,
+

+
    #[arg(long)]
+
    pub(crate) merged: bool,
+

+
    #[arg(long)]
+
    pub(crate) archived: bool,
+
}
+

+
impl From<EmptyStateArgs> for ListStateArgs {
+
    fn from(args: EmptyStateArgs) -> Self {
+
        Self {
+
            all: args.all,
+
            draft: args.draft,
+
            open: args.open,
+
            merged: args.merged,
+
            archived: args.archived,
+
        }
+
    }
+
}
+

+
impl From<&ListStateArgs> for Option<&Status> {
+
    fn from(args: &ListStateArgs) -> Self {
+
        match (args.all, args.draft, args.open, args.merged, args.archived) {
+
            (true, false, false, false, false) => None,
+
            (false, true, false, false, false) => Some(&Status::Draft),
+
            (false, false, true, false, false) | (false, false, false, false, false) => {
+
                Some(&Status::Open)
+
            }
+
            (false, false, false, true, false) => Some(&Status::Merged),
+
            (false, false, false, false, true) => Some(&Status::Archived),
+
            _ => unreachable!(),
+
        }
+
    }
+
}
+

+
#[derive(Debug, Parser)]
+
pub struct ReviewOptionsArgs {
+
    #[arg(long, short)]
+
    pub message: Option<Message>,
+

+
    #[arg(long, short)]
+
    pub patch: bool,
+

+
    #[arg(long, short = 'U')]
+
    pub unified: Option<usize>,
+

+
    #[arg(long, value_name = "NUM")]
+
    pub hunk: Option<usize>,
+

+
    #[arg(long)]
+
    pub by_hunk: bool,
+

+
    #[clap(flatten)]
+
    action: ReviewActionsArgs,
+
}
+

+
#[derive(Debug, Parser)]
+
#[group(multiple = false)]
+
pub struct ReviewActionsArgs {
+
    #[arg(long, conflicts_with = "reject")]
+
    pub accept: bool,
+

+
    #[arg(long, conflicts_with = "accept")]
+
    pub reject: bool,
+

+
    #[arg(long, short)]
+
    pub delete: bool,
+
}
+

+
impl From<ReviewOptionsArgs> for review::Options {
+
    fn from(args: ReviewOptionsArgs) -> Self {
+
        Self {
+
            message: args.message.unwrap_or_default(),
+
            op: match (args.action.accept, args.action.reject, args.action.delete) {
+
                (false, false, false) => review::Operation::default(),
+
                (true, false, false) => review::Operation::Review(review::ReviewOptions {
+
                    verdict: Some(Verdict::Accept),
+
                    ..Default::default()
+
                }),
+
                (false, true, false) => review::Operation::Review(review::ReviewOptions {
+
                    verdict: Some(Verdict::Reject),
+
                    ..Default::default()
+
                }),
+
                (false, false, true) => review::Operation::Delete,
+
                _ => unreachable!("`--accpet`, `--reject` and `--delete` can not be used together"),
+
            },
+
        }
+
    }
+
}
+

+
#[derive(Debug, clap::Args)]
+
#[group(required = false, multiple = false)]
+
pub struct UpdateMessageArg {
+
    #[clap(long = "message", short = 'm', group = "update-message-arg")]
+
    pub message: crate::terminal::patch::Message,
+

+
    #[clap(long = "no-message")]
+
    pub no_message: bool,
+
}
+

+
#[derive(Debug, clap::Args)]
+
#[group(required = false, multiple = false)]
+
pub struct LabelArg {
+
    #[clap(long, group = "label-arg")]
+
    pub add: Option<String>,
+

+
    #[clap(long, group = "label-arg")]
+
    pub delete: Option<String>,
+
}
+

+
#[derive(Debug, clap::Args)]
+
pub struct CheckoutOptions {
+
    #[clap(long, value_parser = parse_refstr)]
+
    pub name: Option<RefString>,
+

+
    #[clap(long, value_parser = parse_refstr)]
+
    pub remote: Option<RefString>,
+

+
    #[clap(long, short)]
+
    pub force: bool,
+
}
+

+
impl From<CheckoutOptions> for checkout::Options {
+
    fn from(value: CheckoutOptions) -> Self {
+
        Self {
+
            name: value.name,
+
            remote: value.remote,
+
            force: value.force,
+
        }
+
    }
+
}
+

+
fn parse_refstr(refstr: &str) -> Result<RefString, git::fmt::Error> {
+
    RefString::try_from(refstr)
+
}
modified crates/radicle-cli/src/commands/patch/assign.rs
@@ -9,8 +9,8 @@ use crate::terminal as term;

pub fn run(
    patch_id: &PatchId,
-
    add: BTreeSet<Did>,
-
    delete: BTreeSet<Did>,
+
    add: Vec<Did>,
+
    delete: Vec<Did>,
    profile: &Profile,
    repository: &Repository,
) -> anyhow::Result<()> {
modified crates/radicle-cli/src/commands/patch/label.rs
@@ -6,8 +6,8 @@ use crate::terminal as term;

pub fn run(
    patch_id: &PatchId,
-
    add: BTreeSet<Label>,
-
    delete: BTreeSet<Label>,
+
    add: Vec<Label>,
+
    delete: Vec<Label>,
    profile: &Profile,
    repository: &Repository,
) -> anyhow::Result<()> {
modified crates/radicle-cli/src/commands/patch/review.rs
@@ -21,19 +21,16 @@ Markdown supported.
"#;

#[derive(Debug, PartialEq, Eq)]
-
pub enum Operation {
-
    Delete,
-
    Review {
-
        by_hunk: bool,
-
        unified: usize,
-
        hunk: Option<usize>,
-
        verdict: Option<Verdict>,
-
    },
+
pub struct ReviewOptions {
+
    pub by_hunk: bool,
+
    pub unified: usize,
+
    pub hunk: Option<usize>,
+
    pub verdict: Option<Verdict>,
}

-
impl Default for Operation {
+
impl Default for ReviewOptions {
    fn default() -> Self {
-
        Self::Review {
+
        Self {
            by_hunk: false,
            unified: 3,
            hunk: None,
@@ -42,6 +39,18 @@ impl Default for Operation {
    }
}

+
#[derive(Debug, PartialEq, Eq)]
+
pub enum Operation {
+
    Delete,
+
    Review(ReviewOptions),
+
}
+

+
impl Default for Operation {
+
    fn default() -> Self {
+
        Operation::Review(ReviewOptions::default())
+
    }
+
}
+

#[derive(Debug, Default)]
pub struct Options {
    pub message: Message,
@@ -77,24 +86,19 @@ pub fn run(

    let patch_id_pretty = term::format::tertiary(term::format::cob(&patch_id));
    match options.op {
-
        Operation::Review {
-
            verdict,
-
            by_hunk,
-
            unified,
-
            hunk,
-
        } if by_hunk => {
+
        Operation::Review(review_opts) if review_opts.by_hunk => {
            crate::warning::obsolete("rad patch review --patch");
            let mut opts = git::raw::DiffOptions::new();
            opts.patience(true)
                .minimal(true)
-
                .context_lines(unified as u32);
+
                .context_lines(review_opts.unified as u32);

            builder::ReviewBuilder::new(patch_id, repository)
-
                .hunk(hunk)
-
                .verdict(verdict)
+
                .hunk(review_opts.hunk)
+
                .verdict(review_opts.verdict)
                .run(revision, &mut opts, &signer)?;
        }
-
        Operation::Review { verdict, .. } => {
+
        Operation::Review(review_opts) => {
            let message = options.message.get(REVIEW_HELP_MSG)?;
            let message = message.replace(REVIEW_HELP_MSG.trim(), "");
            let message = if message.is_empty() {
@@ -102,9 +106,9 @@ pub fn run(
            } else {
                Some(message)
            };
-
            patch.review(revision_id, verdict, message, vec![], &signer)?;
+
            patch.review(revision_id, review_opts.verdict, message, vec![], &signer)?;

-
            match verdict {
+
            match review_opts.verdict {
                Some(Verdict::Accept) => {
                    term::success!(
                        "Patch {} {}",
modified crates/radicle-cli/src/main.rs
@@ -66,6 +66,7 @@ enum Commands {
    Init(init::Args),
    Issue(issue::Args),
    Ls(ls::Args),
+
    Patch(patch::Args),
    Path(path::Args),
    Publish(publish::Args),
    Stats(stats::Args),
@@ -278,7 +279,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
            term::run_command_args::<node::Options, _>(node::HELP, node::run, args.to_vec());
        }
        "patch" => {
-
            term::run_command_args::<patch::Options, _>(patch::HELP, patch::run, args.to_vec());
+
            if let Some(Commands::Patch(args)) = CliArgs::parse().command {
+
                term::run_command_fn(patch::run, args);
+
            }
        }
        "path" => {
            if let Some(Commands::Path(args)) = CliArgs::parse().command {