1use clap::{Args, Subcommand};
5
6use super::{AuthoredMessageArgs, CodeScopeArgs, HistoricalRevisionArgs, RemoteChoiceArgs};
7
8#[derive(Clone, Debug, Args)]
10pub struct DiscussArgs {
11 #[command(subcommand)]
12 pub command: DiscussCommands,
13}
14
15#[derive(Clone, Debug, Subcommand)]
16pub enum DiscussCommands {
17 New(DiscussNewArgs),
19 Reply(DiscussReplyArgs),
21 Resolve(DiscussResolveArgs),
23 Reopen(DiscussReopenArgs),
25 List(DiscussListArgs),
27 Show(DiscussShowArgs),
29 Wait(DiscussWaitArgs),
31}
32
33#[derive(Clone, Debug, Args)]
34pub struct DiscussNewArgs {
35 #[command(flatten)]
36 pub scope: CodeScopeArgs,
37 #[command(flatten)]
38 pub revision: HistoricalRevisionArgs,
39 #[command(flatten)]
40 pub message: AuthoredMessageArgs,
41 #[arg(long)]
43 pub title: Option<String>,
44 #[arg(long)]
46 pub visibility: Option<String>,
47 #[arg(long, value_name = "REF")]
49 pub thread: Option<String>,
50}
51
52#[derive(Clone, Debug, Args)]
53pub struct DiscussReplyArgs {
54 #[arg(value_name = "ID")]
56 pub discussion_id: String,
57 #[command(flatten)]
58 pub message: AuthoredMessageArgs,
59 #[arg(long)]
61 pub turn: Option<u32>,
62}
63
64#[derive(Clone, Debug, Args)]
65pub struct DiscussResolveArgs {
66 #[arg(value_name = "ID")]
68 pub discussion_id: String,
69 #[arg(long, value_enum)]
71 pub mode: ResolveModeArg,
72 #[command(flatten)]
74 pub revision: HistoricalRevisionArgs,
75 #[arg(long)]
77 pub reason: Option<String>,
78 #[command(flatten)]
80 pub message: AuthoredMessageArgs,
81 #[arg(long, value_parser = ["constraint", "invariant", "rationale"])]
83 pub kind: Option<String>,
84 #[arg(long)]
86 pub tag: Vec<String>,
87}
88
89#[derive(Clone, Debug, clap::ValueEnum)]
90pub enum ResolveModeArg {
91 ByEdit,
92 Dismiss,
93 IntoAnnotation,
94}
95
96#[derive(Clone, Debug, Args)]
97pub struct DiscussReopenArgs {
98 pub discussion_id: String,
99 #[arg(long)]
101 pub reason: String,
102}
103
104#[derive(Clone, Debug, Args)]
105pub struct DiscussListArgs {
106 #[command(flatten)]
107 pub scope: CodeScopeArgs,
108 #[command(flatten)]
110 pub revision: HistoricalRevisionArgs,
111 #[arg(long, default_value = "open")]
113 pub status: String,
114}
115
116#[derive(Clone, Debug, Args)]
117pub struct DiscussShowArgs {
118 #[arg(value_name = "ID")]
120 pub discussion_id: String,
121}
122
123#[derive(Clone, Debug, Args)]
124pub struct DiscussWaitArgs {
125 #[arg(long)]
127 pub after: Option<i64>,
128 #[command(flatten)]
129 pub remote_choice: RemoteChoiceArgs,
130 #[arg(long)]
132 pub thread: Option<String>,
133 #[arg(long, hide = true)]
135 pub max_events: Option<usize>,
136}
137
138#[cfg(test)]
139mod tests {
140 use clap::Parser;
141
142 use crate::cli::{Cli, Commands, DiscussCommands, ResolveModeArg};
143
144 fn discuss(cli: Cli) -> crate::cli::DiscussArgs {
145 match cli.command {
146 Commands::Discuss(args) => args,
147 _ => panic!("expected discuss"),
148 }
149 }
150
151 #[test]
152 fn writes_are_subcommands() {
153 let opened = discuss(
154 Cli::try_parse_from([
155 "heddle",
156 "discuss",
157 "new",
158 "--path",
159 "src/lib.rs",
160 "--symbol",
161 "greet",
162 "--body",
163 "why greet?",
164 ])
165 .expect("discuss new"),
166 );
167 match opened.command {
168 DiscussCommands::New(args) => {
169 assert_eq!(args.scope.path.as_deref(), Some("src/lib.rs"));
170 assert_eq!(args.scope.symbol.as_deref(), Some("greet"));
171 assert_eq!(args.message.body.as_deref(), Some("why greet?"));
172 }
173 _ => panic!("expected discuss new"),
174 }
175
176 let reply = discuss(
177 Cli::try_parse_from([
178 "heddle",
179 "discuss",
180 "reply",
181 "disc-01a0afc6",
182 "--body",
183 "second thought",
184 ])
185 .expect("discuss reply"),
186 );
187 match reply.command {
188 DiscussCommands::Reply(args) => {
189 assert_eq!(args.discussion_id, "disc-01a0afc6");
190 assert_eq!(args.message.body.as_deref(), Some("second thought"));
191 assert!(args.turn.is_none());
192 }
193 _ => panic!("expected discuss reply"),
194 }
195
196 let threaded = discuss(
197 Cli::try_parse_from([
198 "heddle",
199 "discuss",
200 "reply",
201 "disc-01a0afc6",
202 "--turn",
203 "2",
204 "--body",
205 "reply to that turn",
206 ])
207 .expect("discuss reply --turn"),
208 );
209 match threaded.command {
210 DiscussCommands::Reply(args) => {
211 assert_eq!(args.turn, Some(2));
212 assert_eq!(args.message.body.as_deref(), Some("reply to that turn"));
213 }
214 _ => panic!("expected discuss reply"),
215 }
216
217 let from_file = discuss(
218 Cli::try_parse_from([
219 "heddle",
220 "discuss",
221 "new",
222 "--path",
223 "src/lib.rs",
224 "--file",
225 "why.md",
226 ])
227 .expect("discuss new --file"),
228 );
229 match from_file.command {
230 DiscussCommands::New(args) => {
231 assert_eq!(
232 args.message.file.as_deref(),
233 Some(std::path::Path::new("why.md"))
234 );
235 assert!(args.message.body.is_none());
236 }
237 _ => panic!("expected discuss new"),
238 }
239 }
240
241 #[test]
242 fn write_modes_on_parent_are_gone() {
243 for argv in [
244 [
245 "heddle",
246 "discuss",
247 "--new",
248 "--path",
249 "src/lib.rs",
250 "--body",
251 "x",
252 ]
253 .as_slice(),
254 ["heddle", "discuss", "--id", "disc-01a0afc6", "--body", "x"].as_slice(),
255 [
256 "heddle",
257 "discuss",
258 "open",
259 "src/lib.rs",
260 "greet",
261 "why greet?",
262 ]
263 .as_slice(),
264 ] {
265 assert!(
266 Cli::try_parse_from(argv).is_err(),
267 "{argv:?} must not parse"
268 );
269 }
270 }
271
272 #[test]
273 fn resolve_uses_mode_only() {
274 let resolved = discuss(
275 Cli::try_parse_from([
276 "heddle", "discuss", "resolve", "disc-id", "--mode", "dismiss", "--reason", "done",
277 ])
278 .expect("resolve --mode dismiss"),
279 );
280 match resolved.command {
281 DiscussCommands::Resolve(args) => {
282 assert!(matches!(args.mode, ResolveModeArg::Dismiss));
283 assert_eq!(args.reason.as_deref(), Some("done"));
284 }
285 _ => panic!("expected discuss resolve"),
286 }
287 assert!(
288 Cli::try_parse_from([
289 "heddle",
290 "discuss",
291 "resolve",
292 "disc-id",
293 "--dismiss",
294 "--reason",
295 "done",
296 ])
297 .is_err(),
298 "--dismiss synonym is removed"
299 );
300 assert!(
301 Cli::try_parse_from(["heddle", "discuss", "resolve", "disc-id", "--by-edit"]).is_err(),
302 "--by-edit synonym is removed"
303 );
304 let into = discuss(
305 Cli::try_parse_from([
306 "heddle",
307 "discuss",
308 "resolve",
309 "disc-id",
310 "--mode",
311 "into-annotation",
312 "--body",
313 "keep this",
314 ])
315 .expect("resolve into-annotation"),
316 );
317 match into.command {
318 DiscussCommands::Resolve(args) => {
319 assert!(matches!(args.mode, ResolveModeArg::IntoAnnotation));
320 assert_eq!(args.message.body.as_deref(), Some("keep this"));
321 }
322 _ => panic!("expected discuss resolve"),
323 }
324 }
325
326 #[test]
327 fn list_filters_by_path() {
328 let listed = discuss(
329 Cli::try_parse_from(["heddle", "discuss", "list", "--path", "src/lib.rs"])
330 .expect("list --path"),
331 );
332 match listed.command {
333 DiscussCommands::List(args) => {
334 assert_eq!(args.scope.path.as_deref(), Some("src/lib.rs"));
335 }
336 _ => panic!("expected discuss list"),
337 }
338 assert!(
339 Cli::try_parse_from(["heddle", "discuss", "list", "--file", "src/lib.rs"]).is_err(),
340 "list --file is not a path filter"
341 );
342 }
343}