g-cli 0.1.0

Git that talks back. A human-friendly CLI wrapper for Git.
use colored::Colorize;
use dialoguer::Confirm;

use crate::git;

pub fn run() {
    println!();
    println!("  Analyzing branches...");
    println!();

    let current = git::current_branch();
    let main_branch = detect_main_branch();

    // Get merged branches
    let merged_result = if let Some(ref main_br) = main_branch {
        git::run(&["branch", "--merged", main_br])
    } else {
        git::run(&["branch", "--merged"])
    };

    let mut safe_to_delete: Vec<String> = vec![];
    let mut all_branches: Vec<String> = vec![];

    // Parse all branches
    let branch_list = git::run(&["branch"]);
    if branch_list.success {
        for line in branch_list.stdout.lines() {
            let name = line.trim().trim_start_matches("* ").to_string();
            if !name.is_empty() {
                all_branches.push(name);
            }
        }
    }

    // Parse merged branches
    if merged_result.success {
        for line in merged_result.stdout.lines() {
            let name = line.trim().trim_start_matches("* ").to_string();
            if name.is_empty() { continue; }
            if name == current { continue; }
            if Some(&name) == main_branch.as_ref() { continue; }
            if name == "main" || name == "master" { continue; }
            safe_to_delete.push(name);
        }
    }

    // Find possibly stale branches (not merged)
    let not_merged: Vec<String> = all_branches
        .iter()
        .filter(|b| {
            **b != current
                && !safe_to_delete.contains(b)
                && **b != "main"
                && **b != "master"
        })
        .cloned()
        .collect();

    if safe_to_delete.is_empty() && not_merged.is_empty() {
        println!("  {} All clean! No branches to prune.", "".green());
        println!();
        return;
    }

    if !safe_to_delete.is_empty() {
        println!("  {}:", "Safe to delete (already merged)".green().bold());
        for name in &safe_to_delete {
            let age = branch_last_commit_age(name);
            println!("    {} {}  {}", "-".green(), name, age.dimmed());
        }
        println!();
    }

    if !not_merged.is_empty() {
        println!("  {}:", "Possibly unsafe (not merged)".yellow().bold());
        for name in &not_merged {
            let age = branch_last_commit_age(name);
            println!("    {} {}  {}", "?".yellow(), name, age.dimmed());
        }
        println!();
    }

    if safe_to_delete.is_empty() {
        println!("  No safely deletable branches found.");
        println!();
        return;
    }

    let confirm = Confirm::new()
        .with_prompt(format!(
            "  Delete {} safe branch(es)?",
            safe_to_delete.len()
        ))
        .default(false)
        .interact();

    match confirm {
        Ok(true) => {}
        _ => {
            println!("  Cancelled.");
            println!();
            return;
        }
    }

    println!();

    let mut deleted = 0;
    for name in &safe_to_delete {
        let result = git::run(&["branch", "-d", name]);
        if result.success {
            println!("  {} Deleted '{}'", "".green(), name);
            deleted += 1;
        } else {
            println!("  {} Failed to delete '{}': {}", "".red(), name, result.stderr);
        }
    }

    println!();
    println!(
        "  {} {} branch(es) cleaned up.",
        "".green().bold(),
        deleted
    );
    println!();
}

fn detect_main_branch() -> Option<String> {
    if git::branch_exists("main") {
        Some("main".to_string())
    } else if git::branch_exists("master") {
        Some("master".to_string())
    } else {
        None
    }
}

fn branch_last_commit_age(branch: &str) -> String {
    let result = git::run(&["log", "-1", "--format=%cr", branch]);
    if result.success && !result.stdout.is_empty() {
        format!("(last commit {})", result.stdout)
    } else {
        String::new()
    }
}