1use core::result::Result::Err;
2
3use clap::{Parser, Subcommand};
4
5use radicle::cob::Label;
6use radicle::git;
7use radicle::git::fmt::RefString;
8use radicle::patch::Status;
9use radicle::patch::Verdict;
10use radicle::prelude::Did;
11use radicle::prelude::RepoId;
12
13use crate::commands::patch::checkout;
14use crate::commands::patch::review;
15
16use crate::git::Rev;
17use crate::terminal::patch::Message;
18
19const ABOUT: &str = "Manage patches";
20
21#[derive(Debug, Parser)]
22#[command(about = ABOUT, disable_version_flag = true)]
23pub struct Args {
24 #[command(subcommand)]
25 pub(super) command: Option<Command>,
26
27 #[arg(short, long, global = true)]
29 pub(super) quiet: bool,
30
31 #[arg(long, global = true, conflicts_with = "no_announce")]
33 announce: bool,
34
35 #[arg(long, global = true, conflicts_with = "announce")]
37 no_announce: bool,
38
39 #[arg(long, global = true, value_name = "RID")]
41 pub(super) repo: Option<RepoId>,
42
43 #[arg(long, short, global = true)]
45 pub(super) verbose: bool,
46
47 #[clap(flatten)]
50 pub(super) empty: EmptyArgs,
51}
52
53impl Args {
54 pub(super) fn should_announce(&self) -> bool {
55 self.announce || !self.no_announce
56 }
57}
58
59#[derive(Subcommand, Debug)]
61pub(super) enum Command {
62 #[command(alias = "l")]
64 List(ListArgs),
65
66 #[command(alias = "s")]
68 Show {
69 #[arg(value_name = "PATCH_ID")]
71 id: Rev,
72
73 #[arg(long, short)]
75 patch: bool,
76
77 #[arg(long, short)]
79 verbose: bool,
80 },
81
82 Diff {
86 #[arg(value_name = "PATCH_ID")]
88 id: Rev,
89
90 #[arg(long, short)]
95 revision: Option<Rev>,
96 },
97
98 #[command(alias = "a")]
100 Archive {
101 #[arg(value_name = "PATCH_ID")]
103 id: Rev,
104
105 #[arg(long)]
109 undo: bool,
110 },
111
112 #[command(alias = "u")]
114 Update {
115 #[arg(value_name = "PATCH_ID")]
117 id: Rev,
118
119 #[arg(long, short, value_name = "REVSPEC")]
121 base: Option<Rev>,
122
123 #[clap(flatten)]
125 message: MessageArgs,
126 },
127
128 #[command(alias = "c")]
133 Checkout {
134 #[arg(value_name = "PATCH_ID")]
136 id: Rev,
137
138 #[arg(long)]
140 revision: Option<Rev>,
141
142 #[clap(flatten)]
143 opts: CheckoutArgs,
144 },
145
146 Review {
148 #[arg(value_name = "PATCH_ID")]
150 id: Rev,
151
152 #[arg(long, short)]
156 revision: Option<Rev>,
157
158 #[clap(flatten)]
159 options: ReviewArgs,
160 },
161
162 Resolve {
164 #[arg(value_name = "PATCH_ID")]
166 id: Rev,
167
168 #[arg(long, value_name = "REVIEW_ID")]
170 review: Rev,
171
172 #[arg(long, value_name = "COMMENT_ID")]
174 comment: Rev,
175
176 #[arg(long)]
178 unresolve: bool,
179 },
180
181 #[command(alias = "d")]
187 Delete {
188 #[arg(value_name = "PATCH_ID")]
190 id: Rev,
191 },
192
193 #[command(alias = "r")]
195 Redact {
196 #[arg(value_name = "REVISION_ID")]
198 id: Rev,
199 },
200
201 React {
203 #[arg(value_name = "PATCH_ID|REVISION_ID")]
205 id: Rev,
206
207 #[arg(long, value_name = "CHAR")]
209 emoji: radicle::cob::Reaction,
210
211 #[arg(long)]
213 undo: bool,
214 },
215
216 Assign {
218 #[arg(value_name = "PATCH_ID")]
220 id: Rev,
221
222 #[clap(flatten)]
223 args: AssignArgs,
224 },
225
226 Label {
228 #[arg(value_name = "PATCH_ID")]
230 id: Rev,
231
232 #[clap(flatten)]
233 args: LabelArgs,
234 },
235
236 #[command(alias = "y")]
238 Ready {
239 #[arg(value_name = "PATCH_ID")]
241 id: Rev,
242
243 #[arg(long)]
245 undo: bool,
246 },
247
248 #[command(alias = "e")]
249 Edit {
250 #[arg(value_name = "PATCH_ID")]
252 id: Rev,
253
254 #[arg(long, value_name = "REVISION_ID")]
256 revision: Option<Rev>,
257
258 #[clap(flatten)]
259 message: MessageArgs,
260 },
261
262 Set {
264 #[arg(value_name = "PATCH_ID")]
266 id: Rev,
267
268 #[arg(long, value_name = "REF", value_parser = parse_refstr)]
270 remote: Option<RefString>,
271 },
272
273 Comment(CommentArgs),
275
276 Cache {
278 #[arg(value_name = "PATCH_ID")]
280 id: Option<Rev>,
281
282 #[arg(long)]
284 storage: bool,
285 },
286}
287
288impl Command {
289 pub(super) fn should_announce(&self) -> bool {
290 match self {
291 Self::Update { .. }
292 | Self::Archive { .. }
293 | Self::Ready { .. }
294 | Self::Delete { .. }
295 | Self::Comment { .. }
296 | Self::Review { .. }
297 | Self::Resolve { .. }
298 | Self::Assign { .. }
299 | Self::Label { .. }
300 | Self::Edit { .. }
301 | Self::Redact { .. }
302 | Self::React { .. }
303 | Self::Set { .. } => true,
304 Self::Show { .. }
305 | Self::Diff { .. }
306 | Self::Checkout { .. }
307 | Self::List { .. }
308 | Self::Cache { .. } => false,
309 }
310 }
311}
312
313#[derive(Parser, Debug)]
314pub(super) struct CommentArgs {
315 #[arg(value_name = "REVISION_ID")]
317 revision: Rev,
318
319 #[clap(flatten)]
320 message: MessageArgs,
321
322 #[arg(
326 long,
327 value_name = "COMMENT_ID",
328 conflicts_with = "react",
329 conflicts_with = "redact"
330 )]
331 edit: Option<Rev>,
332
333 #[arg(
339 long,
340 value_name = "COMMENT_ID",
341 conflicts_with = "edit",
342 conflicts_with = "redact",
343 requires = "emoji",
344 group = "reaction"
345 )]
346 react: Option<Rev>,
347
348 #[arg(
350 long,
351 value_name = "COMMENT_ID",
352 conflicts_with = "react",
353 conflicts_with = "edit"
354 )]
355 redact: Option<Rev>,
356
357 #[arg(long, requires = "reaction")]
361 emoji: Option<radicle::cob::Reaction>,
362
363 #[arg(long, value_name = "COMMENT_ID")]
365 reply_to: Option<Rev>,
366
367 #[arg(long, requires = "reaction")]
371 undo: bool,
372}
373
374#[derive(Debug)]
375pub(super) enum CommentAction {
376 Comment {
377 revision: Rev,
378 message: Message,
379 reply_to: Option<Rev>,
380 },
381 Edit {
382 revision: Rev,
383 comment: Rev,
384 message: Message,
385 },
386 Redact {
387 revision: Rev,
388 comment: Rev,
389 },
390 React {
391 revision: Rev,
392 comment: Rev,
393 emoji: radicle::cob::Reaction,
394 undo: bool,
395 },
396}
397
398impl From<CommentArgs> for CommentAction {
399 fn from(
400 CommentArgs {
401 revision,
402 message,
403 edit,
404 react,
405 redact,
406 reply_to,
407 emoji,
408 undo,
409 }: CommentArgs,
410 ) -> Self {
411 match (edit, react, redact) {
412 (Some(edit), None, None) => CommentAction::Edit {
413 revision,
414 comment: edit,
415 message: Message::from(message),
416 },
417 (None, Some(react), None) => CommentAction::React {
418 revision,
419 comment: react,
420 emoji: emoji.expect("emoji must be Some when react is Some"),
421 undo,
422 },
423 (None, None, Some(redact)) => CommentAction::Redact {
424 revision,
425 comment: redact,
426 },
427 (None, None, None) => Self::Comment {
428 revision,
429 message: Message::from(message),
430 reply_to,
431 },
432 _ => unreachable!("`--edit`, `--react`, and `--redact` cannot be used together"),
433 }
434 }
435}
436
437#[derive(Parser, Debug, Default)]
438pub(super) struct EmptyArgs {
439 #[arg(long, hide = true, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
440 authors: Vec<Did>,
441
442 #[arg(long, hide = true)]
443 authored: bool,
444
445 #[clap(flatten)]
446 state: EmptyStateArgs,
447}
448
449#[derive(Parser, Debug, Default)]
450#[group(multiple = false)]
451pub(super) struct EmptyStateArgs {
452 #[arg(long, hide = true)]
453 all: bool,
454
455 #[arg(long, hide = true)]
456 draft: bool,
457
458 #[arg(long, hide = true)]
459 open: bool,
460
461 #[arg(long, hide = true)]
462 merged: bool,
463
464 #[arg(long, hide = true)]
465 archived: bool,
466}
467
468#[derive(Parser, Debug, Default)]
469pub(super) struct ListArgs {
470 #[arg(
473 long = "author",
474 value_name = "DID",
475 num_args = 1..,
476 action = clap::ArgAction::Append,
477 )]
478 pub(super) authors: Vec<Did>,
479
480 #[arg(long)]
482 pub(super) authored: bool,
483
484 #[clap(flatten)]
485 pub(super) state: ListStateArgs,
486}
487
488impl From<EmptyArgs> for ListArgs {
489 fn from(args: EmptyArgs) -> Self {
490 Self {
491 authors: args.authors,
492 authored: args.authored,
493 state: ListStateArgs::from(args.state),
494 }
495 }
496}
497
498#[derive(Parser, Debug, Default)]
499#[group(multiple = false)]
500pub(crate) struct ListStateArgs {
501 #[arg(long)]
503 pub(crate) all: bool,
504
505 #[arg(long)]
507 pub(crate) draft: bool,
508
509 #[arg(long)]
511 pub(crate) open: bool,
512
513 #[arg(long)]
515 pub(crate) merged: bool,
516
517 #[arg(long)]
519 pub(crate) archived: bool,
520}
521
522impl From<EmptyStateArgs> for ListStateArgs {
523 fn from(args: EmptyStateArgs) -> Self {
524 Self {
525 all: args.all,
526 draft: args.draft,
527 open: args.open,
528 merged: args.merged,
529 archived: args.archived,
530 }
531 }
532}
533
534impl From<&ListStateArgs> for Option<&Status> {
535 fn from(args: &ListStateArgs) -> Self {
536 match (args.all, args.draft, args.open, args.merged, args.archived) {
537 (true, false, false, false, false) => None,
538 (false, true, false, false, false) => Some(&Status::Draft),
539 (false, false, true, false, false) | (false, false, false, false, false) => {
540 Some(&Status::Open)
541 }
542 (false, false, false, true, false) => Some(&Status::Merged),
543 (false, false, false, false, true) => Some(&Status::Archived),
544 _ => unreachable!(),
545 }
546 }
547}
548
549#[derive(Debug, Parser)]
550pub(super) struct ReviewArgs {
551 #[arg(long, short, group = "by-hunk", conflicts_with = "delete")]
555 patch: bool,
556
557 #[arg(
561 long,
562 short = 'U',
563 value_name = "N",
564 requires = "by-hunk",
565 default_value_t = 3
566 )]
567 unified: usize,
568
569 #[arg(long, value_name = "INDEX", requires = "by-hunk")]
573 hunk: Option<usize>,
574
575 #[arg(long, conflicts_with = "reject", conflicts_with = "delete")]
577 accept: bool,
578
579 #[arg(long, conflicts_with = "delete")]
581 reject: bool,
582
583 #[arg(long, short)]
587 delete: bool,
588
589 #[clap(flatten)]
590 message_args: MessageArgs,
591}
592
593#[derive(Debug, thiserror::Error)]
594#[non_exhaustive]
595pub enum OperationError {
596 #[error("expected one of `--accept` or `--reject`, or supply a review message")]
597 MissingOption,
598}
599
600impl ReviewArgs {
601 fn as_operation(&self, message: &Message) -> Result<review::Operation, OperationError> {
602 let Self {
603 patch,
604 accept,
605 reject,
606 delete,
607 ..
608 } = self;
609
610 if *patch {
611 let verdict = if *accept {
612 Some(Verdict::Accept)
613 } else if *reject {
614 Some(Verdict::Reject)
615 } else {
616 None
617 };
618 return Ok(review::Operation::Review(review::ReviewOptions {
619 by_hunk: true,
620 unified: self.unified,
621 hunk: self.hunk,
622 verdict,
623 }));
624 }
625
626 if *delete {
627 return Ok(review::Operation::Delete);
628 }
629
630 if *accept {
631 return Ok(review::Operation::Review(review::ReviewOptions {
632 by_hunk: false,
633 unified: 3,
634 hunk: None,
635 verdict: Some(Verdict::Accept),
636 }));
637 }
638
639 if *reject {
640 return Ok(review::Operation::Review(review::ReviewOptions {
641 by_hunk: false,
642 unified: 3,
643 hunk: None,
644 verdict: Some(Verdict::Reject),
645 }));
646 }
647
648 if matches!(message, Message::Edit | Message::Text(_)) {
649 return Ok(review::Operation::Review(review::ReviewOptions {
650 by_hunk: false,
651 unified: self.unified,
652 hunk: self.hunk,
653 verdict: None,
654 }));
655 }
656
657 Err(OperationError::MissingOption)
658 }
659}
660
661impl TryFrom<ReviewArgs> for review::Options {
662 type Error = OperationError;
663
664 fn try_from(args: ReviewArgs) -> Result<Self, Self::Error> {
665 let message = Message::from(args.message_args.clone());
666 let op = args.as_operation(&message)?;
667 Ok(Self { message, op })
668 }
669}
670
671#[derive(Clone, Debug, clap::Args)]
672#[group(required = false, multiple = false)]
673pub(super) struct MessageArgs {
674 #[clap(
679 long,
680 short,
681 value_name = "MESSAGE",
682 num_args = 1..,
683 action = clap::ArgAction::Append
684 )]
685 pub(super) message: Option<Vec<String>>,
686
687 #[arg(long, conflicts_with = "message")]
689 pub(super) no_message: bool,
690}
691
692impl From<MessageArgs> for Message {
693 fn from(
694 MessageArgs {
695 message,
696 no_message,
697 }: MessageArgs,
698 ) -> Self {
699 if no_message {
700 assert!(message.is_none());
701 return Self::Blank;
702 }
703
704 match message {
705 Some(messages) => messages.into_iter().fold(Self::Blank, |mut result, m| {
706 result.append(&m);
707 result
708 }),
709 None => Self::Edit,
710 }
711 }
712}
713
714#[derive(Debug, clap::Args)]
715pub(super) struct CheckoutArgs {
716 #[arg(long, value_name = "BRANCH", value_parser = parse_refstr)]
718 pub(super) name: Option<RefString>,
719
720 #[arg(long, value_parser = parse_refstr)]
722 pub(super) remote: Option<RefString>,
723
724 #[arg(long, short)]
726 pub(super) force: bool,
727}
728
729impl From<CheckoutArgs> for checkout::Options {
730 fn from(value: CheckoutArgs) -> Self {
731 Self {
732 name: value.name,
733 remote: value.remote,
734 force: value.force,
735 }
736 }
737}
738
739#[derive(Parser, Debug)]
740#[group(required = true)]
741pub(super) struct AssignArgs {
742 #[arg(long, short, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
746 pub(super) add: Vec<Did>,
747
748 #[clap(long, short, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
752 pub(super) delete: Vec<Did>,
753}
754
755#[derive(Parser, Debug)]
756#[group(required = true)]
757pub(super) struct LabelArgs {
758 #[arg(long, short, value_name = "LABEL", num_args = 1.., action = clap::ArgAction::Append)]
762 pub(super) add: Vec<Label>,
763
764 #[clap(long, short, value_name = "LABEL", num_args = 1.., action = clap::ArgAction::Append)]
768 pub(super) delete: Vec<Label>,
769}
770
771fn parse_refstr(refstr: &str) -> Result<RefString, git::fmt::Error> {
772 RefString::try_from(refstr)
773}