gitkit 0.4.0

Standalone CLI for configuring git repos — hooks, .gitignore, and .gitattributes
use anyhow::Result;
use std::fs;

use crate::utils::{find_repo_root, git_config_get};

pub fn run() -> Result<()> {
    let in_repo = find_repo_root().is_ok();

    if in_repo {
        print_hooks()?;
        println!();
        print_gitignore()?;
        println!();
        print_gitattributes()?;
        println!();
        print_config("local")?;
        println!();
    }

    print_config("global")?;

    Ok(())
}

fn print_hooks() -> Result<()> {
    println!("Hooks:");

    let root = find_repo_root()?;
    let hooks_dir = root.join(".git").join("hooks");

    if !hooks_dir.exists() {
        println!("  (none)");
        return Ok(());
    }

    let installed: Vec<_> = fs::read_dir(&hooks_dir)?
        .filter_map(|e| e.ok())
        .filter(|e| {
            let name = e.file_name();
            let s = name.to_string_lossy();
            !s.ends_with(".bak") && !s.ends_with(".sample")
        })
        .collect();

    if installed.is_empty() {
        println!("  (none)");
        return Ok(());
    }

    for entry in installed {
        let hook_name = entry.file_name().to_string_lossy().to_string();
        let content = fs::read_to_string(entry.path()).unwrap_or_default();

        match crate::hooks::detect_builtin(&hook_name, &content) {
            Some(b) => println!("{} ({})", b.name, b.hook),
            None => {
                let first_cmd = content
                    .lines()
                    .find(|l| !l.starts_with('#') && !l.starts_with("set ") && !l.trim().is_empty())
                    .unwrap_or("(custom)")
                    .trim();
                println!("  ✓ custom: {}{:?}", hook_name, first_cmd);
            }
        }
    }

    Ok(())
}

fn print_gitignore() -> Result<()> {
    println!(".gitignore:");

    let root = find_repo_root()?;
    let path = root.join(".gitignore");

    if !path.exists() {
        println!("  (none)");
        return Ok(());
    }

    let content = fs::read_to_string(&path)?;
    let patterns: Vec<_> = content
        .lines()
        .filter(|l| !l.is_empty() && !l.starts_with('#'))
        .collect();

    println!("{} patterns", patterns.len());
    Ok(())
}

fn print_gitattributes() -> Result<()> {
    println!(".gitattributes:");

    let root = find_repo_root()?;
    let path = root.join(".gitattributes");

    if !path.exists() {
        println!("  (none)");
        return Ok(());
    }

    let content = fs::read_to_string(&path)?;
    let mut presets = Vec::new();

    if content.contains("eol=lf") {
        presets.push("line-endings (eol=lf)");
    }
    if content.contains("binary") {
        presets.push("binary-files");
    }

    if presets.is_empty() {
        println!("  ✓ custom");
    } else {
        println!("{}", presets.join(", "));
    }

    Ok(())
}

fn print_config(scope: &str) -> Result<()> {
    let label = if scope == "global" {
        "Git config (global)"
    } else {
        "Git config (local)"
    };
    println!("{label}:");

    let scope_flag = if scope == "global" {
        "--global"
    } else {
        "--local"
    };

    let mut any = false;
    for option in crate::config::CONFIG_OPTIONS {
        if let Some(value) = git_config_get(option.key, scope_flag) {
            println!("{} = {value}", option.key);
            any = true;
        }
    }

    if !any {
        println!("  (none)");
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::utils::git_config_get;

    #[test]
    fn git_config_get_returns_none_for_missing_key() {
        let result = git_config_get("nonexistent.key.xyz", "--global");
        assert!(result.is_none());
    }
}