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}