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}