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