Skip to main content

git_derive/
git-derive.rs

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/// A fictional versioning CLI
9#[derive(Debug, Parser)] // requires `derive` feature
10#[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    /// Clones repos
20    #[command(arg_required_else_help = true)]
21    Clone {
22        /// The remote to clone
23        remote: String,
24    },
25    /// Compare two commits
26    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    /// pushes things
45    #[command(arg_required_else_help = true)]
46    Push {
47        /// The remote to target
48        remote: String,
49    },
50    /// adds things
51    #[command(arg_required_else_help = true)]
52    Add {
53        /// Stuff to add
54        #[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    // Continued program logic goes here...
206}