git-send 0.1.6

Commit and push changes with a single command
//! Pre-push and post-push hooks

use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;

use crate::output::Output;

/// Checks if a command exists in PATH.
fn command_exists(cmd: &str) -> bool {
    Command::new("which").arg(cmd).output().is_ok()
}

/// Runs a test command and returns success status.
fn run_test_command(cmd: &str, args: &[&str], output: &Output) -> Result<bool> {
    output.progress(&format!("Running {cmd} test... "));
    let status = Command::new(cmd)
        .args(args)
        .status()
        .context(format!("Failed to run {cmd} test"))?;

    if !status.success() {
        output.error("Tests failed! Aborting push.");
        return Ok(false);
    }

    output.success(&format!("{cmd} tests passed"));
    Ok(true)
}

/// Runs common test commands.
fn run_common_tests(output: &Output) -> Result<bool> {
    let test_commands = [
        ("cargo", &["test"] as &[&str]),
        ("npm", &["test"]),
        ("yarn", &["test"]),
        ("make", &["test"]),
        ("pytest", &[]),
    ];

    for (cmd, args) in &test_commands {
        if !command_exists(cmd) {
            continue;
        }

        let success = run_test_command(cmd, args, output)?;
        if success {
            return Ok(true);
        }

        anyhow::bail!("Pre-push hooks failed");
    }

    Ok(false)
}

/// Runs custom pre-push hook script.
fn run_custom_pre_push_hook(output: &Output) -> Result<bool> {
    if !Path::new(".git-send-pre-push").exists() {
        return Ok(false);
    }

    output.progress("Running custom pre-push hook... ");
    let status = Command::new("sh")
        .arg(".git-send-pre-push")
        .status()
        .context("Failed to run custom pre-push hook")?;

    if !status.success() {
        output.error("Pre-push hook failed! Aborting push.");
        anyhow::bail!("Pre-push hooks failed");
    }

    output.success("Pre-push hook passed");
    Ok(true)
}

/// Runs pre-push hooks (tests, validation scripts).
pub fn run_pre_push_hooks(output: &Output) -> Result<()> {
    let ran_common = run_common_tests(output)?;
    if ran_common {
        return Ok(());
    }

    run_custom_pre_push_hook(output)?;
    Ok(())
}

/// Runs post-push hooks (notifications).
pub fn run_post_push_hooks(output: &Output) -> Result<()> {
    if !Path::new(".git-send-post-push").exists() {
        return Ok(());
    }

    output.progress("Running post-push hook... ");
    Command::new("sh")
        .arg(".git-send-post-push")
        .status()
        .context("Failed to run post-push hook")?;
    output.success("Post-push hook completed");
    Ok(())
}