tix/cli/cli_time.rs
1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4/// The `tix time` subcommands (mission `per-issue-time-tracking-01M3C9A4`, WP04).
5///
6/// A thin adapter: the entry model and its validation, the mutation service and its
7/// append-only History, and every filter, total and finding live under
8/// `crate::time_tracking`. This enum owns only the spelling of the surface — the
9/// duration is whole minutes, a date is a calendar `YYYY-MM-DD`, and there is no flag
10/// here that could express a timer, a timezone, an overlap rule or a billing unit
11/// (C-002/C-009).
12#[derive(Debug, Clone, Subcommand)]
13pub enum TimeCommand {
14 /// Record effort against an issue, in whole minutes
15 ///
16 /// The date is optional and is a calendar date, never an instant: an entry without
17 /// one is real, and a period-bounded read reports it as undated rather than as zero.
18 Add {
19 /// Issue number, filename fragment or unique title fragment
20 #[arg(value_name = "ISSUE")]
21 issue: String,
22 /// The effort, in whole minutes at or above zero
23 ///
24 /// A negative value is accepted by the parser and refused by the shared entry
25 /// validator with its field name, which is a better message than clap's
26 /// "unexpected argument".
27 #[arg(long, value_name = "N", allow_hyphen_values = true)]
28 minutes: i64,
29 /// The calendar date the effort belongs to, `YYYY-MM-DD`
30 #[arg(long, value_name = "YYYY-MM-DD")]
31 date: Option<String>,
32 /// Who did the work: a handle (`ada`, `@ada`) or a `Name <email>` identity
33 #[arg(long, value_name = "IDENTITY")]
34 author: String,
35 /// Free-text context, preserved verbatim
36 #[arg(long, value_name = "TEXT")]
37 note: Option<String>,
38 },
39 /// Correct one entry, naming only the fields that change
40 ///
41 /// A field the command does not name is left exactly as it was, including the entry's
42 /// date, note and any metadata another build attached. `--clear-date` and
43 /// `--clear-note` are how an optional value is removed (an empty `--note ""` would be
44 /// a value, not an absence). A correction requires `--reason`: the prior value stays
45 /// in History, and the reason is what makes the change attributable.
46 Edit {
47 /// Issue number, filename fragment or unique title fragment
48 #[arg(value_name = "ISSUE")]
49 issue: String,
50 /// The stable id of the entry to correct (`tix time show <ISSUE>` lists them)
51 #[arg(long, value_name = "ID")]
52 entry: String,
53 #[command(flatten)]
54 change: TimeEditArgs,
55 },
56 /// Remove one entry from the issue's current entries
57 ///
58 /// The entry leaves the frontmatter; its value and its event stay in History, so a
59 /// deletion is auditable rather than a rewrite. Requires `--reason` and `--confirm`.
60 Delete {
61 /// Issue number, filename fragment or unique title fragment
62 #[arg(value_name = "ISSUE")]
63 issue: String,
64 /// The stable id of the entry to remove
65 #[arg(long, value_name = "ID")]
66 entry: String,
67 /// Why the entry is being removed; recorded in History
68 #[arg(long, value_name = "TEXT")]
69 reason: Option<String>,
70 /// Confirm the removal without prompting
71 #[arg(long)]
72 confirm: bool,
73 },
74 /// Show an issue's current entries and its total
75 Show {
76 /// Issue number, filename fragment or unique title fragment
77 #[arg(value_name = "ISSUE")]
78 issue: String,
79 },
80 /// Show what History retains for an issue: current entries beside its audit
81 History {
82 /// Issue number, filename fragment or unique title fragment
83 #[arg(value_name = "ISSUE")]
84 issue: String,
85 },
86 /// Total recorded effort, by issue, assignee and calendar period
87 ///
88 /// Every value comes from the shared calculation; a scope with nothing to total and a
89 /// scope whose entries carry no usable duration print `-` with their state, never a
90 /// zero. Deleted entries are excluded unless `--audit` asks for them.
91 Total {
92 /// Issue number, filename fragment or unique title fragment (alternative to `--issue`)
93 #[arg(value_name = "ISSUE")]
94 selector: Option<String>,
95 #[command(flatten)]
96 filter: TimeTotalArgs,
97 },
98}
99
100/// The fields an entry correction can name (T020).
101///
102/// A patch, not a replacement: a flag that is absent leaves its field alone, which is why
103/// removal is spelled `--clear-date`/`--clear-note` rather than an empty value. The reason
104/// is required — by the service, which refuses a missing one by name — and is what the
105/// History event keeps once the prior value is replaced.
106#[derive(Debug, Clone, Args)]
107pub struct TimeEditArgs {
108 /// Replacement duration, in whole minutes
109 #[arg(long, value_name = "N", allow_hyphen_values = true)]
110 pub minutes: Option<i64>,
111 /// Replacement calendar date, `YYYY-MM-DD`
112 #[arg(long, value_name = "YYYY-MM-DD", conflicts_with = "clear_date")]
113 pub date: Option<String>,
114 /// Remove the entry's date
115 #[arg(long)]
116 pub clear_date: bool,
117 /// Replacement note
118 #[arg(long, value_name = "TEXT", conflicts_with = "clear_note")]
119 pub note: Option<String>,
120 /// Remove the entry's note
121 #[arg(long)]
122 pub clear_note: bool,
123 /// Replacement author identity
124 #[arg(long, value_name = "IDENTITY")]
125 pub author: Option<String>,
126 /// Why the entry is being corrected; recorded in History, and required
127 #[arg(long, value_name = "TEXT")]
128 pub reason: Option<String>,
129}
130
131/// The read-only scope a `tix time total` is evaluated under (T021).
132///
133/// Every criterion is optional and every named one must match, and the period is
134/// inclusive. The dates are the caller's: this surface never resolves a `today`, so a
135/// total means exactly the calendar window it was asked for.
136#[derive(Debug, Clone, Args)]
137pub struct TimeTotalArgs {
138 /// Stable issue number to scope the total to
139 #[arg(long, value_name = "N")]
140 pub issue: Option<u32>,
141 /// The identity that did the work (an entry's author)
142 #[arg(long, value_name = "IDENTITY")]
143 pub assignee: Option<String>,
144 /// Inclusive period start, `YYYY-MM-DD`
145 #[arg(long, value_name = "YYYY-MM-DD")]
146 pub from: Option<String>,
147 /// Inclusive period end, `YYYY-MM-DD`
148 #[arg(long, value_name = "YYYY-MM-DD")]
149 pub to: Option<String>,
150 /// Also report the entries deletions left in History (never added to the total)
151 #[arg(long)]
152 pub audit: bool,
153 /// Break the total down: `issue`, `assignee` or `date`
154 #[arg(long, value_name = "DIMENSION")]
155 pub by: Option<String>,
156}