tix/cli/cli_bulk.rs
1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4/// `tix bulk <action>` — four actions, and each carries **its own** flags.
5///
6/// The flags are on the subcommand rather than on the parent, which is what makes the
7/// surface unambiguous by construction rather than by a runtime check:
8/// `tix bulk archive --priority high` is a **clap usage error**, not a command that
9/// accepts a field flag and ignores it. `edit` is the only action with field flags,
10/// `link`/`unlink` are the only ones with `--target`, and `archive` takes neither — so
11/// there is no spelling of `tix bulk` that a flag can apply to by accident.
12///
13/// `archive` has no `--type`: archiving is not a relationship, and offering a kind
14/// would suggest it is one.
15/// The flags every `tix bulk` action carries: which issues, and whether to write.
16///
17/// Flattened into **each** action rather than held on `Command::Bulk`, because clap
18/// parses a parent's flags *before* its subcommand — so a parent would spell the
19/// contract's `tix bulk edit --select <...>` as `tix bulk --select <...> edit`, which
20/// is a different surface from the one the contract names. Four `#[command(flatten)]`
21/// of one struct gives the contract's spelling and one vocabulary, so a new flag
22/// cannot be added to one action and forgotten on the other three.
23#[derive(Args, Debug, Clone, PartialEq, Eq)]
24pub struct BulkSelection {
25 /// Comma-separated issue selectors: number, filename fragment or unique title
26 /// fragment
27 #[arg(long, value_name = "LIST")]
28 pub select: Option<String>,
29 /// A `--filter` query, in the existing filter grammar
30 #[arg(long, value_name = "QUERY")]
31 pub filter: Option<String>,
32 /// A `--search` query, in the existing search grammar
33 #[arg(long, value_name = "QUERY")]
34 pub search: Option<String>,
35 /// Which issues are in play: `active`, `active+archived` or `archived`
36 #[arg(long, value_name = "SCOPE")]
37 pub scope: Option<String>,
38 /// Write the batch. Omit for a preview, which is the default.
39 #[arg(long)]
40 pub apply: bool,
41}
42
43impl BulkAction {
44 /// The shared selection flags, whichever action carries them.
45 ///
46 /// One accessor for four flattened fields, so the routing code reads the flags
47 /// once and a new action cannot forget to pass them.
48 pub fn selection(&self) -> &BulkSelection {
49 match self {
50 Self::Edit { selection, .. }
51 | Self::Link { selection, .. }
52 | Self::Unlink { selection, .. }
53 | Self::Archive { selection } => selection,
54 }
55 }
56
57 /// Whether `--apply` was given. A preview is the default, so this is a positive
58 /// question rather than "is this not a preview".
59 pub fn is_apply(&self) -> bool {
60 self.selection().apply
61 }
62}
63
64#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
65pub enum BulkAction {
66 /// Apply a typed field patch to every selected issue
67 Edit {
68 #[command(flatten)]
69 selection: BulkSelection,
70 /// Target column
71 #[arg(long, value_name = "COLUMN")]
72 status: Option<String>,
73 /// `low`, `medium`, `high`, or `` for unset
74 #[arg(long, value_name = "PRIORITY")]
75 priority: Option<String>,
76 /// Bare handle or full identity
77 #[arg(long, value_name = "ASSIGNEE")]
78 assignee: Option<String>,
79 /// Comma-separated labels
80 #[arg(long, value_name = "LABELS")]
81 labels: Option<String>,
82 /// `YYYY-MM-DD`
83 #[arg(long, value_name = "DATE")]
84 start_date: Option<String>,
85 /// `YYYY-MM-DD`
86 #[arg(long, value_name = "DATE")]
87 due_date: Option<String>,
88 /// Estimate: `3`, `3d`, `1 day`, `0.5-1 day`, `~300 LOC, 0.5-1 day`
89 #[arg(long, value_name = "ESTIMATE")]
90 estimate: Option<String>,
91 /// Comma-separated cycle names
92 #[arg(long, value_name = "CYCLES")]
93 cycles: Option<String>,
94 /// Comma-separated milestone names
95 #[arg(long, value_name = "MILESTONES")]
96 milestones: Option<String>,
97 /// Empty a field. Takes one of the clearable field names.
98 #[arg(long, value_name = "FIELD")]
99 clear: Option<String>,
100 },
101 /// Link every selected issue to `--target`
102 Link {
103 #[command(flatten)]
104 selection: BulkSelection,
105 /// The other end of the relationship
106 #[arg(long, value_name = "SELECTOR")]
107 target: String,
108 /// Relationship kind: `blocks`, `blocked_by`, `duplicates`, `related`
109 #[arg(long, value_name = "KIND")]
110 r#type: Option<String>,
111 },
112 /// Unlink every selected issue from `--target`
113 Unlink {
114 #[command(flatten)]
115 selection: BulkSelection,
116 /// The other end of the relationship
117 #[arg(long, value_name = "SELECTOR")]
118 target: String,
119 /// Relationship kind: `blocks`, `blocked_by`, `duplicates`, `related`
120 #[arg(long, value_name = "KIND")]
121 r#type: Option<String>,
122 },
123 /// Ask the archive service to archive every selected issue
124 Archive {
125 #[command(flatten)]
126 selection: BulkSelection,
127 },
128}