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