guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use anyhow::Result;
use serde_json;
use syntect::{
    easy::HighlightLines,
    highlighting::Style,
    util::{LinesWithEndings, as_24_bit_terminal_escaped},
};
use two_face::{syntax, theme};

use crate::{cli::banner, cli::output::*, config::CONFIG};

/// Apply syntax highlighting to output based on format
fn apply_syntax_highlighting(output: &str, format: &str) -> Result<String> {
    // Check if we're in a terminal that supports colors
    if !is_terminal::IsTerminal::is_terminal(&std::io::stdout()) {
        return Ok(output.to_string());
    }

    let ps = syntax::extra_newlines();
    let ts = theme::extra();

    let syntax = match format {
        "json" => ps
            .find_syntax_by_extension("json")
            .unwrap_or_else(|| ps.find_syntax_plain_text()),
        "yaml" => ps
            .find_syntax_by_extension("yaml")
            .unwrap_or_else(|| ps.find_syntax_plain_text()),
        "toml" => ps
            .find_syntax_by_extension("toml")
            .unwrap_or_else(|| ps.find_syntax_plain_text()),
        _ => return Ok(output.to_string()),
    };

    use two_face::theme::EmbeddedThemeName;
    let theme = ts.get(EmbeddedThemeName::Base16OceanDark);
    let mut h = HighlightLines::new(syntax, theme);
    let mut highlighted = String::new();

    for line in LinesWithEndings::from(output) {
        let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps)?;
        let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
        highlighted.push_str(&escaped);
    }

    // Reset terminal color at the end
    highlighted.push_str("\x1b[0m");

    Ok(highlighted)
}

/// Dump the current hooks configuration in various formats
pub async fn dump_hooks_config(format: Option<String>) -> Result<()> {
    // Print banner without context
    banner::print_banner(None);

    let hooks_config = &CONFIG.hooks;
    let output_format = format.as_deref().unwrap_or("yaml");

    let output = match output_format {
        "json" => {
            let json_output = serde_json::to_string_pretty(hooks_config)
                .map_err(|e| anyhow::anyhow!("Failed to serialize to JSON: {}", e))?;
            apply_syntax_highlighting(&json_output, "json")?
        }
        "yaml" => {
            let yaml_output = serde_yaml_bw::to_string(hooks_config)
                .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))?;
            apply_syntax_highlighting(&yaml_output, "yaml")?
        }
        "toml" => {
            let toml_output = toml::to_string_pretty(hooks_config)
                .map_err(|e| anyhow::anyhow!("Failed to serialize to TOML: {}", e))?;
            apply_syntax_highlighting(&toml_output, "toml")?
        }
        _ => {
            styled!(
                "{} Unsupported format: {}",
                ("", "error_symbol"),
                (output_format, "error")
            );
            styled!("Supported formats: json, yaml, toml");
            return Err(anyhow::anyhow!(
                "Unsupported output format: {}",
                output_format
            ));
        }
    };

    println!("{output}");
    Ok(())
}

/// Dump hooks configuration in lefthook-compatible format
pub async fn dump_lefthook_config() -> Result<()> {
    // Print banner without context
    banner::print_banner(None);

    use std::collections::HashMap;

    let hooks_config = &CONFIG.hooks;

    // Build lefthook-compatible structure
    let mut lefthook_config: HashMap<String, serde_json::Value> = HashMap::new();

    // Add pre-commit hook if configured
    if !hooks_config.pre_commit.commands.is_empty() || !hooks_config.pre_commit.scripts.is_empty() {
        let mut hook_config = HashMap::new();

        if hooks_config.pre_commit.skip {
            hook_config.insert("skip".to_string(), serde_json::Value::Bool(true));
        }

        if !hooks_config.pre_commit.parallel {
            hook_config.insert("parallel".to_string(), serde_json::Value::Bool(false));
        }

        // Add commands
        if !hooks_config.pre_commit.commands.is_empty() {
            let commands: HashMap<String, serde_json::Value> = hooks_config
                .pre_commit
                .commands
                .iter()
                .map(|(name, cmd)| {
                    let mut cmd_obj = HashMap::new();
                    cmd_obj.insert(
                        "run".to_string(),
                        serde_json::Value::String(cmd.run.clone()),
                    );

                    if !cmd.description.is_empty() {
                        cmd_obj.insert(
                            "description".to_string(),
                            serde_json::Value::String(cmd.description.clone()),
                        );
                    }

                    if cmd.continue_on_error {
                        cmd_obj.insert(
                            "fail_text".to_string(),
                            serde_json::Value::String("continue".to_string()),
                        );
                    }

                    if cmd.all_files {
                        cmd_obj.insert(
                            "files".to_string(),
                            serde_json::Value::String("git ls-files".to_string()),
                        );
                    }

                    if cmd.stage_fixed {
                        cmd_obj.insert("stage_fixed".to_string(), serde_json::Value::Bool(true));
                    }

                    if !cmd.glob.is_empty() {
                        let glob_value = if cmd.glob.len() == 1 {
                            serde_json::Value::String(cmd.glob[0].clone())
                        } else {
                            serde_json::Value::Array(
                                cmd.glob
                                    .iter()
                                    .map(|g| serde_json::Value::String(g.clone()))
                                    .collect(),
                            )
                        };
                        cmd_obj.insert("glob".to_string(), glob_value);
                    }

                    (
                        name.clone(),
                        serde_json::Value::Object(cmd_obj.into_iter().collect()),
                    )
                })
                .collect();

            hook_config.insert(
                "commands".to_string(),
                serde_json::Value::Object(commands.into_iter().collect()),
            );
        }

        // Add scripts
        if !hooks_config.pre_commit.scripts.is_empty() {
            let scripts: HashMap<String, serde_json::Value> = hooks_config
                .pre_commit
                .scripts
                .iter()
                .map(|(name, script)| {
                    let mut script_obj = HashMap::new();
                    script_obj.insert(
                        "runner".to_string(),
                        serde_json::Value::String(script.runner.clone()),
                    );
                    if !script.env.is_empty() {
                        let env_obj: HashMap<String, serde_json::Value> = script
                            .env
                            .iter()
                            .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
                            .collect();
                        script_obj.insert(
                            "env".to_string(),
                            serde_json::Value::Object(env_obj.into_iter().collect()),
                        );
                    }
                    (
                        name.clone(),
                        serde_json::Value::Object(script_obj.into_iter().collect()),
                    )
                })
                .collect();
            hook_config.insert(
                "scripts".to_string(),
                serde_json::Value::Object(scripts.into_iter().collect()),
            );
        }

        lefthook_config.insert(
            "pre-commit".to_string(),
            serde_json::Value::Object(hook_config.into_iter().collect()),
        );
    }

    // Add commit-msg hook if configured
    if !hooks_config.commit_msg.commands.is_empty() || !hooks_config.commit_msg.scripts.is_empty() {
        let mut hook_config = HashMap::new();

        if hooks_config.commit_msg.skip {
            hook_config.insert("skip".to_string(), serde_json::Value::Bool(true));
        }

        // Add commands
        if !hooks_config.commit_msg.commands.is_empty() {
            let commands: HashMap<String, serde_json::Value> = hooks_config
                .commit_msg
                .commands
                .iter()
                .map(|(name, cmd)| {
                    let mut cmd_obj = HashMap::new();
                    cmd_obj.insert(
                        "run".to_string(),
                        serde_json::Value::String(cmd.run.clone()),
                    );

                    if !cmd.description.is_empty() {
                        cmd_obj.insert(
                            "description".to_string(),
                            serde_json::Value::String(cmd.description.clone()),
                        );
                    }

                    (
                        name.clone(),
                        serde_json::Value::Object(cmd_obj.into_iter().collect()),
                    )
                })
                .collect();

            hook_config.insert(
                "commands".to_string(),
                serde_json::Value::Object(commands.into_iter().collect()),
            );
        }

        // Add scripts
        if !hooks_config.commit_msg.scripts.is_empty() {
            let scripts: HashMap<String, serde_json::Value> = hooks_config
                .commit_msg
                .scripts
                .iter()
                .map(|(name, script)| {
                    let mut script_obj = HashMap::new();
                    script_obj.insert(
                        "runner".to_string(),
                        serde_json::Value::String(script.runner.clone()),
                    );
                    if !script.env.is_empty() {
                        let env_obj: HashMap<String, serde_json::Value> = script
                            .env
                            .iter()
                            .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
                            .collect();
                        script_obj.insert(
                            "env".to_string(),
                            serde_json::Value::Object(env_obj.into_iter().collect()),
                        );
                    }
                    (
                        name.clone(),
                        serde_json::Value::Object(script_obj.into_iter().collect()),
                    )
                })
                .collect();
            hook_config.insert(
                "scripts".to_string(),
                serde_json::Value::Object(scripts.into_iter().collect()),
            );
        }

        lefthook_config.insert(
            "commit-msg".to_string(),
            serde_json::Value::Object(hook_config.into_iter().collect()),
        );
    }

    // Output as YAML for lefthook compatibility
    let yaml_output = serde_yaml_bw::to_string(&lefthook_config)
        .map_err(|e| anyhow::anyhow!("Failed to serialize lefthook config to YAML: {}", e))?;

    // Apply syntax highlighting for YAML
    let highlighted_output = apply_syntax_highlighting(&yaml_output, "yaml")?;
    println!("{highlighted_output}");

    Ok(())
}