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