Skip to main content

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}