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