1use clap::Parser;
2
3#[derive(Parser, Debug)]
17#[command(name = "semantic-diff", version, about)]
18pub struct Cli {
19 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
21 pub git_args: Vec<String>,
22}
23
24impl Cli {
25 pub fn git_diff_args(&self) -> Vec<String> {
27 let mut args = vec!["diff".to_string(), "-M".to_string()];
28 args.extend(self.git_args.iter().cloned());
29 args
30 }
31}
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36
37 #[test]
38 fn test_no_args_produces_bare_diff() {
39 let cli = Cli { git_args: vec![] };
40 assert_eq!(cli.git_diff_args(), vec!["diff", "-M"]);
41 }
42
43 #[test]
44 fn test_head_arg() {
45 let cli = Cli {
46 git_args: vec!["HEAD".to_string()],
47 };
48 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "HEAD"]);
49 }
50
51 #[test]
52 fn test_staged_flag() {
53 let cli = Cli {
54 git_args: vec!["--staged".to_string()],
55 };
56 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "--staged"]);
57 }
58
59 #[test]
60 fn test_two_dot_range() {
61 let cli = Cli {
62 git_args: vec!["main..feature".to_string()],
63 };
64 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "main..feature"]);
65 }
66
67 #[test]
68 fn test_three_dot_range() {
69 let cli = Cli {
70 git_args: vec!["main...feature".to_string()],
71 };
72 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "main...feature"]);
73 }
74
75 #[test]
76 fn test_two_refs() {
77 let cli = Cli {
78 git_args: vec!["main".to_string(), "feature".to_string()],
79 };
80 assert_eq!(
81 cli.git_diff_args(),
82 vec!["diff", "-M", "main", "feature"]
83 );
84 }
85
86 #[test]
87 fn test_path_limiter() {
88 let cli = Cli {
89 git_args: vec![
90 "HEAD".to_string(),
91 "--".to_string(),
92 "src/".to_string(),
93 ],
94 };
95 assert_eq!(
96 cli.git_diff_args(),
97 vec!["diff", "-M", "HEAD", "--", "src/"]
98 );
99 }
100
101 #[test]
102 fn test_cached_alias() {
103 let cli = Cli {
104 git_args: vec!["--cached".to_string()],
105 };
106 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "--cached"]);
107 }
108
109 #[test]
112 fn test_head_tilde_syntax() {
113 let cli = Cli {
114 git_args: vec!["HEAD~3".to_string()],
115 };
116 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "HEAD~3"]);
117 }
118
119 #[test]
120 fn test_head_caret_syntax() {
121 let cli = Cli {
122 git_args: vec!["HEAD^".to_string()],
123 };
124 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "HEAD^"]);
125 }
126
127 #[test]
128 fn test_sha_refs() {
129 let cli = Cli {
130 git_args: vec![
131 "abc1234".to_string(),
132 "def5678".to_string(),
133 ],
134 };
135 assert_eq!(
136 cli.git_diff_args(),
137 vec!["diff", "-M", "abc1234", "def5678"]
138 );
139 }
140
141 #[test]
142 fn test_full_sha() {
143 let sha = "a".repeat(40);
144 let cli = Cli {
145 git_args: vec![sha.clone()],
146 };
147 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", &sha]);
148 }
149
150 #[test]
151 fn test_staged_with_ref() {
152 let cli = Cli {
153 git_args: vec!["--staged".to_string(), "HEAD~1".to_string()],
154 };
155 assert_eq!(
156 cli.git_diff_args(),
157 vec!["diff", "-M", "--staged", "HEAD~1"]
158 );
159 }
160
161 #[test]
162 fn test_multiple_path_limiters() {
163 let cli = Cli {
164 git_args: vec![
165 "HEAD".to_string(),
166 "--".to_string(),
167 "src/".to_string(),
168 "tests/".to_string(),
169 "Cargo.toml".to_string(),
170 ],
171 };
172 assert_eq!(
173 cli.git_diff_args(),
174 vec!["diff", "-M", "HEAD", "--", "src/", "tests/", "Cargo.toml"]
175 );
176 }
177
178 #[test]
179 fn test_two_dot_range_with_paths() {
180 let cli = Cli {
181 git_args: vec![
182 "main..feature".to_string(),
183 "--".to_string(),
184 "src/".to_string(),
185 ],
186 };
187 assert_eq!(
188 cli.git_diff_args(),
189 vec!["diff", "-M", "main..feature", "--", "src/"]
190 );
191 }
192
193 #[test]
194 fn test_three_dot_range_with_paths() {
195 let cli = Cli {
196 git_args: vec![
197 "origin/main...HEAD".to_string(),
198 "--".to_string(),
199 "*.rs".to_string(),
200 ],
201 };
202 assert_eq!(
203 cli.git_diff_args(),
204 vec!["diff", "-M", "origin/main...HEAD", "--", "*.rs"]
205 );
206 }
207
208 #[test]
209 fn test_merge_base_flag() {
210 let cli = Cli {
211 git_args: vec!["--merge-base".to_string(), "main".to_string()],
212 };
213 assert_eq!(
214 cli.git_diff_args(),
215 vec!["diff", "-M", "--merge-base", "main"]
216 );
217 }
218
219 #[test]
220 fn test_no_index_flag() {
221 let cli = Cli {
222 git_args: vec![
223 "--no-index".to_string(),
224 "file_a.txt".to_string(),
225 "file_b.txt".to_string(),
226 ],
227 };
228 assert_eq!(
229 cli.git_diff_args(),
230 vec!["diff", "-M", "--no-index", "file_a.txt", "file_b.txt"]
231 );
232 }
233
234 #[test]
235 fn test_many_positional_args_stress() {
236 let args: Vec<String> = (0..100).map(|i| format!("path_{i}.rs")).collect();
237 let cli = Cli {
238 git_args: args.clone(),
239 };
240 let result = cli.git_diff_args();
241 assert_eq!(result.len(), 102); assert_eq!(result[0], "diff");
243 assert_eq!(result[1], "-M");
244 assert_eq!(result[2], "path_0.rs");
245 assert_eq!(result[101], "path_99.rs");
246 }
247
248 #[test]
249 fn test_unicode_path() {
250 let cli = Cli {
251 git_args: vec![
252 "HEAD".to_string(),
253 "--".to_string(),
254 "src/日本語/ファイル.rs".to_string(),
255 ],
256 };
257 let result = cli.git_diff_args();
258 assert_eq!(result[4], "src/日本語/ファイル.rs");
259 }
260
261 #[test]
262 fn test_path_with_spaces() {
263 let cli = Cli {
264 git_args: vec![
265 "--".to_string(),
266 "path with spaces/file.rs".to_string(),
267 ],
268 };
269 let result = cli.git_diff_args();
270 assert_eq!(result[3], "path with spaces/file.rs");
271 }
272
273 #[test]
274 fn test_at_upstream_syntax() {
275 let cli = Cli {
276 git_args: vec!["@{upstream}".to_string()],
277 };
278 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "@{upstream}"]);
279 }
280
281 #[test]
282 fn test_stash_ref() {
283 let cli = Cli {
284 git_args: vec!["stash@{0}".to_string()],
285 };
286 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "stash@{0}"]);
287 }
288
289 #[test]
290 fn test_remote_tracking_branch() {
291 let cli = Cli {
292 git_args: vec![
293 "origin/main".to_string(),
294 "origin/feature/my-branch".to_string(),
295 ],
296 };
297 assert_eq!(
298 cli.git_diff_args(),
299 vec!["diff", "-M", "origin/main", "origin/feature/my-branch"]
300 );
301 }
302
303 #[test]
304 fn test_tag_ref() {
305 let cli = Cli {
306 git_args: vec!["v1.0.0".to_string(), "v2.0.0".to_string()],
307 };
308 assert_eq!(
309 cli.git_diff_args(),
310 vec!["diff", "-M", "v1.0.0", "v2.0.0"]
311 );
312 }
313
314 #[test]
315 fn test_diff_filter_flag_passthrough() {
316 let cli = Cli {
317 git_args: vec!["--diff-filter=ACMR".to_string(), "HEAD".to_string()],
318 };
319 assert_eq!(
320 cli.git_diff_args(),
321 vec!["diff", "-M", "--diff-filter=ACMR", "HEAD"]
322 );
323 }
324
325 #[test]
326 fn test_stat_flag_passthrough() {
327 let cli = Cli {
328 git_args: vec!["--stat".to_string(), "HEAD".to_string()],
329 };
330 assert_eq!(
331 cli.git_diff_args(),
332 vec!["diff", "-M", "--stat", "HEAD"]
333 );
334 }
335
336 #[test]
337 fn test_name_only_flag_passthrough() {
338 let cli = Cli {
339 git_args: vec!["--name-only".to_string()],
340 };
341 assert_eq!(
342 cli.git_diff_args(),
343 vec!["diff", "-M", "--name-only"]
344 );
345 }
346
347 #[test]
348 fn test_combined_flags_and_ranges() {
349 let cli = Cli {
350 git_args: vec![
351 "--staged".to_string(),
352 "--diff-filter=M".to_string(),
353 "HEAD~5".to_string(),
354 "--".to_string(),
355 "src/".to_string(),
356 ],
357 };
358 assert_eq!(
359 cli.git_diff_args(),
360 vec!["diff", "-M", "--staged", "--diff-filter=M", "HEAD~5", "--", "src/"]
361 );
362 }
363
364 #[test]
365 fn test_empty_string_arg() {
366 let cli = Cli {
367 git_args: vec!["".to_string()],
368 };
369 let result = cli.git_diff_args();
370 assert_eq!(result, vec!["diff", "-M", ""]);
371 }
372
373 #[test]
374 fn test_double_dash_only() {
375 let cli = Cli {
376 git_args: vec!["--".to_string()],
377 };
378 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "--"]);
379 }
380
381 #[test]
382 fn test_clap_parse_no_args() {
383 let cli = Cli::try_parse_from(["semantic-diff"]).unwrap();
385 assert!(cli.git_args.is_empty());
386 assert_eq!(cli.git_diff_args(), vec!["diff", "-M"]);
387 }
388
389 #[test]
390 fn test_clap_parse_head() {
391 let cli = Cli::try_parse_from(["semantic-diff", "HEAD"]).unwrap();
392 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "HEAD"]);
393 }
394
395 #[test]
396 fn test_clap_parse_staged() {
397 let cli = Cli::try_parse_from(["semantic-diff", "--staged"]).unwrap();
398 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "--staged"]);
399 }
400
401 #[test]
402 fn test_clap_parse_cached() {
403 let cli = Cli::try_parse_from(["semantic-diff", "--cached"]).unwrap();
404 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "--cached"]);
405 }
406
407 #[test]
408 fn test_clap_parse_two_dot_range() {
409 let cli = Cli::try_parse_from(["semantic-diff", "main..feature"]).unwrap();
410 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "main..feature"]);
411 }
412
413 #[test]
414 fn test_clap_parse_three_dot_range() {
415 let cli = Cli::try_parse_from(["semantic-diff", "main...feature"]).unwrap();
416 assert_eq!(cli.git_diff_args(), vec!["diff", "-M", "main...feature"]);
417 }
418
419 #[test]
420 fn test_clap_parse_two_refs() {
421 let cli = Cli::try_parse_from(["semantic-diff", "abc123", "def456"]).unwrap();
422 assert_eq!(
423 cli.git_diff_args(),
424 vec!["diff", "-M", "abc123", "def456"]
425 );
426 }
427
428 #[test]
429 fn test_clap_parse_ref_with_paths() {
430 let cli = Cli::try_parse_from([
431 "semantic-diff",
432 "HEAD~3",
433 "--",
434 "src/main.rs",
435 "src/lib.rs",
436 ])
437 .unwrap();
438 assert_eq!(
439 cli.git_diff_args(),
440 vec!["diff", "-M", "HEAD~3", "--", "src/main.rs", "src/lib.rs"]
441 );
442 }
443
444 #[test]
445 fn test_clap_parse_complex_scenario() {
446 let cli = Cli::try_parse_from([
447 "semantic-diff",
448 "--staged",
449 "--diff-filter=ACMR",
450 "HEAD~5",
451 "--",
452 "src/",
453 "tests/",
454 ])
455 .unwrap();
456 assert_eq!(
457 cli.git_diff_args(),
458 vec![
459 "diff",
460 "-M",
461 "--staged",
462 "--diff-filter=ACMR",
463 "HEAD~5",
464 "--",
465 "src/",
466 "tests/"
467 ]
468 );
469 }
470
471 #[test]
472 fn test_clap_parse_merge_base() {
473 let cli =
474 Cli::try_parse_from(["semantic-diff", "--merge-base", "main"]).unwrap();
475 assert_eq!(
476 cli.git_diff_args(),
477 vec!["diff", "-M", "--merge-base", "main"]
478 );
479 }
480
481 #[test]
482 fn test_clap_version_does_not_conflict() {
483 let result = Cli::try_parse_from(["semantic-diff", "--version"]);
485 assert!(result.is_err());
487 }
488
489 #[test]
490 fn test_clap_help_does_not_conflict() {
491 let result = Cli::try_parse_from(["semantic-diff", "--help"]);
492 assert!(result.is_err());
493 }
494}