dotlock-bin 0.1.4

Encrypted project-local environment variables manager
use std::{
    fs::{self, OpenOptions},
    io::Write,
    path::Path,
    process::Command,
};

use crate::domain::{error::DotLockError, model::DotLockResult};

const ATTRIBUTES: &[&str] = &[
    ".lock/secrets.lock merge=dotlock",
    ".lock/vault.toml merge=dotlock",
];

pub fn install_merge_driver_if_in_git_repo() -> DotLockResult<bool> {
    if !is_git_repo()? {
        return Ok(false);
    }

    install_gitattributes()?;
    configure_merge_driver()?;
    Ok(true)
}

fn is_git_repo() -> DotLockResult<bool> {
    let output = Command::new("git")
        .args(["rev-parse", "--is-inside-work-tree"])
        .output()
        .map_err(|err| DotLockError::Io(err.to_string()))?;

    Ok(output.status.success() && String::from_utf8_lossy(&output.stdout).trim() == "true")
}

fn install_gitattributes() -> DotLockResult<()> {
    let path = Path::new(".gitattributes");
    let existing = if path.exists() {
        fs::read_to_string(path).map_err(DotLockError::from)?
    } else {
        String::new()
    };

    let missing: Vec<&str> = ATTRIBUTES
        .iter()
        .copied()
        .filter(|line| !existing.lines().any(|existing| existing.trim() == *line))
        .collect();

    if missing.is_empty() {
        return Ok(());
    }

    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .map_err(DotLockError::from)?;

    if !existing.is_empty() && !existing.ends_with('\n') {
        writeln!(file).map_err(DotLockError::from)?;
    }

    for line in missing {
        writeln!(file, "{line}").map_err(DotLockError::from)?;
    }

    Ok(())
}

fn configure_merge_driver() -> DotLockResult<()> {
    run_git_config(["merge.dotlock.driver", "dl _git-merge %A %B %O"])?;
    run_git_config(["merge.dotlock.name", "DotLock vault merge driver"])
}

fn run_git_config(args: [&str; 2]) -> DotLockResult<()> {
    let status = Command::new("git")
        .args(["config", args[0], args[1]])
        .status()
        .map_err(|err| DotLockError::Io(err.to_string()))?;

    if !status.success() {
        return Err(DotLockError::Io(format!(
            "git config failed for {}",
            args[0]
        )));
    }

    Ok(())
}