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