1use std::collections::HashMap;
2use std::ffi::OsStr;
3use std::ffi::OsString;
4use std::path::PathBuf;
5
6use clap::{Args, Parser, Subcommand, ValueEnum};
7
8#[derive(Debug, Parser)] #[command(name = "git")]
11#[command(about = "A fictional versioning CLI", long_about = None)]
12struct Cli {
13 #[command(subcommand)]
14 command: Commands,
15}
16
17#[derive(Debug, Subcommand)]
18enum Commands {
19 #[command(arg_required_else_help = true)]
21 Clone {
22 remote: String,
24 },
25 Diff {
27 #[arg(value_name = "COMMIT")]
28 base: Option<OsString>,
29 #[arg(value_name = "COMMIT")]
30 head: Option<OsString>,
31 #[arg(last = true)]
32 path: Option<OsString>,
33 #[arg(
34 long,
35 require_equals = true,
36 value_name = "WHEN",
37 num_args = 0..=1,
38 default_value_t = ColorWhen::Auto,
39 default_missing_value = "always",
40 value_enum
41 )]
42 color: ColorWhen,
43 },
44 #[command(arg_required_else_help = true)]
46 Push {
47 remote: String,
49 },
50 #[command(arg_required_else_help = true)]
52 Add {
53 #[arg(required = true)]
55 path: Vec<PathBuf>,
56 },
57 Stash(StashArgs),
58 #[command(external_subcommand)]
59 External(Vec<OsString>),
60}
61
62#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
63enum ColorWhen {
64 Always,
65 Auto,
66 Never,
67}
68
69impl std::fmt::Display for ColorWhen {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 self.to_possible_value()
72 .expect("no values are skipped")
73 .get_name()
74 .fmt(f)
75 }
76}
77
78#[derive(Debug, Args)]
79#[command(args_conflicts_with_subcommands = true)]
80#[command(flatten_help = true)]
81struct StashArgs {
82 #[command(subcommand)]
83 command: Option<StashCommands>,
84
85 #[command(flatten)]
86 push: StashPushArgs,
87}
88
89#[derive(Debug, Subcommand)]
90enum StashCommands {
91 Push(StashPushArgs),
92 Pop { stash: Option<String> },
93 Apply { stash: Option<String> },
94}
95
96#[derive(Debug, Args)]
97struct StashPushArgs {
98 #[arg(short, long)]
99 message: Option<String>,
100}
101
102fn aliases() -> HashMap<&'static str, Vec<&'static str>> {
103 HashMap::from([
104 ("last", vec!["diff", "HEAD~", "HEAD", "--"]),
105 ("stage", vec!["add"]),
106 ])
107}
108
109fn parse_aliases() -> Result<Cli, clap::Error> {
110 let args = Cli::try_parse()?;
111 expand_aliases(args, Vec::new())
112}
113
114fn expand_aliases(args: Cli, mut expanded: Vec<String>) -> Result<Cli, clap::Error> {
115 let Commands::External(external_args) = &args.command else {
116 return Ok(args);
117 };
118 let Some(name) = external_args.first().and_then(|name| name.to_str()) else {
119 return Ok(args);
120 };
121
122 let aliases = aliases();
123 let Some(alias) = aliases.get(name) else {
124 return Ok(args);
125 };
126 if expanded.iter().any(|expanded| expanded == name) {
127 return Err(clap::Error::raw(
128 clap::error::ErrorKind::InvalidSubcommand,
129 format!("recursive alias `{}`", expanded[0]),
130 ));
131 }
132 expanded.push(name.to_owned());
133
134 let mut alias_args = vec![OsString::from("git")];
135 alias_args.extend(alias.iter().map(OsString::from));
136 alias_args.extend(external_args.iter().skip(1).cloned());
137 let args = Cli::try_parse_from(alias_args)?;
138
139 expand_aliases(args, expanded)
140}
141
142fn main() {
143 let args = parse_aliases().unwrap_or_else(|error| error.exit());
144
145 match args.command {
146 Commands::Clone { remote } => {
147 println!("Cloning {remote}");
148 }
149 Commands::Diff {
150 mut base,
151 mut head,
152 mut path,
153 color,
154 } => {
155 if path.is_none() {
156 path = head;
157 head = None;
158 if path.is_none() {
159 path = base;
160 base = None;
161 }
162 }
163 let base = base
164 .as_deref()
165 .map(|s| s.to_str().unwrap())
166 .unwrap_or("stage");
167 let head = head
168 .as_deref()
169 .map(|s| s.to_str().unwrap())
170 .unwrap_or("worktree");
171 let path = path.as_deref().unwrap_or_else(|| OsStr::new(""));
172 println!(
173 "Diffing {}..{} {} (color={})",
174 base,
175 head,
176 path.to_string_lossy(),
177 color
178 );
179 }
180 Commands::Push { remote } => {
181 println!("Pushing to {remote}");
182 }
183 Commands::Add { path } => {
184 println!("Adding {path:?}");
185 }
186 Commands::Stash(stash) => {
187 let stash_cmd = stash.command.unwrap_or(StashCommands::Push(stash.push));
188 match stash_cmd {
189 StashCommands::Push(push) => {
190 println!("Pushing {push:?}");
191 }
192 StashCommands::Pop { stash } => {
193 println!("Popping {stash:?}");
194 }
195 StashCommands::Apply { stash } => {
196 println!("Applying {stash:?}");
197 }
198 }
199 }
200 Commands::External(args) => {
201 println!("Calling out to {:?} with {:?}", args[0], &args[1..]);
202 }
203 }
204
205 }