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