1mod archive;
2mod args;
3mod assign;
4mod cache;
5mod checkout;
6mod comment;
7mod delete;
8mod diff;
9mod edit;
10mod label;
11mod list;
12mod react;
13mod ready;
14mod redact;
15mod resolve;
16mod review;
17mod show;
18mod update;
19
20use std::collections::BTreeSet;
21
22use anyhow::anyhow;
23
24use radicle::cob::patch::PatchId;
25use radicle::cob::{Label, patch};
26use radicle::patch::cache::Patches as _;
27use radicle::storage::git::transport;
28use radicle::{Node, prelude::*};
29
30use crate::git::Rev;
31use crate::node;
32use crate::terminal as term;
33use crate::terminal::args::rid_or_cwd;
34use crate::terminal::patch::Message;
35
36pub use args::Args;
37
38use args::{AssignArgs, Command, CommentAction, LabelArgs};
39
40pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
41 let (workdir, rid) = rid_or_cwd(args.repo)?;
42 let profile = ctx.profile()?;
43 let repository = profile.storage.repository(rid)?;
44
45 let mut announce = args.should_announce();
48 let command = args
49 .command
50 .unwrap_or_else(|| Command::List(args.empty.into()));
51 announce &= command.should_announce();
52
53 transport::local::register(profile.storage.clone());
54
55 match command {
56 Command::List(args) => {
57 let mut authors: BTreeSet<Did> = args.authors.iter().cloned().collect();
58 if args.authored {
59 authors.insert(profile.did());
60 }
61 list::run((&args.state).into(), authors, &repository, &profile)?;
62 }
63
64 Command::Show { id, patch, verbose } => {
65 let patch_id = id.resolve(&repository.backend)?;
66 show::run(
67 &patch_id,
68 patch,
69 verbose,
70 &profile,
71 &repository,
72 workdir.as_ref(),
73 )?;
74 }
75
76 Command::Diff { id, revision } => {
77 let patch_id = id.resolve(&repository.backend)?;
78 let revision_id = revision
79 .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
80 .transpose()?
81 .map(patch::RevisionId::from);
82 diff::run(&patch_id, revision_id, &repository, &profile)?;
83 }
84
85 Command::Update { id, base, message } => {
86 let message = Message::from(message);
87 let patch_id = id.resolve(&repository.backend)?;
88 let base_id = base
89 .as_ref()
90 .map(|base| base.resolve(&repository.backend))
91 .transpose()?;
92 let workdir = workdir.ok_or(anyhow!(
93 "this command must be run from a repository checkout"
94 ))?;
95
96 update::run(patch_id, base_id, message, &profile, &repository, &workdir)?;
97 }
98
99 Command::Archive { id, undo } => {
100 let patch_id = id.resolve::<PatchId>(&repository.backend)?;
101 archive::run(&patch_id, undo, &profile, &repository)?;
102 }
103
104 Command::Ready { id, undo } => {
105 let patch_id = id.resolve::<PatchId>(&repository.backend)?;
106
107 if !ready::run(&patch_id, undo, &profile, &repository)? {
108 if undo {
109 anyhow::bail!("the patch must be open to be put in draft state");
110 } else {
111 anyhow::bail!("this patch must be in draft state to be put in open state");
112 }
113 }
114 }
115
116 Command::Delete { id } => {
117 let patch_id = id.resolve::<PatchId>(&repository.backend)?;
118 delete::run(&patch_id, &profile, &repository)?;
119 }
120
121 Command::Checkout { id, revision, opts } => {
122 let patch_id = id.resolve::<radicle::git::Oid>(&repository.backend)?;
123 let revision_id = revision
124 .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
125 .transpose()?
126 .map(patch::RevisionId::from);
127 let workdir = workdir.ok_or(anyhow!(
128 "this command must be run from a repository checkout"
129 ))?;
130 checkout::run(
131 &patch::PatchId::from(patch_id),
132 revision_id,
133 &repository,
134 &workdir,
135 &profile,
136 opts.into(),
137 )?;
138 }
139
140 Command::Comment(c) => match CommentAction::from(c) {
141 CommentAction::Comment {
142 revision,
143 message,
144 reply_to,
145 } => {
146 comment::run(
147 revision,
148 message,
149 reply_to,
150 args.quiet,
151 &repository,
152 &profile,
153 )?;
154 }
155 CommentAction::Edit {
156 revision,
157 comment,
158 message,
159 } => {
160 let comment = comment.resolve(&repository.backend)?;
161 comment::edit::run(
162 revision,
163 comment,
164 message,
165 args.quiet,
166 &repository,
167 &profile,
168 )?;
169 }
170 CommentAction::Redact { revision, comment } => {
171 let comment = comment.resolve(&repository.backend)?;
172 comment::redact::run(revision, comment, &repository, &profile)?;
173 }
174 CommentAction::React {
175 revision,
176 comment,
177 emoji,
178 undo,
179 } => {
180 let comment = comment.resolve(&repository.backend)?;
181 if undo {
182 comment::react::run(revision, comment, emoji, false, &repository, &profile)?;
183 } else {
184 comment::react::run(revision, comment, emoji, true, &repository, &profile)?;
185 }
186 }
187 },
188
189 Command::Review {
190 id,
191 revision,
192 options,
193 } => {
194 let patch_id = id.resolve(&repository.backend)?;
195 let revision_id = revision
196 .map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
197 .transpose()?
198 .map(patch::RevisionId::from);
199 review::run(patch_id, revision_id, options.into(), &profile, &repository)?;
200 }
201
202 Command::Resolve {
203 id,
204 review,
205 comment,
206 unresolve,
207 } => {
208 let patch = id.resolve(&repository.backend)?;
209 let review = patch::ReviewId::from(
210 review.resolve::<radicle::cob::EntryId>(&repository.backend)?,
211 );
212 let comment = comment.resolve(&repository.backend)?;
213 if unresolve {
214 resolve::unresolve(patch, review, comment, &repository, &profile)?;
215 term::success!("Unresolved comment {comment}");
216 } else {
217 resolve::resolve(patch, review, comment, &repository, &profile)?;
218 term::success!("Resolved comment {comment}");
219 }
220 }
221 Command::Edit {
222 id,
223 revision,
224 message,
225 } => {
226 let message = Message::from(message);
227 let patch_id = id.resolve(&repository.backend)?;
228 let revision_id = revision
229 .map(|id| id.resolve::<radicle::git::Oid>(&repository.backend))
230 .transpose()?
231 .map(patch::RevisionId::from);
232 edit::run(&patch_id, revision_id, message, &profile, &repository)?;
233 }
234 Command::Redact { id } => {
235 redact::run(&id, &profile, &repository)?;
236 }
237 Command::Assign {
238 id,
239 args: AssignArgs { add, delete },
240 } => {
241 let patch_id = id.resolve(&repository.backend)?;
242 assign::run(
243 &patch_id,
244 add.into_iter().collect(),
245 delete.into_iter().collect(),
246 &profile,
247 &repository,
248 )?;
249 }
250 Command::Label {
251 id,
252 args: LabelArgs { add, delete },
253 } => {
254 let patch_id = id.resolve(&repository.backend)?;
255 label::run(
256 &patch_id,
257 add.into_iter().collect(),
258 delete.into_iter().collect(),
259 &profile,
260 &repository,
261 )?;
262 }
263 Command::Set { id, remote } => {
264 let patches = term::cob::patches(&profile, &repository)?;
265 let patch_id = id.resolve(&repository.backend)?;
266 let patch = patches
267 .get(&patch_id)?
268 .ok_or_else(|| anyhow!("patch {patch_id} not found"))?;
269 let workdir = workdir.ok_or(anyhow!(
270 "this command must be run from a repository checkout"
271 ))?;
272 radicle::rad::setup_patch_upstream(
273 &patch_id,
274 *patch.head(),
275 &workdir,
276 remote.as_ref().unwrap_or(&radicle::rad::REMOTE_NAME),
277 true,
278 )?;
279 }
280 Command::Cache { id, storage } => {
281 let mode = if storage {
282 cache::CacheMode::Storage
283 } else {
284 let patch_id = id.map(|id| id.resolve(&repository.backend)).transpose()?;
285 patch_id.map_or(
286 cache::CacheMode::Repository {
287 repository: &repository,
288 },
289 |id| cache::CacheMode::Patch {
290 id,
291 repository: &repository,
292 },
293 )
294 };
295 cache::run(mode, &profile)?;
296 }
297 Command::React {
298 id,
299 emoji: react,
300 undo,
301 } => {
302 if undo {
303 react::run(&id, react, false, &repository, &profile)?;
304 } else {
305 react::run(&id, react, true, &repository, &profile)?;
306 }
307 }
308 }
309
310 if announce {
311 let mut node = Node::new(profile.socket_from_env());
312 node::announce(
313 &repository,
314 node::SyncSettings::default(),
315 node::SyncReporting::default(),
316 &mut node,
317 &profile,
318 )?;
319 }
320 Ok(())
321}