forkctl 0.0.9

Control audited StGit downstream patch stacks
use crate::error::DomainError;
use anyhow::{Context, Result};
use std::ffi::{OsStr, OsString};
use std::path::Path;
use std::process::{Command, Output};
use std::sync::OnceLock;

static GIT_LOCAL_ENV: OnceLock<Vec<OsString>> = OnceLock::new();

pub fn command(dir: &Path, program: &str) -> Command {
    let mut command = Command::new(program);
    command.current_dir(dir);
    for key in git_local_env_vars() {
        command.env_remove(key);
    }
    command
}

pub fn capture<I, S>(dir: &Path, program: &str, args: I) -> Result<String>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let output = output(dir, program, args)?;
    Ok(String::from_utf8(output.stdout)
        .with_context(|| format!("{program} output is not UTF-8"))?
        .trim()
        .to_string())
}

pub fn output<I, S>(dir: &Path, program: &str, args: I) -> Result<Output>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let args = owned_args(args);
    let output = command(dir, program)
        .args(&args)
        .output()
        .with_context(|| format!("run {program}"))?;
    if output.status.success() {
        Ok(output)
    } else {
        Err(DomainError::subprocess(program, &args, dir, &output).into())
    }
}

pub fn succeeds<I, S>(dir: &Path, program: &str, args: I) -> Result<bool>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    Ok(command(dir, program)
        .args(args)
        .output()
        .with_context(|| format!("run {program}"))?
        .status
        .success())
}

pub fn run<I, S>(dir: &Path, program: &str, args: I) -> Result<()>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    output(dir, program, args).map(|_| ())
}

fn git_local_env_vars() -> &'static [OsString] {
    GIT_LOCAL_ENV.get_or_init(|| {
        Command::new("git")
            .args(["rev-parse", "--local-env-vars"])
            .output()
            .ok()
            .filter(|output| output.status.success())
            .map_or_else(
                || {
                    [
                        "GIT_ALTERNATE_OBJECT_DIRECTORIES",
                        "GIT_CONFIG",
                        "GIT_CONFIG_PARAMETERS",
                        "GIT_CONFIG_COUNT",
                        "GIT_OBJECT_DIRECTORY",
                        "GIT_DIR",
                        "GIT_WORK_TREE",
                        "GIT_IMPLICIT_WORK_TREE",
                        "GIT_GRAFT_FILE",
                        "GIT_INDEX_FILE",
                        "GIT_NO_REPLACE_OBJECTS",
                        "GIT_REPLACE_REF_BASE",
                        "GIT_PREFIX",
                        "GIT_SHALLOW_FILE",
                        "GIT_COMMON_DIR",
                    ]
                    .into_iter()
                    .map(OsString::from)
                    .collect()
                },
                |output| {
                    String::from_utf8_lossy(&output.stdout)
                        .lines()
                        .filter(|line| !line.is_empty())
                        .map(OsString::from)
                        .collect()
                },
            )
    })
}

fn owned_args<I, S>(args: I) -> Vec<OsString>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    args.into_iter()
        .map(|arg| arg.as_ref().to_owned())
        .collect()
}