1use std::str::FromStr;
2
3use clap::{Parser, Subcommand};
4
5use radicle::{
6 cob::{Label, Reaction, Title},
7 identity::{did::DidError, Did, RepoId},
8 issue::{CloseReason, State},
9};
10
11use crate::{git::Rev, terminal::patch::Message};
12
13#[derive(Default, Debug, Clone, PartialEq, Eq)]
14pub enum Assigned {
15 #[default]
16 Me,
17 Peer(Did),
18}
19
20#[derive(Parser, Debug)]
21#[command(about = super::ABOUT, disable_version_flag = true)]
22pub struct Args {
23 #[command(subcommand)]
24 pub(crate) command: Option<Command>,
25
26 #[arg(short, long)]
28 #[clap(global = true)]
29 pub(crate) quiet: bool,
30
31 #[arg(long)]
33 #[arg(value_name = "no-announce")]
34 #[clap(global = true)]
35 pub(crate) no_announce: bool,
36
37 #[arg(long)]
39 #[clap(global = true)]
40 pub(crate) header: bool,
41
42 #[arg(value_name = "RID")]
44 #[arg(long, short)]
45 #[clap(global = true)]
46 pub(crate) repo: Option<RepoId>,
47
48 #[arg(long, short)]
50 #[clap(global = true)]
51 pub(crate) verbose: bool,
52
53 #[clap(flatten)]
56 pub(crate) empty: EmptyArgs,
57}
58
59#[derive(Subcommand, Debug)]
60pub(crate) enum Command {
61 Assign {
63 #[arg(value_name = "ISSUE_ID")]
65 id: Rev,
66
67 #[arg(long, short)]
69 #[arg(value_name = "DID")]
70 #[arg(action = clap::ArgAction::Append)]
71 add: Vec<Did>,
72
73 #[arg(long, short)]
75 #[arg(value_name = "DID")]
76 #[arg(action = clap::ArgAction::Append)]
77 delete: Vec<Did>,
78 },
79 Cache {
81 #[arg(value_name = "ISSUE_ID")]
83 id: Option<Rev>,
84
85 #[arg(long)]
87 storage: bool,
88 },
89 #[clap(long_about = include_str!("comment.txt"))]
91 Comment(CommentArgs),
92 Edit {
94 #[arg(value_name = "ISSUE_ID")]
96 id: Rev,
97
98 #[arg(long, short)]
100 title: Option<Title>,
101
102 #[arg(long, short)]
104 description: Option<String>,
105 },
106 Delete {
108 #[arg(value_name = "ISSUE_ID")]
110 id: Rev,
111 },
112 Label {
114 #[arg(value_name = "ISSUE_ID")]
116 id: Rev,
117
118 #[arg(long, short)]
120 #[arg(value_name = "label")]
121 #[arg(action = clap::ArgAction::Append)]
122 add: Vec<Label>,
123
124 #[arg(long, short)]
126 #[arg(value_name = "label")]
127 #[arg(action = clap::ArgAction::Append)]
128 delete: Vec<Label>,
129 },
130 List(ListArgs),
132 Open {
134 #[arg(long, short)]
136 title: Option<Title>,
137
138 #[arg(long, short)]
140 description: Option<String>,
141
142 #[arg(long)]
144 labels: Vec<Label>,
145
146 #[arg(value_name = "DID")]
148 #[arg(long)]
149 assignees: Vec<Did>,
150 },
151 React {
153 #[arg(value_name = "ISSUE_ID")]
155 id: Rev,
156
157 #[arg(long = "emoji")]
159 #[arg(value_name = "CHAR")]
160 reaction: Option<Reaction>,
161
162 #[arg(long = "to")]
164 #[arg(value_name = "COMMENT_ID")]
165 comment_id: Option<Rev>,
166 },
167 Show {
169 #[arg(value_name = "ISSUE_ID")]
171 id: Rev,
172 },
173 State {
175 #[arg(value_name = "ISSUE_ID")]
177 id: Rev,
178
179 #[clap(flatten)]
181 target_state: StateArgs,
182 },
183}
184
185impl Command {
186 pub(crate) fn should_announce_for(&self) -> bool {
189 match self {
190 Command::Open { .. }
191 | Command::React { .. }
192 | Command::State { .. }
193 | Command::Delete { .. }
194 | Command::Assign { .. }
195 | Command::Label { .. }
196 | Command::Edit { .. } => true,
198 Command::Comment(args) => !args.is_edit(),
199 _ => false,
200 }
201 }
202}
203
204#[derive(Parser, Debug, Default)]
206pub(crate) struct EmptyArgs {
207 #[arg(long, name = "DID")]
208 #[arg(default_missing_value = "me")]
209 #[arg(num_args = 0..=1)]
210 #[arg(hide = true)]
211 pub(crate) assigned: Option<Assigned>,
212
213 #[clap(flatten)]
214 pub(crate) state: EmptyStateArgs,
215}
216
217#[derive(Parser, Debug, Default)]
219#[group(multiple = false)]
220pub(crate) struct EmptyStateArgs {
221 #[arg(long, hide = true)]
222 all: bool,
223
224 #[arg(long, hide = true)]
225 open: bool,
226
227 #[arg(long, hide = true)]
228 closed: bool,
229
230 #[arg(long, hide = true)]
231 solved: bool,
232}
233
234#[derive(Parser, Debug, Default)]
236pub(crate) struct ListArgs {
237 #[arg(long, name = "DID")]
239 #[arg(default_missing_value = "me")]
240 #[arg(num_args = 0..=1)]
241 pub(crate) assigned: Option<Assigned>,
242
243 #[clap(flatten)]
244 pub(crate) state: ListStateArgs,
245}
246
247#[derive(Parser, Debug, Default)]
248#[group(multiple = false)]
249pub(crate) struct ListStateArgs {
250 #[arg(long)]
252 all: bool,
253
254 #[arg(long)]
256 open: bool,
257
258 #[arg(long)]
260 closed: bool,
261
262 #[arg(long)]
264 solved: bool,
265}
266
267impl From<&ListStateArgs> for Option<State> {
268 fn from(args: &ListStateArgs) -> Self {
269 match (args.all, args.open, args.closed, args.solved) {
270 (true, false, false, false) => None,
271 (false, true, false, false) | (false, false, false, false) => Some(State::Open),
272 (false, false, true, false) => Some(State::Closed {
273 reason: CloseReason::Other,
274 }),
275 (false, false, false, true) => Some(State::Closed {
276 reason: CloseReason::Solved,
277 }),
278 _ => unreachable!(),
279 }
280 }
281}
282
283impl From<EmptyStateArgs> for ListStateArgs {
284 fn from(args: EmptyStateArgs) -> Self {
285 Self {
286 all: args.all,
287 open: args.open,
288 closed: args.closed,
289 solved: args.solved,
290 }
291 }
292}
293
294impl From<EmptyArgs> for ListArgs {
295 fn from(args: EmptyArgs) -> Self {
296 Self {
297 assigned: args.assigned,
298 state: ListStateArgs::from(args.state),
299 }
300 }
301}
302
303#[derive(Parser, Debug)]
305pub(crate) struct CommentArgs {
306 #[arg(value_name = "ISSUE_ID")]
308 id: Rev,
309
310 #[arg(long, short)]
312 #[arg(value_name = "MESSAGE")]
313 message: Message,
314
315 #[arg(long, value_name = "COMMENT_ID")]
318 #[arg(conflicts_with = "edit")]
319 reply_to: Option<Rev>,
320
321 #[arg(long, value_name = "COMMENT_ID")]
323 #[arg(conflicts_with = "reply_to")]
324 edit: Option<Rev>,
325}
326
327impl CommentArgs {
328 pub(crate) fn is_edit(&self) -> bool {
332 self.edit.is_some()
333 }
334}
335
336#[derive(Parser, Debug)]
338#[group(required = true, multiple = false)]
339pub(crate) struct StateArgs {
340 #[arg(long)]
342 pub(crate) open: bool,
343
344 #[arg(long)]
346 pub(crate) closed: bool,
347
348 #[arg(long)]
350 pub(crate) solved: bool,
351}
352
353impl From<StateArgs> for StateArg {
354 fn from(state: StateArgs) -> Self {
355 match (state.open, state.closed, state.solved) {
357 (true, _, _) => StateArg::Open,
358 (_, true, _) => StateArg::Closed,
359 (_, _, true) => StateArg::Solved,
360 _ => unreachable!(),
361 }
362 }
363}
364
365#[derive(Clone, Copy, Debug)]
367pub(crate) enum StateArg {
368 Open,
371 Closed,
374 Solved,
377}
378
379impl From<StateArg> for State {
380 fn from(value: StateArg) -> Self {
381 match value {
382 StateArg::Open => Self::Open,
383 StateArg::Closed => Self::Closed {
384 reason: CloseReason::Other,
385 },
386 StateArg::Solved => Self::Closed {
387 reason: CloseReason::Solved,
388 },
389 }
390 }
391}
392
393impl FromStr for Assigned {
394 type Err = DidError;
395
396 fn from_str(s: &str) -> Result<Self, Self::Err> {
397 if s == "me" {
398 Ok(Assigned::Me)
399 } else {
400 let value = s.parse::<Did>()?;
401 Ok(Assigned::Peer(value))
402 }
403 }
404}
405
406pub(crate) enum CommentAction {
408 Comment {
410 id: Rev,
412 message: Message,
414 },
415 Reply {
417 id: Rev,
419 message: Message,
421 reply_to: Rev,
423 },
424 Edit {
426 id: Rev,
428 message: Message,
430 to_edit: Rev,
432 },
433}
434
435impl From<CommentArgs> for CommentAction {
436 fn from(
437 CommentArgs {
438 id,
439 message,
440 reply_to,
441 edit,
442 }: CommentArgs,
443 ) -> Self {
444 match (reply_to, edit) {
445 (Some(_), Some(_)) => {
446 unreachable!("the argument '--reply-to' cannot be used with '--edit'")
447 }
448 (Some(reply_to), None) => Self::Reply {
449 id,
450 message,
451 reply_to,
452 },
453 (None, Some(to_edit)) => Self::Edit {
454 id,
455 message,
456 to_edit,
457 },
458 (None, None) => Self::Comment { id, message },
459 }
460 }
461}