git-smash 0.1.0

Smash staged changes into previous commits
use crate::errors::*;

use crate::config::{CommitRange, Config, FixupMode};
use regex::Regex;
use semver::{Version, VersionReq};
use std::path::PathBuf;
use std::process::{Command, Stdio};

pub struct GitConfigBuilder {
    key: &'static str,
    default: Option<&'static str>,
    value_type: Option<&'static str>,
}

impl GitConfigBuilder {
    pub const fn new(key: &'static str) -> Self {
        Self {
            key,
            default: None,
            value_type: None,
        }
    }

    pub const fn with_default(mut self, default: &'static str) -> Self {
        self.default = Some(default);
        self
    }

    pub const fn with_type(mut self, value_type: &'static str) -> Self {
        self.value_type = Some(value_type);
        self
    }

    pub fn get(&self) -> Result<Option<String>> {
        let mut args = vec!["config", "--get"];
        if let Some(default) = self.default {
            args.push("--default");
            args.push(default);
        }
        if let Some(value_type) = self.value_type {
            args.push("--type");
            args.push(value_type);
        }
        args.push(self.key);

        let output = Command::new("git")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .args(&args)
            .output()?;
        if !output.status.success() {
            match output.status.code() {
                Some(1) => {
                    return Ok(None);
                }
                _ => bail!("{}", String::from_utf8_lossy(&output.stderr).trim()),
            }
        }
        Ok(Some(
            String::from_utf8_lossy(&output.stdout)
                .trim_end()
                .to_owned(),
        ))
    }

    pub fn get_as_bool(&self) -> Result<Option<bool>> {
        let value = self.get()?;
        match value {
            None => Ok(None),
            Some(value) => {
                Ok(Some(value.parse::<bool>().with_context(|| {
                    anyhow!("Failed to parse key '{}' as bool", self.key)
                })?))
            }
        }
    }

    pub fn get_as_int(&self) -> Result<Option<u32>> {
        let value = self.get()?;
        match value {
            None => Ok(None),
            Some(value) => {
                Ok(Some(value.parse::<u32>().with_context(|| {
                    anyhow!("Failed to parse key '{}' as u32", self.key)
                })?))
            }
        }
    }
}

pub fn git_rebase(rev: &str, interactive: bool) -> Result<()> {
    let root = git_rev_root().context("failed to get git rev root")?;
    let rev = match root.starts_with(rev) {
        true => "--root".to_string(),
        false => format!("{}^", rev),
    };

    let args = vec![
        "rebase",
        "--interactive",
        "--autosquash",
        "--autostash",
        &rev,
    ];
    let mut cmd = Command::new("git");
    if !interactive {
        cmd.env("GIT_EDITOR", "true");
        cmd.env("GIT_SEQUENCE_EDITOR", "true");
    }
    let cmd = cmd.args(&args).spawn()?;
    let output = cmd.wait_with_output()?;

    if !output.status.success() {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
    }

    Ok(())
}

pub fn git_rev_root() -> Result<String> {
    let args = vec!["rev-list", "--max-parents=0", "--no-abbrev-commit", "HEAD"];
    let output = Command::new("git")
        .stdout(Stdio::piped())
        .args(&args)
        .output()?;
    if !output.status.success() {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
    }
    Ok(String::from_utf8_lossy(&output.stdout)
        .into_owned()
        .trim_end()
        .to_owned())
}

pub fn git_rev_range(config: &Config) -> Result<Option<String>> {
    let head = "HEAD".to_string();

    match &config.range {
        CommitRange::All => Ok(Some(head)),
        CommitRange::Local => {
            let upstream = git_rev_parse("@{upstream}");
            if let Ok(upstream) = upstream {
                let head = git_rev_parse("HEAD").context("failed to rev parse HEAD")?;
                if upstream == head {
                    return Ok(None);
                }
                return Ok(Some("@{upstream}..HEAD".to_string()));
            }
            Ok(Some(head))
        }
        CommitRange::Range(range) => Ok(Some(range.into())),
    }
}

pub fn git_rev_parse(rev: &str) -> Result<String> {
    let args = vec!["rev-parse", rev];
    let output = Command::new("git")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .args(&args)
        .output()?;
    if !output.status.success() {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
    }
    Ok(String::from_utf8_lossy(&output.stdout)
        .into_owned()
        .trim_end()
        .to_owned())
}

pub fn git_rev_list(rev: &str, max_count: u32) -> Result<Vec<String>> {
    let max_count = format!("{}", max_count);
    let args = vec!["rev-list", "-n", &max_count, "--no-abbrev-commit", rev];
    let output = Command::new("git")
        .stdout(Stdio::piped())
        .args(&args)
        .output()?;
    if !output.status.success() {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
    }
    Ok(String::from_utf8_lossy(&output.stdout)
        .into_owned()
        .trim_end()
        .to_owned()
        .lines()
        .map(|e| e.to_owned())
        .collect())
}

pub fn git_toplevel() -> Result<PathBuf> {
    git_rev_parse("--show-toplevel").map(PathBuf::from)
}

pub fn is_valid_git_rev(rev: &str) -> Result<bool> {
    let files_args = vec!["rev-parse", "--verify", rev];
    let mut cmd = Command::new("git")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .args(&files_args)
        .spawn()?;
    Ok(cmd.wait()?.success())
}

pub fn git_commit_fixup(target: &str, mode: FixupMode) -> Result<()> {
    let fixup = mode.to_cli_option(target);
    let mut args = vec!["commit", "--verbose", &fixup];
    if let FixupMode::Fixup = mode {
        args.push("--no-edit")
    };
    let output = Command::new("git")
        .args(&args)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .output()?;
    if !output.status.success() {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
    }
    Ok(())
}

pub fn git_staged_files() -> Result<Vec<String>> {
    let files_args = vec![
        "--no-pager",
        "diff",
        "--color=never",
        "--name-only",
        "--cached",
    ];
    let output = Command::new("git")
        .stdout(Stdio::piped())
        .args(&files_args)
        .output()?;
    if !output.status.success() {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
    }
    Ok(String::from_utf8_lossy(&output.stdout)
        .trim()
        .lines()
        .map(|e| e.to_owned())
        .collect())
}

pub fn git_version() -> Result<Version> {
    let args = vec!["version"];
    let output = Command::new("git")
        .stdout(Stdio::piped())
        .args(&args)
        .output()?;
    if !output.status.success() {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
    }
    let git_version = String::from_utf8_lossy(&output.stdout).trim().to_owned();
    let version_regex = Regex::new(r"[^ ]+ [^ ]+ (?P<version>[^ ]+)")
        .context("failed to create git version regex")?;
    let captures = version_regex
        .captures(&git_version)
        .with_context(|| format!("Failed to match git version from '{}'", git_version))?;
    let version = captures
        .name("version")
        .context("failed to get version capture group")?
        .as_str();
    let version = Version::parse(version)
        .with_context(|| format!("failed to parse version from '{}'", version))?;

    Ok(version)
}

pub fn git_check_version(git_version: &Version, check: &str, feature: &str) -> Result<()> {
    if !VersionReq::parse(check)
        .with_context(|| format!("failed to parse version {}", check))?
        .matches(git_version)
    {
        bail!(
            "git version {} does not match {} required for {}",
            git_version,
            check,
            feature
        )
    }
    Ok(())
}