1#[path = "patch/archive.rs"]
2mod archive;
3#[path = "patch/assign.rs"]
4mod assign;
5#[path = "patch/cache.rs"]
6mod cache;
7#[path = "patch/checkout.rs"]
8mod checkout;
9#[path = "patch/comment.rs"]
10mod comment;
11#[path = "patch/delete.rs"]
12mod delete;
13#[path = "patch/diff.rs"]
14mod diff;
15#[path = "patch/edit.rs"]
16mod edit;
17#[path = "patch/label.rs"]
18mod label;
19#[path = "patch/list.rs"]
20mod list;
21#[path = "patch/react.rs"]
22mod react;
23#[path = "patch/ready.rs"]
24mod ready;
25#[path = "patch/redact.rs"]
26mod redact;
27#[path = "patch/resolve.rs"]
28mod resolve;
29#[path = "patch/review.rs"]
30mod review;
31#[path = "patch/show.rs"]
32mod show;
33#[path = "patch/update.rs"]
34mod update;
35
36use std::collections::BTreeSet;
37use std::ffi::OsString;
38use std::str::FromStr as _;
39
40use anyhow::anyhow;
41
42use radicle::cob::patch::PatchId;
43use radicle::cob::{patch, Label, Reaction};
44use radicle::git::RefString;
45use radicle::patch::cache::Patches as _;
46use radicle::storage::git::transport;
47use radicle::{prelude::*, Node};
48
49use crate::git::Rev;
50use crate::node;
51use crate::terminal as term;
52use crate::terminal::args::{string, Args, Error, Help};
53use crate::terminal::patch::Message;
54
55pub const HELP: Help = Help {
56 name: "patch",
57 description: "Manage patches",
58 version: env!("RADICLE_VERSION"),
59 usage: r#"
60Usage
61
62 rad patch [<option>...]
63 rad patch list [--all|--merged|--open|--archived|--draft|--authored] [--author <did>]... [<option>...]
64 rad patch show <patch-id> [<option>...]
65 rad patch diff <patch-id> [<option>...]
66 rad patch archive <patch-id> [--undo] [<option>...]
67 rad patch update <patch-id> [<option>...]
68 rad patch checkout <patch-id> [<option>...]
69 rad patch review <patch-id> [--accept | --reject] [-m [<string>]] [-d | --delete] [<option>...]
70 rad patch resolve <patch-id> [--review <review-id>] [--comment <comment-id>] [--unresolve] [<option>...]
71 rad patch delete <patch-id> [<option>...]
72 rad patch redact <revision-id> [<option>...]
73 rad patch react <patch-id | revision-id> [--react <emoji>] [<option>...]
74 rad patch assign <revision-id> [--add <did>] [--delete <did>] [<option>...]
75 rad patch label <revision-id> [--add <label>] [--delete <label>] [<option>...]
76 rad patch ready <patch-id> [--undo] [<option>...]
77 rad patch edit <patch-id> [<option>...]
78 rad patch set <patch-id> [<option>...]
79 rad patch comment <patch-id | revision-id> [<option>...]
80 rad patch cache [<patch-id>] [--storage] [<option>...]
81
82Show options
83
84 -p, --patch Show the actual patch diff
85 -v, --verbose Show additional information about the patch
86 --debug Show the patch as Rust debug output
87
88Diff options
89
90 -r, --revision <id> The revision to diff (default: latest)
91
92Comment options
93
94 -m, --message <string> Provide a comment message via the command-line
95 --reply-to <comment> The comment to reply to
96 --edit <comment> The comment to edit (use --message to edit with the provided message)
97 --react <comment> The comment to react to
98 --emoji <char> The emoji to react with when --react is used
99 --redact <comment> The comment to redact
100
101Edit options
102
103 -m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
104
105Review options
106
107 -r, --revision <id> Review the given revision of the patch
108 -p, --patch Review by patch hunks
109 --hunk <index> Only review a specific hunk
110 --accept Accept a patch or set of hunks
111 --reject Reject a patch or set of hunks
112 -U, --unified <n> Generate diffs with <n> lines of context instead of the usual three
113 -d, --delete Delete a review draft
114 -m, --message [<string>] Provide a comment with the review (default: prompt)
115
116Resolve options
117
118 --review <id> The review id which the comment is under
119 --comment <id> The comment to (un)resolve
120 --undo Unresolve the comment
121
122Assign options
123
124 -a, --add <did> Add an assignee to the patch (may be specified multiple times).
125 Note: --add will take precedence over --delete
126
127 -d, --delete <did> Delete an assignee from the patch (may be specified multiple times).
128 Note: --add will take precedence over --delete
129
130Archive options
131
132 --undo Unarchive a patch
133
134Label options
135
136 -a, --add <label> Add a label to the patch (may be specified multiple times).
137 Note: --add will take precedence over --delete
138
139 -d, --delete <label> Delete a label from the patch (may be specified multiple times).
140 Note: --add will take precedence over --delete
141
142Update options
143
144 -b, --base <revspec> Provide a Git revision as the base commit
145 -m, --message [<string>] Provide a comment message to the patch or revision (default: prompt)
146 --no-message Leave the patch or revision comment message blank
147
148List options
149
150 --all Show all patches, including merged and archived patches
151 --archived Show only archived patches
152 --merged Show only merged patches
153 --open Show only open patches (default)
154 --draft Show only draft patches
155 --authored Show only patches that you have authored
156 --author <did> Show only patched where the given user is an author
157 (may be specified multiple times)
158
159Ready options
160
161 --undo Convert a patch back to a draft
162
163Checkout options
164
165 --revision <id> Checkout the given revision of the patch
166 --name <string> Provide a name for the branch to checkout
167 --remote <string> Provide the git remote to use as the upstream
168 -f, --force Checkout the head of the revision, even if the branch already exists
169
170Set options
171
172 --remote <string> Provide the git remote to use as the upstream
173
174React options
175
176 --emoji <char> The emoji to react to the patch or revision with
177
178Other options
179
180 --repo <rid> Operate on the given repository (default: cwd)
181 --[no-]announce Announce changes made to the network
182 -q, --quiet Quiet output
183 --help Print help
184"#,
185};
186
187#[derive(Debug, Default, PartialEq, Eq)]
188pub enum OperationName {
189 Assign,
190 Show,
191 Diff,
192 Update,
193 Archive,
194 Delete,
195 Checkout,
196 Comment,
197 React,
198 Ready,
199 Review,
200 Resolve,
201 Label,
202 #[default]
203 List,
204 Edit,
205 Redact,
206 Set,
207 Cache,
208}
209
210#[derive(Debug, PartialEq, Eq)]
211pub enum CommentOperation {
212 Edit,
213 React,
214 Redact,
215}
216
217#[derive(Debug, Default, PartialEq, Eq)]
218pub struct AssignOptions {
219 pub add: BTreeSet<Did>,
220 pub delete: BTreeSet<Did>,
221}
222
223#[derive(Debug, Default, PartialEq, Eq)]
224pub struct LabelOptions {
225 pub add: BTreeSet<Label>,
226 pub delete: BTreeSet<Label>,
227}
228
229#[derive(Debug)]
230pub enum Operation {
231 Show {
232 patch_id: Rev,
233 diff: bool,
234 debug: bool,
235 },
236 Diff {
237 patch_id: Rev,
238 revision_id: Option<Rev>,
239 },
240 Update {
241 patch_id: Rev,
242 base_id: Option<Rev>,
243 message: Message,
244 },
245 Archive {
246 patch_id: Rev,
247 undo: bool,
248 },
249 Ready {
250 patch_id: Rev,
251 undo: bool,
252 },
253 Delete {
254 patch_id: Rev,
255 },
256 Checkout {
257 patch_id: Rev,
258 revision_id: Option<Rev>,
259 opts: checkout::Options,
260 },
261 Comment {
262 revision_id: Rev,
263 message: Message,
264 reply_to: Option<Rev>,
265 },
266 CommentEdit {
267 revision_id: Rev,
268 comment_id: Rev,
269 message: Message,
270 },
271 CommentRedact {
272 revision_id: Rev,
273 comment_id: Rev,
274 },
275 CommentReact {
276 revision_id: Rev,
277 comment_id: Rev,
278 reaction: Reaction,
279 undo: bool,
280 },
281 React {
282 revision_id: Rev,
283 reaction: Reaction,
284 undo: bool,
285 },
286 Review {
287 patch_id: Rev,
288 revision_id: Option<Rev>,
289 opts: review::Options,
290 },
291 Resolve {
292 patch_id: Rev,
293 review_id: Rev,
294 comment_id: Rev,
295 undo: bool,
296 },
297 Assign {
298 patch_id: Rev,
299 opts: AssignOptions,
300 },
301 Label {
302 patch_id: Rev,
303 opts: LabelOptions,
304 },
305 List {
306 filter: Option<patch::Status>,
307 },
308 Edit {
309 patch_id: Rev,
310 revision_id: Option<Rev>,
311 message: Message,
312 },
313 Redact {
314 revision_id: Rev,
315 },
316 Set {
317 patch_id: Rev,
318 remote: Option<RefString>,
319 },
320 Cache {
321 patch_id: Option<Rev>,
322 storage: bool,
323 },
324}
325
326impl Operation {
327 fn is_announce(&self) -> bool {
328 match self {
329 Operation::Update { .. }
330 | Operation::Archive { .. }
331 | Operation::Ready { .. }
332 | Operation::Delete { .. }
333 | Operation::Comment { .. }
334 | Operation::CommentEdit { .. }
335 | Operation::CommentRedact { .. }
336 | Operation::CommentReact { .. }
337 | Operation::Review { .. }
338 | Operation::Resolve { .. }
339 | Operation::Assign { .. }
340 | Operation::Label { .. }
341 | Operation::Edit { .. }
342 | Operation::Redact { .. }
343 | Operation::React { .. }
344 | Operation::Set { .. } => true,
345 Operation::Show { .. }
346 | Operation::Diff { .. }
347 | Operation::Checkout { .. }
348 | Operation::List { .. }
349 | Operation::Cache { .. } => false,
350 }
351 }
352}
353
354#[derive(Debug)]
355pub struct Options {
356 pub op: Operation,
357 pub repo: Option<RepoId>,
358 pub announce: bool,
359 pub verbose: bool,
360 pub quiet: bool,
361 pub authored: bool,
362 pub authors: Vec<Did>,
363}
364
365impl Args for Options {
366 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
367 use lexopt::prelude::*;
368
369 let mut parser = lexopt::Parser::from_args(args);
370 let mut op: Option<OperationName> = None;
371 let mut verbose = false;
372 let mut quiet = false;
373 let mut authored = false;
374 let mut authors = vec![];
375 let mut announce = true;
376 let mut patch_id = None;
377 let mut revision_id = None;
378 let mut review_id = None;
379 let mut comment_id = None;
380 let mut message = Message::default();
381 let mut filter = Some(patch::Status::Open);
382 let mut diff = false;
383 let mut debug = false;
384 let mut undo = false;
385 let mut reaction: Option<Reaction> = None;
386 let mut reply_to: Option<Rev> = None;
387 let mut comment_op: Option<(CommentOperation, Rev)> = None;
388 let mut checkout_opts = checkout::Options::default();
389 let mut remote: Option<RefString> = None;
390 let mut assign_opts = AssignOptions::default();
391 let mut label_opts = LabelOptions::default();
392 let mut review_op = review::Operation::default();
393 let mut base_id = None;
394 let mut repo = None;
395 let mut cache_storage = false;
396
397 while let Some(arg) = parser.next()? {
398 match arg {
399 Long("message") | Short('m') => {
401 if message != Message::Blank {
402 let txt: String = term::args::string(&parser.value()?);
404 message.append(&txt);
405 }
406 }
407 Long("no-message") => {
408 message = Message::Blank;
409 }
410 Long("announce") => {
411 announce = true;
412 }
413 Long("no-announce") => {
414 announce = false;
415 }
416
417 Long("patch") | Short('p') if op == Some(OperationName::Show) => {
419 diff = true;
420 }
421 Long("debug") if op == Some(OperationName::Show) => {
422 debug = true;
423 }
424
425 Long("undo") if op == Some(OperationName::Ready) => {
427 undo = true;
428 }
429
430 Long("undo") if op == Some(OperationName::Archive) => {
432 undo = true;
433 }
434
435 Short('b') | Long("base") if op == Some(OperationName::Update) => {
437 let val = parser.value()?;
438 let rev = term::args::rev(&val)?;
439
440 base_id = Some(rev);
441 }
442
443 Long("emoji") if op == Some(OperationName::React) => {
445 if let Some(emoji) = parser.value()?.to_str() {
446 reaction =
447 Some(Reaction::from_str(emoji).map_err(|_| anyhow!("invalid emoji"))?);
448 }
449 }
450 Long("undo") if op == Some(OperationName::React) => {
451 undo = true;
452 }
453
454 Long("reply-to") if op == Some(OperationName::Comment) => {
456 let val = parser.value()?;
457 let rev = term::args::rev(&val)?;
458
459 reply_to = Some(rev);
460 }
461
462 Long("edit") if op == Some(OperationName::Comment) => {
463 let val = parser.value()?;
464 let rev = term::args::rev(&val)?;
465
466 comment_op = Some((CommentOperation::Edit, rev));
467 }
468
469 Long("react") if op == Some(OperationName::Comment) => {
470 let val = parser.value()?;
471 let rev = term::args::rev(&val)?;
472
473 comment_op = Some((CommentOperation::React, rev));
474 }
475 Long("emoji")
476 if op == Some(OperationName::Comment)
477 && matches!(comment_op, Some((CommentOperation::React, _))) =>
478 {
479 if let Some(emoji) = parser.value()?.to_str() {
480 reaction =
481 Some(Reaction::from_str(emoji).map_err(|_| anyhow!("invalid emoji"))?);
482 }
483 }
484 Long("undo")
485 if op == Some(OperationName::Comment)
486 && matches!(comment_op, Some((CommentOperation::React, _))) =>
487 {
488 undo = true;
489 }
490
491 Long("redact") if op == Some(OperationName::Comment) => {
492 let val = parser.value()?;
493 let rev = term::args::rev(&val)?;
494
495 comment_op = Some((CommentOperation::Redact, rev));
496 }
497
498 Long("revision") | Short('r') if op == Some(OperationName::Edit) => {
500 let val = parser.value()?;
501 let rev = term::args::rev(&val)?;
502
503 revision_id = Some(rev);
504 }
505
506 Long("revision") | Short('r')
508 if op == Some(OperationName::Review) || op == Some(OperationName::Diff) =>
509 {
510 let val = parser.value()?;
511 let rev = term::args::rev(&val)?;
512
513 revision_id = Some(rev);
514 }
515 Long("patch") | Short('p') if op == Some(OperationName::Review) => {
516 if let review::Operation::Review { by_hunk, .. } = &mut review_op {
517 *by_hunk = true;
518 } else {
519 return Err(arg.unexpected().into());
520 }
521 }
522 Long("unified") | Short('U') if op == Some(OperationName::Review) => {
523 if let review::Operation::Review { unified, .. } = &mut review_op {
524 let val = parser.value()?;
525 *unified = term::args::number(&val)?;
526 } else {
527 return Err(arg.unexpected().into());
528 }
529 }
530 Long("hunk") if op == Some(OperationName::Review) => {
531 if let review::Operation::Review { hunk, .. } = &mut review_op {
532 let val = parser.value()?;
533 let val = term::args::number(&val)
534 .map_err(|e| anyhow!("invalid hunk value: {e}"))?;
535
536 *hunk = Some(val);
537 } else {
538 return Err(arg.unexpected().into());
539 }
540 }
541 Long("delete") | Short('d') if op == Some(OperationName::Review) => {
542 review_op = review::Operation::Delete;
543 }
544 Long("accept") if op == Some(OperationName::Review) => {
545 if let review::Operation::Review {
546 verdict: verdict @ None,
547 ..
548 } = &mut review_op
549 {
550 *verdict = Some(patch::Verdict::Accept);
551 } else {
552 return Err(arg.unexpected().into());
553 }
554 }
555 Long("reject") if op == Some(OperationName::Review) => {
556 if let review::Operation::Review {
557 verdict: verdict @ None,
558 ..
559 } = &mut review_op
560 {
561 *verdict = Some(patch::Verdict::Reject);
562 } else {
563 return Err(arg.unexpected().into());
564 }
565 }
566
567 Long("undo") if op == Some(OperationName::Resolve) => {
569 undo = true;
570 }
571 Long("review") if op == Some(OperationName::Resolve) => {
572 let val = parser.value()?;
573 let rev = term::args::rev(&val)?;
574
575 review_id = Some(rev);
576 }
577 Long("comment") if op == Some(OperationName::Resolve) => {
578 let val = parser.value()?;
579 let rev = term::args::rev(&val)?;
580
581 comment_id = Some(rev);
582 }
583
584 Long("revision") if op == Some(OperationName::Checkout) => {
586 let val = parser.value()?;
587 let rev = term::args::rev(&val)?;
588
589 revision_id = Some(rev);
590 }
591
592 Long("force") | Short('f') if op == Some(OperationName::Checkout) => {
593 checkout_opts.force = true;
594 }
595
596 Long("name") if op == Some(OperationName::Checkout) => {
597 let val = parser.value()?;
598 checkout_opts.name = Some(term::args::refstring("name", val)?);
599 }
600
601 Long("remote") if op == Some(OperationName::Checkout) => {
602 let val = parser.value()?;
603 checkout_opts.remote = Some(term::args::refstring("remote", val)?);
604 }
605
606 Short('a') | Long("add") if matches!(op, Some(OperationName::Assign)) => {
608 assign_opts.add.insert(term::args::did(&parser.value()?)?);
609 }
610
611 Short('d') | Long("delete") if matches!(op, Some(OperationName::Assign)) => {
612 assign_opts
613 .delete
614 .insert(term::args::did(&parser.value()?)?);
615 }
616
617 Short('a') | Long("add") if matches!(op, Some(OperationName::Label)) => {
619 let val = parser.value()?;
620 let name = term::args::string(&val);
621 let label = Label::new(name)?;
622
623 label_opts.add.insert(label);
624 }
625
626 Short('d') | Long("delete") if matches!(op, Some(OperationName::Label)) => {
627 let val = parser.value()?;
628 let name = term::args::string(&val);
629 let label = Label::new(name)?;
630
631 label_opts.delete.insert(label);
632 }
633
634 Long("remote") if op == Some(OperationName::Set) => {
636 let val = parser.value()?;
637 remote = Some(term::args::refstring("remote", val)?);
638 }
639
640 Long("all") => {
642 filter = None;
643 }
644 Long("draft") => {
645 filter = Some(patch::Status::Draft);
646 }
647 Long("archived") => {
648 filter = Some(patch::Status::Archived);
649 }
650 Long("merged") => {
651 filter = Some(patch::Status::Merged);
652 }
653 Long("open") => {
654 filter = Some(patch::Status::Open);
655 }
656 Long("authored") => {
657 authored = true;
658 }
659 Long("author") if op == Some(OperationName::List) => {
660 authors.push(term::args::did(&parser.value()?)?);
661 }
662
663 Long("storage") if op == Some(OperationName::Cache) => {
665 cache_storage = true;
666 }
667
668 Long("verbose") | Short('v') => {
670 verbose = true;
671 }
672 Long("quiet") | Short('q') => {
673 quiet = true;
674 }
675 Long("repo") => {
676 let val = parser.value()?;
677 let rid = term::args::rid(&val)?;
678
679 repo = Some(rid);
680 }
681 Long("help") => {
682 return Err(Error::HelpManual { name: "rad-patch" }.into());
683 }
684 Short('h') => {
685 return Err(Error::Help.into());
686 }
687
688 Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
689 "l" | "list" => op = Some(OperationName::List),
690 "s" | "show" => op = Some(OperationName::Show),
691 "u" | "update" => op = Some(OperationName::Update),
692 "d" | "delete" => op = Some(OperationName::Delete),
693 "c" | "checkout" => op = Some(OperationName::Checkout),
694 "a" | "archive" => op = Some(OperationName::Archive),
695 "y" | "ready" => op = Some(OperationName::Ready),
696 "e" | "edit" => op = Some(OperationName::Edit),
697 "r" | "redact" => op = Some(OperationName::Redact),
698 "diff" => op = Some(OperationName::Diff),
699 "assign" => op = Some(OperationName::Assign),
700 "label" => op = Some(OperationName::Label),
701 "comment" => op = Some(OperationName::Comment),
702 "review" => op = Some(OperationName::Review),
703 "resolve" => op = Some(OperationName::Resolve),
704 "set" => op = Some(OperationName::Set),
705 "cache" => op = Some(OperationName::Cache),
706 unknown => anyhow::bail!("unknown operation '{}'", unknown),
707 },
708 Value(val) if op == Some(OperationName::Redact) => {
709 let rev = term::args::rev(&val)?;
710 revision_id = Some(rev);
711 }
712 Value(val)
713 if patch_id.is_none()
714 && [
715 Some(OperationName::Show),
716 Some(OperationName::Diff),
717 Some(OperationName::Update),
718 Some(OperationName::Delete),
719 Some(OperationName::Archive),
720 Some(OperationName::Ready),
721 Some(OperationName::Checkout),
722 Some(OperationName::Comment),
723 Some(OperationName::Review),
724 Some(OperationName::Resolve),
725 Some(OperationName::Edit),
726 Some(OperationName::Set),
727 Some(OperationName::Assign),
728 Some(OperationName::Label),
729 Some(OperationName::Cache),
730 ]
731 .contains(&op) =>
732 {
733 let val = string(&val);
734 patch_id = Some(Rev::from(val));
735 }
736 _ => return Err(anyhow::anyhow!(arg.unexpected())),
737 }
738 }
739
740 let op = match op.unwrap_or_default() {
741 OperationName::List => Operation::List { filter },
742 OperationName::Show => Operation::Show {
743 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
744 diff,
745 debug,
746 },
747 OperationName::Diff => Operation::Diff {
748 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
749 revision_id,
750 },
751 OperationName::Delete => Operation::Delete {
752 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
753 },
754 OperationName::Update => Operation::Update {
755 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
756 base_id,
757 message,
758 },
759 OperationName::Archive => Operation::Archive {
760 patch_id: patch_id.ok_or_else(|| anyhow!("a patch id must be provided"))?,
761 undo,
762 },
763 OperationName::Checkout => Operation::Checkout {
764 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
765 revision_id,
766 opts: checkout_opts,
767 },
768 OperationName::Comment => match comment_op {
769 Some((CommentOperation::Edit, comment)) => Operation::CommentEdit {
770 revision_id: patch_id
771 .ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
772 comment_id: comment,
773 message,
774 },
775 Some((CommentOperation::React, comment)) => Operation::CommentReact {
776 revision_id: patch_id
777 .ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
778 comment_id: comment,
779 reaction: reaction
780 .ok_or_else(|| anyhow!("a reaction emoji must be provided"))?,
781 undo,
782 },
783 Some((CommentOperation::Redact, comment)) => Operation::CommentRedact {
784 revision_id: patch_id
785 .ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
786 comment_id: comment,
787 },
788 None => Operation::Comment {
789 revision_id: patch_id
790 .ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
791 message,
792 reply_to,
793 },
794 },
795 OperationName::React => Operation::React {
796 revision_id: patch_id
797 .ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
798 reaction: reaction.ok_or_else(|| anyhow!("a reaction emoji must be provided"))?,
799 undo,
800 },
801 OperationName::Review => Operation::Review {
802 patch_id: patch_id
803 .ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
804 revision_id,
805 opts: review::Options {
806 message,
807 op: review_op,
808 },
809 },
810 OperationName::Resolve => Operation::Resolve {
811 patch_id: patch_id
812 .ok_or_else(|| anyhow!("a patch or revision must be provided"))?,
813 review_id: review_id.ok_or_else(|| anyhow!("a review must be provided"))?,
814 comment_id: comment_id.ok_or_else(|| anyhow!("a comment must be provided"))?,
815 undo,
816 },
817 OperationName::Ready => Operation::Ready {
818 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
819 undo,
820 },
821 OperationName::Edit => Operation::Edit {
822 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
823 revision_id,
824 message,
825 },
826 OperationName::Redact => Operation::Redact {
827 revision_id: revision_id.ok_or_else(|| anyhow!("a revision must be provided"))?,
828 },
829 OperationName::Assign => Operation::Assign {
830 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
831 opts: assign_opts,
832 },
833 OperationName::Label => Operation::Label {
834 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
835 opts: label_opts,
836 },
837 OperationName::Set => Operation::Set {
838 patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
839 remote,
840 },
841 OperationName::Cache => Operation::Cache {
842 patch_id,
843 storage: cache_storage,
844 },
845 };
846
847 Ok((
848 Options {
849 op,
850 repo,
851 verbose,
852 quiet,
853 announce,
854 authored,
855 authors,
856 },
857 vec![],
858 ))
859 }
860}
861
862pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
863 let (workdir, rid) = if let Some(rid) = options.repo {
864 (None, rid)
865 } else {
866 radicle::rad::cwd()
867 .map(|(workdir, rid)| (Some(workdir), rid))
868 .map_err(|_| anyhow!("this command must be run in the context of a repository"))?
869 };
870
871 let profile = ctx.profile()?;
872 let repository = profile.storage.repository(rid)?;
873 let announce = options.announce && options.op.is_announce();
874
875 transport::local::register(profile.storage.clone());
876
877 match options.op {
878 Operation::List { filter } => {
879 let mut authors: BTreeSet<Did> = options.authors.iter().cloned().collect();
880 if options.authored {
881 authors.insert(profile.did());
882 }
883 list::run(filter.as_ref(), authors, &repository, &profile)?;
884 }
885 Operation::Show {
886 patch_id,
887 diff,
888 debug,
889 } => {
890 let patch_id = patch_id.resolve(&repository.backend)?;
891 show::run(
892 &patch_id,
893 diff,
894 debug,
895 options.verbose,
896 &profile,
897 &repository,
898 workdir.as_ref(),
899 )?;
900 }
901 Operation::Diff {
902 patch_id,
903 revision_id,
904 } => {
905 let patch_id = patch_id.resolve(&repository.backend)?;
906 let revision_id = revision_id
907 .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
908 .transpose()?
909 .map(patch::RevisionId::from);
910 diff::run(&patch_id, revision_id, &repository, &profile)?;
911 }
912 Operation::Update {
913 ref patch_id,
914 ref base_id,
915 ref message,
916 } => {
917 let patch_id = patch_id.resolve(&repository.backend)?;
918 let base_id = base_id
919 .as_ref()
920 .map(|base| base.resolve(&repository.backend))
921 .transpose()?;
922 let workdir = workdir.ok_or(anyhow!(
923 "this command must be run from a repository checkout"
924 ))?;
925
926 update::run(
927 patch_id,
928 base_id,
929 message.clone(),
930 &profile,
931 &repository,
932 &workdir,
933 )?;
934 }
935 Operation::Archive { ref patch_id, undo } => {
936 let patch_id = patch_id.resolve::<PatchId>(&repository.backend)?;
937 archive::run(&patch_id, undo, &profile, &repository)?;
938 }
939 Operation::Ready { ref patch_id, undo } => {
940 let patch_id = patch_id.resolve::<PatchId>(&repository.backend)?;
941
942 if !ready::run(&patch_id, undo, &profile, &repository)? {
943 if undo {
944 anyhow::bail!("the patch must be open to be put in draft state");
945 } else {
946 anyhow::bail!("this patch must be in draft state to be put in open state");
947 }
948 }
949 }
950 Operation::Delete { patch_id } => {
951 let patch_id = patch_id.resolve::<PatchId>(&repository.backend)?;
952 delete::run(&patch_id, &profile, &repository)?;
953 }
954 Operation::Checkout {
955 patch_id,
956 revision_id,
957 opts,
958 } => {
959 let patch_id = patch_id.resolve::<radicle::git::Oid>(&repository.backend)?;
960 let revision_id = revision_id
961 .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
962 .transpose()?
963 .map(patch::RevisionId::from);
964 let workdir = workdir.ok_or(anyhow!(
965 "this command must be run from a repository checkout"
966 ))?;
967 checkout::run(
968 &patch::PatchId::from(patch_id),
969 revision_id,
970 &repository,
971 &workdir,
972 &profile,
973 opts,
974 )?;
975 }
976 Operation::Comment {
977 revision_id,
978 message,
979 reply_to,
980 } => {
981 comment::run(
982 revision_id,
983 message,
984 reply_to,
985 options.quiet,
986 &repository,
987 &profile,
988 )?;
989 }
990 Operation::Review {
991 patch_id,
992 revision_id,
993 opts,
994 } => {
995 let patch_id = patch_id.resolve(&repository.backend)?;
996 let revision_id = revision_id
997 .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
998 .transpose()?
999 .map(patch::RevisionId::from);
1000 review::run(patch_id, revision_id, opts, &profile, &repository)?;
1001 }
1002 Operation::Resolve {
1003 ref patch_id,
1004 ref review_id,
1005 ref comment_id,
1006 undo,
1007 } => {
1008 let patch = patch_id.resolve(&repository.backend)?;
1009 let review = patch::ReviewId::from(
1010 review_id.resolve::<radicle::cob::EntryId>(&repository.backend)?,
1011 );
1012 let comment = comment_id.resolve(&repository.backend)?;
1013 if undo {
1014 resolve::unresolve(patch, review, comment, &repository, &profile)?;
1015 term::success!("Unresolved comment {comment_id}");
1016 } else {
1017 resolve::resolve(patch, review, comment, &repository, &profile)?;
1018 term::success!("Resolved comment {comment_id}");
1019 }
1020 }
1021 Operation::Edit {
1022 patch_id,
1023 revision_id,
1024 message,
1025 } => {
1026 let patch_id = patch_id.resolve(&repository.backend)?;
1027 let revision_id = revision_id
1028 .map(|id| id.resolve::<radicle::git::Oid>(&repository.backend))
1029 .transpose()?
1030 .map(patch::RevisionId::from);
1031 edit::run(&patch_id, revision_id, message, &profile, &repository)?;
1032 }
1033 Operation::Redact { revision_id } => {
1034 redact::run(&revision_id, &profile, &repository)?;
1035 }
1036 Operation::Assign {
1037 patch_id,
1038 opts: AssignOptions { add, delete },
1039 } => {
1040 let patch_id = patch_id.resolve(&repository.backend)?;
1041 assign::run(&patch_id, add, delete, &profile, &repository)?;
1042 }
1043 Operation::Label {
1044 patch_id,
1045 opts: LabelOptions { add, delete },
1046 } => {
1047 let patch_id = patch_id.resolve(&repository.backend)?;
1048 label::run(&patch_id, add, delete, &profile, &repository)?;
1049 }
1050 Operation::Set { patch_id, remote } => {
1051 let patches = term::cob::patches(&profile, &repository)?;
1052 let patch_id = patch_id.resolve(&repository.backend)?;
1053 let patch = patches
1054 .get(&patch_id)?
1055 .ok_or_else(|| anyhow!("patch {patch_id} not found"))?;
1056 let workdir = workdir.ok_or(anyhow!(
1057 "this command must be run from a repository checkout"
1058 ))?;
1059 radicle::rad::setup_patch_upstream(
1060 &patch_id,
1061 *patch.head(),
1062 &workdir,
1063 remote.as_ref().unwrap_or(&radicle::rad::REMOTE_NAME),
1064 true,
1065 )?;
1066 }
1067 Operation::Cache { patch_id, storage } => {
1068 let mode = if storage {
1069 cache::CacheMode::Storage
1070 } else {
1071 let patch_id = patch_id
1072 .map(|id| id.resolve(&repository.backend))
1073 .transpose()?;
1074 patch_id.map_or(
1075 cache::CacheMode::Repository {
1076 repository: &repository,
1077 },
1078 |id| cache::CacheMode::Patch {
1079 id,
1080 repository: &repository,
1081 },
1082 )
1083 };
1084 cache::run(mode, &profile)?;
1085 }
1086 Operation::CommentEdit {
1087 revision_id,
1088 comment_id,
1089 message,
1090 } => {
1091 let comment = comment_id.resolve(&repository.backend)?;
1092 comment::edit::run(
1093 revision_id,
1094 comment,
1095 message,
1096 options.quiet,
1097 &repository,
1098 &profile,
1099 )?;
1100 }
1101 Operation::CommentRedact {
1102 revision_id,
1103 comment_id,
1104 } => {
1105 let comment = comment_id.resolve(&repository.backend)?;
1106 comment::redact::run(revision_id, comment, &repository, &profile)?;
1107 }
1108 Operation::CommentReact {
1109 revision_id,
1110 comment_id,
1111 reaction,
1112 undo,
1113 } => {
1114 let comment = comment_id.resolve(&repository.backend)?;
1115 if undo {
1116 comment::react::run(revision_id, comment, reaction, false, &repository, &profile)?;
1117 } else {
1118 comment::react::run(revision_id, comment, reaction, true, &repository, &profile)?;
1119 }
1120 }
1121 Operation::React {
1122 revision_id,
1123 reaction,
1124 undo,
1125 } => {
1126 if undo {
1127 react::run(&revision_id, reaction, false, &repository, &profile)?;
1128 } else {
1129 react::run(&revision_id, reaction, true, &repository, &profile)?;
1130 }
1131 }
1132 }
1133
1134 if announce {
1135 let mut node = Node::new(profile.socket());
1136 node::announce(
1137 &repository,
1138 node::SyncSettings::default(),
1139 node::SyncReporting::default(),
1140 &mut node,
1141 &profile,
1142 )?;
1143 }
1144 Ok(())
1145}