Skip to main content

git_cli/
completion.rs

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