Skip to main content

radicle_cli/commands/
patch.rs

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    // Fallback to [`Command::List`] if no subcommand is provided.
46    // Construct it using the [`EmptyArgs`] in `args.empty`.
47    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}