use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use crate::git_cmd::GitCommand;
use crate::prelude::*;
use crate::Cache;
use crate::Command2;
type Aliases = HashMap<String, String>;
pub(crate) struct AppBuilder {
git_dir: Option<PathBuf>,
git_aliases: Option<Aliases>,
git_cmd: Option<GitCommand>,
final_command: Command2,
cache: Option<Cache>,
}
#[derive(Debug)]
pub(crate) struct App {
pub git_dir: PathBuf,
pub git_cmd: Option<GitCommand>,
pub final_command: Command2,
pub git_aliases: Aliases,
pub cache: Cache,
}
impl AppBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn build(self) -> App {
assert!(self.final_command.inner.get_current_dir().is_some());
App {
final_command: self.final_command,
git_dir: self.git_dir.unwrap_or_default(),
git_aliases: self.git_aliases.unwrap_or_default(),
git_cmd: self.git_cmd,
cache: self.cache.unwrap_or_default(),
}
}
pub fn current_dir<P>(mut self, v: P) -> Self
where
P: AsRef<Path>,
{
self.final_command.inner.current_dir(v.as_ref());
self
}
}
macro_rules! build {
($field:ident, $type:ty) => {
impl AppBuilder {
pub fn $field(mut self, v: $type) -> Self {
self.$field = Some(v);
self
}
}
};
}
build!(git_aliases, Aliases);
build!(git_dir, PathBuf);
build!(cache, Cache);
impl Default for App {
fn default() -> Self {
Self {
git_dir: PathBuf::new(),
cache: Cache::default(),
git_aliases: Aliases::new(),
git_cmd: None,
final_command: Command2::new("git"),
}
}
}
impl Default for AppBuilder {
fn default() -> Self {
Self {
git_dir: None,
cache: None,
git_aliases: None,
git_cmd: None,
final_command: Command2::new("git"),
}
}
}
pub fn parse_range(arg: &str) -> Option<(usize, usize)> {
if let Ok(single) = arg.parse::<usize>() {
Some((single, single))
} else {
let (a, b) = arg.split_once('-')?;
let a = a.parse::<usize>().ok()?;
let b = b.parse::<usize>().ok()?;
Some((a.min(b), a.max(b)))
}
}
impl App {
pub fn parse<I>(mut self, args: I) -> Self
where
I: IntoIterator<Item = String>,
{
if atty::is(atty::Stream::Stdout) {
self.final_command.hidden_args(["-c", "color.ui=always"]);
}
let mut args = args.into_iter().skip(1);
self.before_cmd(&mut args).after_cmd(&mut args);
self
}
fn before_cmd<I>(&mut self, args: &mut I) -> &mut Self
where
I: Iterator<Item = String>,
{
use GitCommand as GC;
for arg in args {
if let Ok(v) = GC::try_from(&arg) {
self.git_cmd = Some(v);
self.final_command.arg(arg);
break;
}
if let Some(Ok(v)) = self.git_aliases.get(&arg).map(GC::try_from) {
self.git_cmd = Some(v);
self.final_command.arg(arg);
break;
}
self.final_command.arg(arg);
}
self
}
fn after_cmd<I>(&mut self, args: &mut I) -> &mut Self
where
I: Iterator<Item = String>,
{
let mut skip = false;
if let None = self.git_cmd {
self.final_command.inner.args(args);
return self;
}
for arg in args {
let arg = arg.as_str();
let git_cmd = self.git_cmd.as_mut().unwrap();
match git_cmd {
GitCommand::Status(ref mut v) => match arg {
"--short" | "-s" | "--porcelain" => v.short(),
_ => {}
},
_ => {}
};
match (skip, parse_range(&arg)) {
(false, Some((start, end))) => {
let cmd = &mut self.final_command.inner;
for i in start..end + 1 {
self.cache.load(i, cmd)
}
}
_ => self.final_command.arg(&arg),
}
skip = git_cmd.skip_next_arg(&arg);
}
self
}
pub fn get_current_dir(&self) -> &Path {
self.final_command.inner.get_current_dir().unwrap()
}
pub(crate) fn run(mut self) -> Result<ExitCode> {
use GitCommand as G;
let git_cmd = self.git_cmd.clone();
match git_cmd {
Some(G::Version) => {
let result = self.final_command.inner.status();
let exitcode = result.map(|v| v.exitcode())?;
println!("gitnu version {CARGO_PKG_VERSION}");
Ok(exitcode)
}
Some(G::Status(_)) => self.git_status(),
_ => {
let result = self.final_command.inner.status();
let exitcode = result.map(|v| v.exitcode())?;
Ok(exitcode)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test {
($name:ident, $input_args:expr, $output_args:expr) => {
#[test]
fn $name() {
let mut args = vec!["git"];
args.extend($input_args);
let args = args.iter().map(|v| v.to_string());
let app = App::default().parse(args);
let received_args = app.final_command.get_args();
let expected_args: Vec<_> =
$output_args.into_iter().map(String::from).collect();
assert_eq!(received_args, expected_args);
}
};
}
test!(test_single, ["add", "1"], ["add", "1"]);
test!(test_range, ["add", "2-4"], ["add", "2", "3", "4"]);
test!(test_mix, ["add", "8", "2-4"], ["add", "8", "2", "3", "4"]);
test!(
test_overlap,
["add", "3-5", "2-4"],
["add", "3", "4", "5", "2", "3", "4"]
);
test!(
test_double_dash,
["add", "3-5", "--", "2-4"],
["add", "3", "4", "5", "--", "2", "3", "4"]
);
test!(test_zeroes_1, ["add", "0"], ["add", "0"]);
test!(test_zeroes_2, ["add", "0-1"], ["add", "0", "1"]);
test!(test_zeroes_3, ["add", "0-0"], ["add", "0"]);
test!(test_date_filename, ["add", "2021-01-31"], ["add", "2021-01-31"]);
}