dotlock-bin 0.1.5

Encrypted project-local environment variables manager
use std::{
    io,
    process::{Command, Stdio},
    thread,
    time::{Duration, Instant},
};

use colored::Colorize;

use crate::{
    crypto::VaultConfig,
    domain::{error::DotLockError, model::DotLockResult},
    storage::vault_file::load_vault_metadata,
};

const DEFAULT_TIMEOUT_SECS: u64 = 3;
const DEFAULT_REMOTE: &str = "origin";

pub fn auto_fetch_if_enabled(vault_path: &str) -> DotLockResult<()> {
    let metadata = load_vault_metadata(vault_path)?;
    let env_override = std::env::var("DOTLOCK_AUTO_FETCH").ok();
    if !auto_fetch_enabled(&metadata.config, env_override.as_deref()) {
        return Ok(());
    }

    if !git_output(["rev-parse", "--is-inside-work-tree"])?
        .is_some_and(|output| output.trim() == "true")
    {
        return Ok(());
    }

    let Some(branch) = git_output(["branch", "--show-current"])? else {
        return Ok(());
    };
    let branch = branch.trim();
    if branch.is_empty() {
        return Ok(());
    }

    let remote = metadata
        .config
        .auto_fetch_remote
        .as_deref()
        .unwrap_or(DEFAULT_REMOTE);
    let timeout = Duration::from_secs(timeout_secs(&metadata.config));

    let pulled = run_git_with_timeout(
        &["pull", "--ff-only", "--no-rebase", remote, branch],
        timeout,
    )?;
    if pulled {
        return Ok(());
    }

    let _ = run_git_with_timeout(&["fetch", remote, branch], timeout);
    eprintln!(
        "{} auto-fetch failed; using local vault",
        "info:".cyan().bold()
    );
    Ok(())
}

pub fn auto_fetch_enabled(config: &VaultConfig, env_override: Option<&str>) -> bool {
    if env_override.is_some_and(is_disable_value) {
        return false;
    }
    config.auto_fetch_on_run
}

pub fn timeout_secs(config: &VaultConfig) -> u64 {
    config
        .auto_fetch_timeout_secs
        .unwrap_or(DEFAULT_TIMEOUT_SECS)
}

fn is_disable_value(value: &str) -> bool {
    matches!(
        value.to_ascii_lowercase().as_str(),
        "0" | "false" | "no" | "off"
    )
}

fn git_output<const N: usize>(args: [&str; N]) -> DotLockResult<Option<String>> {
    let output = match Command::new("git")
        .args(args)
        .stderr(Stdio::null())
        .output()
    {
        Ok(output) => output,
        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
        Err(err) => return Err(DotLockError::Io(err.to_string())),
    };
    if !output.status.success() {
        return Ok(None);
    }
    Ok(Some(String::from_utf8_lossy(&output.stdout).to_string()))
}

fn run_git_with_timeout(args: &[&str], timeout: Duration) -> DotLockResult<bool> {
    let mut child = match Command::new("git")
        .args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        Ok(child) => child,
        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(false),
        Err(err) => return Err(DotLockError::Io(err.to_string())),
    };

    let started = Instant::now();
    loop {
        if let Some(status) = child.try_wait().map_err(DotLockError::from)? {
            return Ok(status.success());
        }
        if started.elapsed() >= timeout {
            let _ = child.kill();
            let _ = child.wait();
            return Ok(false);
        }
        thread::sleep(Duration::from_millis(25));
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        crypto::VaultConfig,
        git::fetch::{auto_fetch_enabled, timeout_secs},
    };

    #[test]
    fn auto_fetch_is_enabled_only_by_config_and_can_be_disabled_by_env() {
        let config = VaultConfig {
            auto_fetch_on_run: true,
            auto_fetch_timeout_secs: None,
            auto_fetch_remote: None,
            auto_ratchet_after_writes: None,
            dynamic_resolve_timeout_secs: None,
        };

        assert!(auto_fetch_enabled(&config, None));
        assert!(!auto_fetch_enabled(&config, Some("0")));
        assert!(!auto_fetch_enabled(&config, Some("false")));
        assert!(!auto_fetch_enabled(&VaultConfig::default(), None));
    }

    #[test]
    fn timeout_defaults_to_three_seconds() {
        assert_eq!(timeout_secs(&VaultConfig::default()), 3);
        assert_eq!(
            timeout_secs(&VaultConfig {
                auto_fetch_timeout_secs: Some(1),
                ..VaultConfig::default()
            }),
            1
        );
    }
}