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