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