cli: Fix panic on `rad patch review` without options

When invoked without any of `--patch`, `--delete`, `--accept`,
or `--reject`, the command panicked.

It will now correctly return an error to use `--accept`, `--reject`,
or to supply a message.
Note that `--patch` and `--delete` are removed as these commands are
now obsolete.

The new doc test also documents two previously-untested behaviors
of `rad patch review`: neutral reviews (verdict=None with summary)
and the COB layer's silent no-op when a duplicate review is submitted
(see `test_patch_review_duplicate` in radicle-cli/tests/commands/patch.rs).

Fixes 8ceff5d4b8fa94bcbe243f9f8ef52138f65e495d

Signed-off-by: Arthur Mariano <arthvm@proton.me>
Co-authored-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Arthur Mariano 2026-05-27 11:51:07 -03:00 committed by Fintan Halpenny
parent 804fa3e44c
commit 0bbedb9650
5 changed files with 75 additions and 18 deletions

View File

@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
merged. The Git remote helper will now fallback to the canonical default merged. The Git remote helper will now fallback to the canonical default
branch, if the default branch of the delegate could not be found, and branch, if the default branch of the delegate could not be found, and
correctly mark the patch as merged. correctly mark the patch as merged.
- `rad patch review` presents an appropriate error to the user when the user
does not provide an option, one of `--accept` or `--reject`, or a summary
message.
## 1.9.1 ## 1.9.1

View File

@ -0,0 +1,24 @@
The `rad patch review` command must provide an option or a review message, since
a review must have a verdict or a review summary:
``` (fail)
$ rad patch review aa45913 --no-message --no-announce
✗ Error: expected one of `--accept` or `--reject`, or supply a review message
```
This time we will supply a message:
```
$ rad patch review aa45913 -m "Looks reasonable, no strong opinion." --no-announce
✓ Patch aa45913 reviewed
```
Submitting a second review on the same revision by the same author is ignored.
Currently, there is no way to edit a review from the CLI.
Note that the review command will still succeed, but with no effect:
```
$ rad patch review aa45913 -m "Changed my mind." --no-announce
✓ Patch aa45913 reviewed
```

View File

@ -17,6 +17,7 @@ mod review;
mod show; mod show;
mod update; mod update;
use core::convert::TryInto;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use anyhow::anyhow; use anyhow::anyhow;
@ -196,7 +197,13 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
.map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend)) .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
.transpose()? .transpose()?
.map(patch::RevisionId::from); .map(patch::RevisionId::from);
review::run(patch_id, revision_id, options.into(), &profile, &repository)?; review::run(
patch_id,
revision_id,
options.try_into()?,
&profile,
&repository,
)?;
} }
Command::Resolve { Command::Resolve {

View File

@ -1,3 +1,5 @@
use core::result::Result::Err;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use radicle::cob::Label; use radicle::cob::Label;
@ -588,8 +590,15 @@ pub(super) struct ReviewArgs {
message_args: MessageArgs, message_args: MessageArgs,
} }
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum OperationError {
#[error("expected one of `--accept` or `--reject`, or supply a review message")]
MissingOption,
}
impl ReviewArgs { impl ReviewArgs {
fn as_operation(&self) -> review::Operation { fn as_operation(&self, message: &Message) -> Result<review::Operation, OperationError> {
let Self { let Self {
patch, patch,
accept, accept,
@ -606,51 +615,60 @@ impl ReviewArgs {
} else { } else {
None None
}; };
return review::Operation::Review(review::ReviewOptions { return Ok(review::Operation::Review(review::ReviewOptions {
by_hunk: true, by_hunk: true,
unified: self.unified, unified: self.unified,
hunk: self.hunk, hunk: self.hunk,
verdict, verdict,
}); }));
} }
if *delete { if *delete {
return review::Operation::Delete; return Ok(review::Operation::Delete);
} }
if *accept { if *accept {
return review::Operation::Review(review::ReviewOptions { return Ok(review::Operation::Review(review::ReviewOptions {
by_hunk: false, by_hunk: false,
unified: 3, unified: 3,
hunk: None, hunk: None,
verdict: Some(Verdict::Accept), verdict: Some(Verdict::Accept),
}); }));
} }
if *reject { if *reject {
return review::Operation::Review(review::ReviewOptions { return Ok(review::Operation::Review(review::ReviewOptions {
by_hunk: false, by_hunk: false,
unified: 3, unified: 3,
hunk: None, hunk: None,
verdict: Some(Verdict::Reject), verdict: Some(Verdict::Reject),
}); }));
} }
panic!("expected one of `--patch`, `--delete`, `--accept`, or `--reject`"); if matches!(message, Message::Edit | Message::Text(_)) {
return Ok(review::Operation::Review(review::ReviewOptions {
by_hunk: false,
unified: self.unified,
hunk: self.hunk,
verdict: None,
}));
}
Err(OperationError::MissingOption)
} }
} }
impl From<ReviewArgs> for review::Options { impl TryFrom<ReviewArgs> for review::Options {
fn from(args: ReviewArgs) -> Self { type Error = OperationError;
let op = args.as_operation();
Self { fn try_from(args: ReviewArgs) -> Result<Self, Self::Error> {
message: Message::from(args.message_args), let message = Message::from(args.message_args.clone());
op, let op = args.as_operation(&message)?;
} Ok(Self { message, op })
} }
} }
#[derive(Debug, clap::Args)] #[derive(Clone, Debug, clap::Args)]
#[group(required = false, multiple = false)] #[group(required = false, multiple = false)]
pub(super) struct MessageArgs { pub(super) struct MessageArgs {
/// Provide a message (default: prompt) /// Provide a message (default: prompt)

View File

@ -27,6 +27,11 @@ fn rad_patch_checkout() {
Environment::alice(["rad-init", "rad-patch-checkout"]); Environment::alice(["rad-init", "rad-patch-checkout"]);
} }
#[test]
fn rad_patch_review_no_options() {
Environment::alice(["rad-init", "rad-patch", "rad-patch-review-no-options"]);
}
#[test] #[test]
fn rad_patch_checkout_revision() { fn rad_patch_checkout_revision() {
Environment::alice([ Environment::alice([