1use clap::{Parser, Subcommand};
2
3use radicle::cob::Label;
4use radicle::git;
5use radicle::git::fmt::RefString;
6use radicle::patch::Status;
7use radicle::patch::Verdict;
8use radicle::prelude::Did;
9use radicle::prelude::RepoId;
10
11use crate::commands::patch::checkout;
12use crate::commands::patch::review;
13
14use crate::git::Rev;
15use crate::terminal::patch::Message;
16
17const ABOUT: &str = "Manage patches";
18
19#[derive(Debug, Parser)]
20#[command(about = ABOUT, disable_version_flag = true)]
21pub struct Args {
22 #[command(subcommand)]
23 pub(super) command: Option<Command>,
24
25 #[arg(short, long, global = true)]
27 pub(super) quiet: bool,
28
29 #[arg(long, global = true, conflicts_with = "no_announce")]
31 announce: bool,
32
33 #[arg(long, global = true, conflicts_with = "announce")]
35 no_announce: bool,
36
37 #[arg(long, global = true, value_name = "RID")]
39 pub(super) repo: Option<RepoId>,
40
41 #[arg(long, short, global = true)]
43 pub(super) verbose: bool,
44
45 #[clap(flatten)]
48 pub(super) empty: EmptyArgs,
49}
50
51impl Args {
52 pub(super) fn should_announce(&self) -> bool {
53 self.announce || !self.no_announce
54 }
55}
56
57#[derive(Subcommand, Debug)]
59pub(super) enum Command {
60 #[command(alias = "l")]
62 List(ListArgs),
63
64 #[command(alias = "s")]
66 Show {
67 #[arg(value_name = "PATCH_ID")]
69 id: Rev,
70
71 #[arg(long, short)]
73 patch: bool,
74
75 #[arg(long, short)]
77 verbose: bool,
78 },
79
80 Diff {
84 #[arg(value_name = "PATCH_ID")]
86 id: Rev,
87
88 #[arg(long, short)]
93 revision: Option<Rev>,
94 },
95
96 #[command(alias = "a")]
98 Archive {
99 #[arg(value_name = "PATCH_ID")]
101 id: Rev,
102
103 #[arg(long)]
107 undo: bool,
108 },
109
110 #[command(alias = "u")]
112 Update {
113 #[arg(value_name = "PATCH_ID")]
115 id: Rev,
116
117 #[arg(long, short, value_name = "REVSPEC")]
119 base: Option<Rev>,
120
121 #[clap(flatten)]
123 message: MessageArgs,
124 },
125
126 #[command(alias = "c")]
131 Checkout {
132 #[arg(value_name = "PATCH_ID")]
134 id: Rev,
135
136 #[arg(long)]
138 revision: Option<Rev>,
139
140 #[clap(flatten)]
141 opts: CheckoutArgs,
142 },
143
144 Review {
146 #[arg(value_name = "PATCH_ID")]
148 id: Rev,
149
150 #[arg(long, short)]
154 revision: Option<Rev>,
155
156 #[clap(flatten)]
157 options: ReviewArgs,
158 },
159
160 Resolve {
162 #[arg(value_name = "PATCH_ID")]
164 id: Rev,
165
166 #[arg(long, value_name = "REVIEW_ID")]
168 review: Rev,
169
170 #[arg(long, value_name = "COMMENT_ID")]
172 comment: Rev,
173
174 #[arg(long)]
176 unresolve: bool,
177 },
178
179 #[command(alias = "d")]
185 Delete {
186 #[arg(value_name = "PATCH_ID")]
188 id: Rev,
189 },
190
191 #[command(alias = "r")]
193 Redact {
194 #[arg(value_name = "REVISION_ID")]
196 id: Rev,
197 },
198
199 React {
201 #[arg(value_name = "PATCH_ID|REVISION_ID")]
203 id: Rev,
204
205 #[arg(long, value_name = "CHAR")]
207 emoji: radicle::cob::Reaction,
208
209 #[arg(long)]
211 undo: bool,
212 },
213
214 Assign {
216 #[arg(value_name = "PATCH_ID")]
218 id: Rev,
219
220 #[clap(flatten)]
221 args: AssignArgs,
222 },
223
224 Label {
226 #[arg(value_name = "PATCH_ID")]
228 id: Rev,
229
230 #[clap(flatten)]
231 args: LabelArgs,
232 },
233
234 #[command(alias = "y")]
236 Ready {
237 #[arg(value_name = "PATCH_ID")]
239 id: Rev,
240
241 #[arg(long)]
243 undo: bool,
244 },
245
246 #[command(alias = "e")]
247 Edit {
248 #[arg(value_name = "PATCH_ID")]
250 id: Rev,
251
252 #[arg(long, value_name = "REVISION_ID")]
254 revision: Option<Rev>,
255
256 #[clap(flatten)]
257 message: MessageArgs,
258 },
259
260 Set {
262 #[arg(value_name = "PATCH_ID")]
264 id: Rev,
265
266 #[arg(long, value_name = "REF", value_parser = parse_refstr)]
268 remote: Option<RefString>,
269 },
270
271 Comment(CommentArgs),
273
274 Cache {
276 #[arg(value_name = "PATCH_ID")]
278 id: Option<Rev>,
279
280 #[arg(long)]
282 storage: bool,
283 },
284}
285
286impl Command {
287 pub(super) fn should_announce(&self) -> bool {
288 match self {
289 Self::Update { .. }
290 | Self::Archive { .. }
291 | Self::Ready { .. }
292 | Self::Delete { .. }
293 | Self::Comment { .. }
294 | Self::Review { .. }
295 | Self::Resolve { .. }
296 | Self::Assign { .. }
297 | Self::Label { .. }
298 | Self::Edit { .. }
299 | Self::Redact { .. }
300 | Self::React { .. }
301 | Self::Set { .. } => true,
302 Self::Show { .. }
303 | Self::Diff { .. }
304 | Self::Checkout { .. }
305 | Self::List { .. }
306 | Self::Cache { .. } => false,
307 }
308 }
309}
310
311#[derive(Parser, Debug)]
312pub(super) struct CommentArgs {
313 #[arg(value_name = "REVISION_ID")]
315 revision: Rev,
316
317 #[clap(flatten)]
318 message: MessageArgs,
319
320 #[arg(
324 long,
325 value_name = "COMMENT_ID",
326 conflicts_with = "react",
327 conflicts_with = "redact"
328 )]
329 edit: Option<Rev>,
330
331 #[arg(
337 long,
338 value_name = "COMMENT_ID",
339 conflicts_with = "edit",
340 conflicts_with = "redact",
341 requires = "emoji",
342 group = "reaction"
343 )]
344 react: Option<Rev>,
345
346 #[arg(
348 long,
349 value_name = "COMMENT_ID",
350 conflicts_with = "react",
351 conflicts_with = "edit"
352 )]
353 redact: Option<Rev>,
354
355 #[arg(long, requires = "reaction")]
359 emoji: Option<radicle::cob::Reaction>,
360
361 #[arg(long, value_name = "COMMENT_ID")]
363 reply_to: Option<Rev>,
364
365 #[arg(long, requires = "reaction")]
369 undo: bool,
370}
371
372#[derive(Debug)]
373pub(super) enum CommentAction {
374 Comment {
375 revision: Rev,
376 message: Message,
377 reply_to: Option<Rev>,
378 },
379 Edit {
380 revision: Rev,
381 comment: Rev,
382 message: Message,
383 },
384 Redact {
385 revision: Rev,
386 comment: Rev,
387 },
388 React {
389 revision: Rev,
390 comment: Rev,
391 emoji: radicle::cob::Reaction,
392 undo: bool,
393 },
394}
395
396impl From<CommentArgs> for CommentAction {
397 fn from(
398 CommentArgs {
399 revision,
400 message,
401 edit,
402 react,
403 redact,
404 reply_to,
405 emoji,
406 undo,
407 }: CommentArgs,
408 ) -> Self {
409 match (edit, react, redact) {
410 (Some(edit), None, None) => CommentAction::Edit {
411 revision,
412 comment: edit,
413 message: Message::from(message),
414 },
415 (None, Some(react), None) => CommentAction::React {
416 revision,
417 comment: react,
418 emoji: emoji.expect("emoji must be Some when react is Some"),
419 undo,
420 },
421 (None, None, Some(redact)) => CommentAction::Redact {
422 revision,
423 comment: redact,
424 },
425 (None, None, None) => Self::Comment {
426 revision,
427 message: Message::from(message),
428 reply_to,
429 },
430 _ => unreachable!("`--edit`, `--react`, and `--redact` cannot be used together"),
431 }
432 }
433}
434
435#[derive(Parser, Debug, Default)]
436pub(super) struct EmptyArgs {
437 #[arg(long, hide = true, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
438 authors: Vec<Did>,
439
440 #[arg(long, hide = true)]
441 authored: bool,
442
443 #[clap(flatten)]
444 state: EmptyStateArgs,
445}
446
447#[derive(Parser, Debug, Default)]
448#[group(multiple = false)]
449pub(super) struct EmptyStateArgs {
450 #[arg(long, hide = true)]
451 all: bool,
452
453 #[arg(long, hide = true)]
454 draft: bool,
455
456 #[arg(long, hide = true)]
457 open: bool,
458
459 #[arg(long, hide = true)]
460 merged: bool,
461
462 #[arg(long, hide = true)]
463 archived: bool,
464}
465
466#[derive(Parser, Debug, Default)]
467pub(super) struct ListArgs {
468 #[arg(
471 long = "author",
472 value_name = "DID",
473 num_args = 1..,
474 action = clap::ArgAction::Append,
475 )]
476 pub(super) authors: Vec<Did>,
477
478 #[arg(long)]
480 pub(super) authored: bool,
481
482 #[clap(flatten)]
483 pub(super) state: ListStateArgs,
484}
485
486impl From<EmptyArgs> for ListArgs {
487 fn from(args: EmptyArgs) -> Self {
488 Self {
489 authors: args.authors,
490 authored: args.authored,
491 state: ListStateArgs::from(args.state),
492 }
493 }
494}
495
496#[derive(Parser, Debug, Default)]
497#[group(multiple = false)]
498pub(crate) struct ListStateArgs {
499 #[arg(long)]
501 pub(crate) all: bool,
502
503 #[arg(long)]
505 pub(crate) draft: bool,
506
507 #[arg(long)]
509 pub(crate) open: bool,
510
511 #[arg(long)]
513 pub(crate) merged: bool,
514
515 #[arg(long)]
517 pub(crate) archived: bool,
518}
519
520impl From<EmptyStateArgs> for ListStateArgs {
521 fn from(args: EmptyStateArgs) -> Self {
522 Self {
523 all: args.all,
524 draft: args.draft,
525 open: args.open,
526 merged: args.merged,
527 archived: args.archived,
528 }
529 }
530}
531
532impl From<&ListStateArgs> for Option<&Status> {
533 fn from(args: &ListStateArgs) -> Self {
534 match (args.all, args.draft, args.open, args.merged, args.archived) {
535 (true, false, false, false, false) => None,
536 (false, true, false, false, false) => Some(&Status::Draft),
537 (false, false, true, false, false) | (false, false, false, false, false) => {
538 Some(&Status::Open)
539 }
540 (false, false, false, true, false) => Some(&Status::Merged),
541 (false, false, false, false, true) => Some(&Status::Archived),
542 _ => unreachable!(),
543 }
544 }
545}
546
547#[derive(Debug, Parser)]
548pub(super) struct ReviewArgs {
549 #[arg(long, short, group = "by-hunk", conflicts_with = "delete")]
553 patch: bool,
554
555 #[arg(
559 long,
560 short = 'U',
561 value_name = "N",
562 requires = "by-hunk",
563 default_value_t = 3
564 )]
565 unified: usize,
566
567 #[arg(long, value_name = "INDEX", requires = "by-hunk")]
571 hunk: Option<usize>,
572
573 #[arg(long, conflicts_with = "reject", conflicts_with = "delete")]
575 accept: bool,
576
577 #[arg(long, conflicts_with = "delete")]
579 reject: bool,
580
581 #[arg(long, short)]
585 delete: bool,
586
587 #[clap(flatten)]
588 message_args: MessageArgs,
589}
590
591impl ReviewArgs {
592 fn as_operation(&self) -> review::Operation {
593 let Self {
594 patch,
595 accept,
596 reject,
597 delete,
598 ..
599 } = self;
600
601 if *patch {
602 let verdict = if *accept {
603 Some(Verdict::Accept)
604 } else if *reject {
605 Some(Verdict::Reject)
606 } else {
607 None
608 };
609 return review::Operation::Review(review::ReviewOptions {
610 by_hunk: true,
611 unified: self.unified,
612 hunk: self.hunk,
613 verdict,
614 });
615 }
616
617 if *delete {
618 return review::Operation::Delete;
619 }
620
621 if *accept {
622 return review::Operation::Review(review::ReviewOptions {
623 by_hunk: false,
624 unified: 3,
625 hunk: None,
626 verdict: Some(Verdict::Accept),
627 });
628 }
629
630 if *reject {
631 return review::Operation::Review(review::ReviewOptions {
632 by_hunk: false,
633 unified: 3,
634 hunk: None,
635 verdict: Some(Verdict::Reject),
636 });
637 }
638
639 panic!("expected one of `--patch`, `--delete`, `--accept`, or `--reject`");
640 }
641}
642
643impl From<ReviewArgs> for review::Options {
644 fn from(args: ReviewArgs) -> Self {
645 let op = args.as_operation();
646 Self {
647 message: Message::from(args.message_args),
648 op,
649 }
650 }
651}
652
653#[derive(Debug, clap::Args)]
654#[group(required = false, multiple = false)]
655pub(super) struct MessageArgs {
656 #[clap(
661 long,
662 short,
663 value_name = "MESSAGE",
664 num_args = 1..,
665 action = clap::ArgAction::Append
666 )]
667 pub(super) message: Option<Vec<String>>,
668
669 #[arg(long, conflicts_with = "message")]
671 pub(super) no_message: bool,
672}
673
674impl From<MessageArgs> for Message {
675 fn from(
676 MessageArgs {
677 message,
678 no_message,
679 }: MessageArgs,
680 ) -> Self {
681 if no_message {
682 assert!(message.is_none());
683 return Self::Blank;
684 }
685
686 match message {
687 Some(messages) => messages.into_iter().fold(Self::Blank, |mut result, m| {
688 result.append(&m);
689 result
690 }),
691 None => Self::Edit,
692 }
693 }
694}
695
696#[derive(Debug, clap::Args)]
697pub(super) struct CheckoutArgs {
698 #[arg(long, value_name = "BRANCH", value_parser = parse_refstr)]
700 pub(super) name: Option<RefString>,
701
702 #[arg(long, value_parser = parse_refstr)]
704 pub(super) remote: Option<RefString>,
705
706 #[arg(long, short)]
708 pub(super) force: bool,
709}
710
711impl From<CheckoutArgs> for checkout::Options {
712 fn from(value: CheckoutArgs) -> Self {
713 Self {
714 name: value.name,
715 remote: value.remote,
716 force: value.force,
717 }
718 }
719}
720
721#[derive(Parser, Debug)]
722#[group(required = true)]
723pub(super) struct AssignArgs {
724 #[arg(long, short, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
728 pub(super) add: Vec<Did>,
729
730 #[clap(long, short, value_name = "DID", num_args = 1.., action = clap::ArgAction::Append)]
734 pub(super) delete: Vec<Did>,
735}
736
737#[derive(Parser, Debug)]
738#[group(required = true)]
739pub(super) struct LabelArgs {
740 #[arg(long, short, value_name = "LABEL", num_args = 1.., action = clap::ArgAction::Append)]
744 pub(super) add: Vec<Label>,
745
746 #[clap(long, short, value_name = "LABEL", num_args = 1.., action = clap::ArgAction::Append)]
750 pub(super) delete: Vec<Label>,
751}
752
753fn parse_refstr(refstr: &str) -> Result<RefString, git::fmt::Error> {
754 RefString::try_from(refstr)
755}