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}