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