1use clap::{Arg, ArgAction, Command, ValueHint};
2use clap_complete::{Shell, generate};
3use std::io::{self, Write};
4
5pub fn dispatch(shell_raw: &str, extra: &[String]) -> i32 {
6 if !extra.is_empty() {
7 eprintln!("git-cli: error: expected `git-cli completion <bash|zsh>`");
8 return 1;
9 }
10
11 match shell_raw {
12 "bash" => generate_script(Shell::Bash),
13 "zsh" => generate_script(Shell::Zsh),
14 other => {
15 eprintln!("git-cli: error: unsupported completion shell '{other}'");
16 eprintln!("usage: git-cli completion <bash|zsh>");
17 1
18 }
19 }
20}
21
22fn generate_script(generator: Shell) -> i32 {
23 let mut command = build_command_model();
24 let bin_name = command.get_name().to_string();
25 if matches!(generator, Shell::Bash) {
26 let mut output = Vec::new();
27 generate(generator, &mut command, bin_name.clone(), &mut output);
28 let normalized = normalize_bash_completion(
29 String::from_utf8(output).expect("bash completion should be valid UTF-8"),
30 );
31 io::stdout()
32 .write_all(normalized.as_bytes())
33 .expect("failed to write bash completion");
34 return 0;
35 }
36
37 generate(generator, &mut command, bin_name, &mut io::stdout());
38 0
39}
40
41fn normalize_bash_completion(script: String) -> String {
42 script.replace("__subcmd__", "__")
43}
44
45fn build_command_model() -> Command {
46 Command::new("git-cli")
47 .version(env!("CARGO_PKG_VERSION"))
48 .about("Git helper CLI")
49 .disable_help_subcommand(true)
50 .subcommand(build_utils_group())
51 .subcommand(build_reset_group())
52 .subcommand(build_commit_group())
53 .subcommand(build_branch_group())
54 .subcommand(build_ci_group())
55 .subcommand(build_open_group())
56 .subcommand(Command::new("help").about("Display help message for git-cli"))
57 .subcommand(
58 Command::new("completion")
59 .about("Export shell completion script")
60 .arg(
61 Arg::new("shell")
62 .value_name("shell")
63 .value_parser(["bash", "zsh"])
64 .required(true),
65 ),
66 )
67}
68
69fn build_utils_group() -> Command {
70 Command::new("utils")
71 .about("Utility helpers")
72 .subcommand(Command::new("zip").about("Create zip archive from HEAD"))
73 .subcommand(
74 Command::new("copy-staged")
75 .visible_alias("copy")
76 .about("Copy staged diff to clipboard")
77 .arg(
78 Arg::new("stdout")
79 .long("stdout")
80 .help("Print staged diff to stdout")
81 .action(ArgAction::SetTrue),
82 )
83 .arg(
84 Arg::new("print")
85 .short('p')
86 .long("print")
87 .help("Alias for --stdout")
88 .action(ArgAction::SetTrue),
89 )
90 .arg(
91 Arg::new("both")
92 .long("both")
93 .help("Print diff and copy it to clipboard")
94 .action(ArgAction::SetTrue),
95 ),
96 )
97 .subcommand(
98 Command::new("root").about("Jump to git root").arg(
99 Arg::new("shell")
100 .long("shell")
101 .help("Print shell command instead of plain output")
102 .action(ArgAction::SetTrue),
103 ),
104 )
105 .subcommand(
106 Command::new("commit-hash")
107 .visible_alias("hash")
108 .about("Resolve commit hash")
109 .arg(Arg::new("ref").value_name("ref")),
110 )
111 .subcommand(Command::new("help").about("Display help message for utils"))
112}
113
114fn build_reset_group() -> Command {
115 let count_arg = || Arg::new("count").value_name("count");
116
117 Command::new("reset")
118 .about("Reset helpers")
119 .subcommand(
120 Command::new("soft")
121 .about("Reset to HEAD~N (soft)")
122 .arg(count_arg()),
123 )
124 .subcommand(
125 Command::new("mixed")
126 .about("Reset to HEAD~N (mixed)")
127 .arg(count_arg()),
128 )
129 .subcommand(
130 Command::new("hard")
131 .about("Reset to HEAD~N (hard)")
132 .arg(count_arg()),
133 )
134 .subcommand(Command::new("undo").about("Undo last reset"))
135 .subcommand(Command::new("back-head").about("Checkout HEAD@{1}"))
136 .subcommand(Command::new("back-checkout").about("Return to previous branch"))
137 .subcommand(
138 Command::new("remote")
139 .about("Reset to remote branch")
140 .arg(
141 Arg::new("ref")
142 .long("ref")
143 .help("Remote ref in <remote>/<branch> form")
144 .value_name("ref"),
145 )
146 .arg(
147 Arg::new("remote")
148 .short('r')
149 .long("remote")
150 .help("Remote name")
151 .value_name("remote"),
152 )
153 .arg(
154 Arg::new("branch")
155 .short('b')
156 .long("branch")
157 .help("Remote branch name")
158 .value_name("branch"),
159 )
160 .arg(
161 Arg::new("no-fetch")
162 .long("no-fetch")
163 .help("Skip fetching remote refs")
164 .action(ArgAction::SetTrue),
165 )
166 .arg(
167 Arg::new("prune")
168 .long("prune")
169 .help("Run fetch with --prune")
170 .action(ArgAction::SetTrue),
171 )
172 .arg(
173 Arg::new("clean")
174 .long("clean")
175 .help("Run git clean -fd after reset")
176 .action(ArgAction::SetTrue),
177 )
178 .arg(
179 Arg::new("set-upstream")
180 .long("set-upstream")
181 .help("Set upstream to the target remote branch")
182 .action(ArgAction::SetTrue),
183 )
184 .arg(
185 Arg::new("yes")
186 .short('y')
187 .long("yes")
188 .help("Skip confirmation prompts")
189 .action(ArgAction::SetTrue),
190 ),
191 )
192 .subcommand(Command::new("help").about("Display help message for reset"))
193}
194
195fn build_commit_group() -> Command {
196 Command::new("commit")
197 .about("Commit helpers")
198 .subcommand(
199 Command::new("context")
200 .about("Print commit context")
201 .arg(
202 Arg::new("stdout")
203 .long("stdout")
204 .help("Print report to stdout")
205 .action(ArgAction::SetTrue),
206 )
207 .arg(
208 Arg::new("both")
209 .long("both")
210 .help("Print report and write output file")
211 .action(ArgAction::SetTrue),
212 )
213 .arg(
214 Arg::new("no-color")
215 .long("no-color")
216 .help("Disable ANSI colors")
217 .action(ArgAction::SetTrue),
218 )
219 .arg(
220 Arg::new("include")
221 .long("include")
222 .help("Additional glob(s) to include")
223 .value_name("glob")
224 .num_args(1..),
225 ),
226 )
227 .subcommand(
228 Command::new("context-json")
229 .visible_aliases(["context_json", "contextjson", "json"])
230 .about("Print commit context as JSON")
231 .arg(
232 Arg::new("stdout")
233 .long("stdout")
234 .help("Print JSON to stdout")
235 .action(ArgAction::SetTrue),
236 )
237 .arg(
238 Arg::new("both")
239 .long("both")
240 .help("Print JSON and write files")
241 .action(ArgAction::SetTrue),
242 )
243 .arg(
244 Arg::new("pretty")
245 .long("pretty")
246 .help("Pretty-print JSON output")
247 .action(ArgAction::SetTrue),
248 )
249 .arg(
250 Arg::new("bundle")
251 .long("bundle")
252 .help("Write bundle files to output directory")
253 .action(ArgAction::SetTrue),
254 )
255 .arg(
256 Arg::new("out-dir")
257 .long("out-dir")
258 .help("Output directory for generated files")
259 .value_name("path"),
260 ),
261 )
262 .subcommand(
263 Command::new("to-stash")
264 .visible_alias("stash")
265 .about("Create stash from commit")
266 .arg(Arg::new("ref").value_name("ref")),
267 )
268 .subcommand(Command::new("help").about("Display help message for commit"))
269}
270
271fn build_branch_group() -> Command {
272 Command::new("branch")
273 .about("Branch helpers")
274 .subcommand(
275 Command::new("cleanup")
276 .visible_alias("delete-merged")
277 .about("Delete merged branches")
278 .arg(
279 Arg::new("base")
280 .short('b')
281 .long("base")
282 .help("Base ref used to determine merged branches")
283 .value_name("base"),
284 )
285 .arg(
286 Arg::new("squash")
287 .short('s')
288 .long("squash")
289 .help("Include branches already applied via squash")
290 .action(ArgAction::SetTrue),
291 )
292 .arg(
293 Arg::new("remove-worktrees")
294 .short('w')
295 .long("remove-worktrees")
296 .help("Force-remove linked worktrees for candidate branches")
297 .action(ArgAction::SetTrue),
298 ),
299 )
300 .subcommand(Command::new("help").about("Display help message for branch"))
301}
302
303fn build_ci_group() -> Command {
304 Command::new("ci")
305 .about("CI helpers")
306 .subcommand(
307 Command::new("pick")
308 .about("Cherry-pick into CI branch")
309 .arg(
310 Arg::new("remote")
311 .short('r')
312 .long("remote")
313 .help("Remote used for fetch/push")
314 .value_name("name"),
315 )
316 .arg(
317 Arg::new("no-fetch")
318 .long("no-fetch")
319 .help("Skip remote fetch before branch creation")
320 .action(ArgAction::SetTrue),
321 )
322 .arg(
323 Arg::new("force")
324 .short('f')
325 .long("force")
326 .help("Reset existing CI branch and force push")
327 .action(ArgAction::SetTrue),
328 )
329 .arg(
330 Arg::new("stay")
331 .long("stay")
332 .help("Stay on CI branch after push")
333 .action(ArgAction::SetTrue),
334 ),
335 )
336 .subcommand(Command::new("help").about("Display help message for ci"))
337}
338
339fn build_open_group() -> Command {
340 Command::new("open")
341 .about("Open remote pages")
342 .subcommand(
343 Command::new("repo")
344 .about("Open repository page")
345 .arg(remotes_arg()),
346 )
347 .subcommand(
348 Command::new("branch")
349 .about("Open branch tree page")
350 .arg(Arg::new("ref").value_name("ref")),
351 )
352 .subcommand(
353 Command::new("default-branch")
354 .visible_alias("default")
355 .about("Open default branch tree page")
356 .arg(remotes_arg()),
357 )
358 .subcommand(
359 Command::new("commit")
360 .about("Open commit page")
361 .arg(Arg::new("ref").value_name("ref")),
362 )
363 .subcommand(
364 Command::new("compare")
365 .about("Open compare page")
366 .arg(Arg::new("from").value_name("from"))
367 .arg(Arg::new("to").value_name("to")),
368 )
369 .subcommand(
370 Command::new("pr")
371 .visible_aliases(["pull-request", "mr", "merge-request"])
372 .about("Open pull or merge request page")
373 .arg(Arg::new("id").value_name("id")),
374 )
375 .subcommand(
376 Command::new("pulls")
377 .visible_aliases(["prs", "merge-requests", "mrs"])
378 .about("Open pull or merge request list"),
379 )
380 .subcommand(
381 Command::new("issues")
382 .visible_alias("issue")
383 .about("Open issues list/page")
384 .arg(Arg::new("id").value_name("id")),
385 )
386 .subcommand(
387 Command::new("actions")
388 .visible_alias("action")
389 .about("Open actions page")
390 .arg(Arg::new("workflow").value_name("workflow")),
391 )
392 .subcommand(
393 Command::new("releases")
394 .visible_alias("release")
395 .about("Open releases list/page")
396 .arg(Arg::new("tag").value_name("tag")),
397 )
398 .subcommand(
399 Command::new("tags")
400 .visible_alias("tag")
401 .about("Open tags list/page")
402 .arg(Arg::new("tag").value_name("tag")),
403 )
404 .subcommand(
405 Command::new("commits")
406 .visible_alias("history")
407 .about("Open commit history page")
408 .arg(Arg::new("ref").value_name("ref")),
409 )
410 .subcommand(
411 Command::new("file")
412 .visible_alias("blob")
413 .about("Open file page")
414 .arg(
415 Arg::new("path")
416 .value_name("path")
417 .value_hint(ValueHint::FilePath),
418 )
419 .arg(Arg::new("ref").value_name("ref")),
420 )
421 .subcommand(
422 Command::new("blame")
423 .about("Open blame page")
424 .arg(
425 Arg::new("path")
426 .value_name("path")
427 .value_hint(ValueHint::FilePath),
428 )
429 .arg(Arg::new("ref").value_name("ref")),
430 )
431 .subcommand(Command::new("help").about("Display help message for open"))
432}
433
434fn remotes_arg() -> Arg {
435 Arg::new("remote").value_name("remote")
436}