Skip to main content

git_cli/
completion.rs

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}