Skip to main content

git_cli/
usage.rs

1use std::ffi::OsString;
2use std::io::{self, Write};
3
4use crate::commit;
5use crate::{branch, ci, completion, open, reset, utils};
6
7#[derive(Debug, Clone, Copy)]
8enum Group {
9    Utils,
10    Reset,
11    Commit,
12    Branch,
13    Ci,
14    Open,
15    Completion,
16}
17
18impl Group {
19    fn parse(raw: &str) -> Option<Self> {
20        match raw {
21            "utils" => Some(Self::Utils),
22            "reset" => Some(Self::Reset),
23            "commit" => Some(Self::Commit),
24            "branch" => Some(Self::Branch),
25            "ci" => Some(Self::Ci),
26            "open" => Some(Self::Open),
27            "completion" => Some(Self::Completion),
28            _ => None,
29        }
30    }
31}
32
33pub fn dispatch(args: Vec<OsString>) -> i32 {
34    let args: Vec<String> = args
35        .into_iter()
36        .map(|v| v.to_string_lossy().to_string())
37        .collect();
38
39    if args.is_empty() {
40        print_top_level_usage(&mut io::stdout());
41        return 0;
42    }
43
44    let group_raw = &args[0];
45    if is_version_token(group_raw) {
46        print_version(&mut io::stdout());
47        return 0;
48    }
49    if is_help_token(group_raw) {
50        print_top_level_usage(&mut io::stdout());
51        return 0;
52    }
53
54    let cmd_raw = args.get(1);
55    if cmd_raw.is_none() || cmd_raw.is_some_and(|v| is_help_token(v)) {
56        return print_group_usage(group_raw);
57    }
58
59    let cmd_raw = cmd_raw.expect("cmd present");
60    match Group::parse(group_raw) {
61        Some(Group::Utils) => match utils::dispatch(cmd_raw, &args[2..]) {
62            Some(code) => code,
63            None => {
64                eprintln!("Unknown {group_raw} command: {cmd_raw}");
65                let _ = print_group_usage(group_raw);
66                2
67            }
68        },
69        Some(Group::Reset) => match reset::dispatch(cmd_raw, &args[2..]) {
70            Some(code) => code,
71            None => {
72                eprintln!("Unknown {group_raw} command: {cmd_raw}");
73                let _ = print_group_usage(group_raw);
74                2
75            }
76        },
77        Some(Group::Commit) => {
78            let known = [
79                "context",
80                "context-json",
81                "context_json",
82                "contextjson",
83                "json",
84                "to-stash",
85                "stash",
86            ];
87            if !known.contains(&cmd_raw.as_str()) {
88                eprintln!("Unknown {group_raw} command: {cmd_raw}");
89                let _ = print_group_usage(group_raw);
90                return 2;
91            }
92            commit::dispatch(cmd_raw, &args[2..])
93        }
94        Some(Group::Branch) => match branch::dispatch(cmd_raw, &args[2..]) {
95            Some(code) => code,
96            None => {
97                eprintln!("Unknown {group_raw} command: {cmd_raw}");
98                let _ = print_group_usage(group_raw);
99                2
100            }
101        },
102        Some(Group::Ci) => match ci::dispatch(cmd_raw, &args[2..]) {
103            Some(code) => code,
104            None => {
105                eprintln!("Unknown {group_raw} command: {cmd_raw}");
106                let _ = print_group_usage(group_raw);
107                2
108            }
109        },
110        Some(Group::Open) => match open::dispatch(cmd_raw, &args[2..]) {
111            Some(code) => code,
112            None => {
113                eprintln!("Unknown {group_raw} command: {cmd_raw}");
114                let _ = print_group_usage(group_raw);
115                2
116            }
117        },
118        Some(Group::Completion) => completion::dispatch(cmd_raw, &args[2..]),
119        None => {
120            eprintln!("Unknown group: {group_raw}");
121            print_top_level_usage(&mut io::stdout());
122            2
123        }
124    }
125}
126
127fn is_help_token(raw: &str) -> bool {
128    matches!(raw, "-h" | "--help" | "help")
129}
130
131fn is_version_token(raw: &str) -> bool {
132    matches!(raw, "-V" | "--version")
133}
134
135fn print_version(out: &mut dyn Write) {
136    writeln!(out, "git-cli {}", env!("CARGO_PKG_VERSION")).ok();
137}
138
139fn print_group_usage(group_raw: &str) -> i32 {
140    let mut out = io::stdout();
141
142    match Group::parse(group_raw) {
143        Some(Group::Utils) => {
144            writeln!(out, "Usage: git-cli utils <command> [args]").ok();
145            writeln!(out, "  zip | copy-staged | root | commit-hash").ok();
146            0
147        }
148        Some(Group::Reset) => {
149            writeln!(out, "Usage: git-cli reset <command> [args]").ok();
150            writeln!(
151                out,
152                "  soft | mixed | hard | undo | back-head | back-checkout | remote"
153            )
154            .ok();
155            0
156        }
157        Some(Group::Commit) => {
158            writeln!(out, "Usage: git-cli commit <command> [args]").ok();
159            writeln!(out, "  context | context-json | to-stash").ok();
160            0
161        }
162        Some(Group::Branch) => {
163            writeln!(out, "Usage: git-cli branch <command> [args]").ok();
164            writeln!(out, "  cleanup").ok();
165            0
166        }
167        Some(Group::Ci) => {
168            writeln!(out, "Usage: git-cli ci <command> [args]").ok();
169            writeln!(out, "  pick").ok();
170            0
171        }
172        Some(Group::Open) => {
173            writeln!(out, "Usage: git-cli open <command> [args]").ok();
174            writeln!(
175                out,
176                "  repo | branch | default-branch | commit | compare | pr | pulls | issues | actions | releases | tags | commits | file | blame"
177            )
178            .ok();
179            0
180        }
181        Some(Group::Completion) => {
182            writeln!(out, "Usage: git-cli completion <shell>").ok();
183            writeln!(out, "  bash | zsh").ok();
184            0
185        }
186        None => {
187            eprintln!("Unknown group: {group_raw}");
188            print_top_level_usage(&mut out);
189            2
190        }
191    }
192}
193
194fn print_top_level_usage(out: &mut dyn Write) {
195    writeln!(out, "Usage:").ok();
196    writeln!(out, "  git-cli <group> <command> [args]").ok();
197    writeln!(out).ok();
198    writeln!(out, "Groups:").ok();
199    writeln!(out, "  utils    zip | copy-staged | root | commit-hash").ok();
200    writeln!(
201        out,
202        "  reset    soft | mixed | hard | undo | back-head | back-checkout | remote"
203    )
204    .ok();
205    writeln!(out, "  commit   context | context-json | to-stash").ok();
206    writeln!(out, "  branch   cleanup").ok();
207    writeln!(out, "  ci       pick").ok();
208    writeln!(
209        out,
210        "  open     repo | branch | default-branch | commit | compare | pr | pulls | issues | actions | releases | tags | commits | file | blame"
211    )
212    .ok();
213    writeln!(out, "  completion  bash | zsh").ok();
214    writeln!(out).ok();
215    writeln!(out, "Help:").ok();
216    writeln!(out, "  git-cli help").ok();
217    writeln!(out, "  git-cli <group> help").ok();
218    writeln!(out).ok();
219    writeln!(out, "Examples:").ok();
220    writeln!(out, "  git-cli utils zip").ok();
221    writeln!(out, "  git-cli reset hard 3").ok();
222}
223
224#[cfg(test)]
225mod tests {
226    use super::{
227        Group, dispatch, is_help_token, is_version_token, print_group_usage, print_top_level_usage,
228    };
229    use std::ffi::OsString;
230
231    fn to_args(args: &[&str]) -> Vec<OsString> {
232        args.iter().map(OsString::from).collect()
233    }
234
235    #[test]
236    fn group_parse_recognizes_known_groups() {
237        assert!(matches!(Group::parse("utils"), Some(Group::Utils)));
238        assert!(matches!(Group::parse("reset"), Some(Group::Reset)));
239        assert!(matches!(Group::parse("commit"), Some(Group::Commit)));
240        assert!(matches!(Group::parse("branch"), Some(Group::Branch)));
241        assert!(matches!(Group::parse("ci"), Some(Group::Ci)));
242        assert!(matches!(Group::parse("open"), Some(Group::Open)));
243        assert!(matches!(
244            Group::parse("completion"),
245            Some(Group::Completion)
246        ));
247        assert!(Group::parse("unknown").is_none());
248    }
249
250    #[test]
251    fn help_token_detection_matches_cli_aliases() {
252        assert!(is_help_token("-h"));
253        assert!(is_help_token("--help"));
254        assert!(is_help_token("help"));
255        assert!(!is_help_token("HELP"));
256    }
257
258    #[test]
259    fn version_token_detection_matches_cli_aliases() {
260        assert!(is_version_token("-V"));
261        assert!(is_version_token("--version"));
262        assert!(!is_version_token("-v"));
263    }
264
265    #[test]
266    fn dispatch_returns_two_for_unknown_group_or_command() {
267        assert_eq!(dispatch(to_args(&["unknown", "cmd"])), 2);
268        assert_eq!(dispatch(to_args(&["reset", "unknown"])), 2);
269        assert_eq!(dispatch(to_args(&["branch", "unknown"])), 2);
270        assert_eq!(dispatch(to_args(&["ci", "unknown"])), 2);
271        assert_eq!(dispatch(to_args(&["open", "unknown"])), 2);
272        assert_eq!(dispatch(to_args(&["completion", "fish"])), 1);
273    }
274
275    #[test]
276    fn commit_group_unknown_command_is_rejected_before_runtime() {
277        assert_eq!(dispatch(to_args(&["commit", "unknown"])), 2);
278    }
279
280    #[test]
281    fn dispatch_version_flag_returns_zero() {
282        assert_eq!(dispatch(to_args(&["-V"])), 0);
283        assert_eq!(dispatch(to_args(&["--version"])), 0);
284    }
285
286    #[test]
287    fn print_group_usage_supports_each_group_and_unknown() {
288        assert_eq!(print_group_usage("utils"), 0);
289        assert_eq!(print_group_usage("reset"), 0);
290        assert_eq!(print_group_usage("commit"), 0);
291        assert_eq!(print_group_usage("branch"), 0);
292        assert_eq!(print_group_usage("ci"), 0);
293        assert_eq!(print_group_usage("open"), 0);
294        assert_eq!(print_group_usage("completion"), 0);
295        assert_eq!(print_group_usage("unknown"), 2);
296    }
297
298    #[test]
299    fn print_top_level_usage_includes_required_sections() {
300        let mut out = Vec::<u8>::new();
301        print_top_level_usage(&mut out);
302        let text = String::from_utf8(out).expect("utf8");
303
304        assert!(text.contains("Usage:"));
305        assert!(text.contains("Groups:"));
306        assert!(text.contains("Examples:"));
307        assert!(text.contains("git-cli reset hard 3"));
308        assert!(text.contains("completion  bash | zsh"));
309    }
310}