g-cli 0.1.0

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

use crate::git;

pub fn run() {
    let branch = git::current_branch();

    // Check for uncommitted changes
    let files = git::parse_status();
    if !files.is_empty() {
        println!();
        println!(
            "  {} You have {} uncommitted change(s).",
            "".yellow(),
            files.len()
        );
        println!("  Save or stash them before syncing.");
        println!();
        println!("  {}:", "Suggestions".dimmed());
        println!("    {} {}", "".dimmed(), "g save \"...\"".cyan());
        println!("    {} {}", "".dimmed(), "g stash \"...\"".cyan());
        println!();
        return;
    }

    // Check for upstream
    let has_upstream = git::ahead_behind().is_some();
    if !has_upstream {
        println!();
        println!(
            "  {} No upstream configured for '{}'.",
            "".yellow(),
            branch.cyan()
        );
        println!("  Push first with {}", "g push".cyan());
        println!();
        return;
    }

    println!();
    println!("  Pulling latest changes from remote...");
    println!();

    let result = git::run(&["pull", "--no-rebase"]);

    if result.success {
        if result.stdout.contains("Already up to date") {
            println!("  {} Already up to date.", "".green());
        } else {
            println!("  {} Synced successfully.", "".green().bold());
            if !result.stdout.is_empty() {
                // Show a brief summary
                let lines: Vec<&str> = result.stdout.lines().collect();
                let summary_lines: Vec<&&str> = lines.iter().take(5).collect();
                for line in summary_lines {
                    println!("    {}", line.dimmed());
                }
                if lines.len() > 5 {
                    println!("    {} more lines...", format!("(+{})", lines.len() - 5).dimmed());
                }
            }
        }
        println!();
        return;
    }

    // Check if it's a merge conflict
    let stderr = &result.stderr;
    let stdout = &result.stdout;
    let has_conflict = stderr.contains("CONFLICT")
        || stderr.contains("Merge conflict")
        || stdout.contains("CONFLICT")
        || stdout.contains("Merge conflict");

    if has_conflict {
        // Find conflicting files
        let conflict_files = find_conflict_files();

        println!(
            "  {} Merge conflict detected!",
            "".yellow().bold()
        );
        println!();

        if !conflict_files.is_empty() {
            println!("  {}:", "Conflicting files".yellow());
            for f in &conflict_files {
                println!("    {} {}", "!".yellow(), f);
            }
            println!();
        }

        let options = &[
            "Accept incoming changes (theirs)",
            "Keep your version (ours)",
            "Abort merge (go back to before sync)",
        ];

        let selection = Select::new()
            .with_prompt("  What would you like to do?")
            .items(options)
            .default(0)
            .interact();

        let selection = match selection {
            Ok(s) => s,
            Err(_) => {
                println!("  Aborting merge...");
                git::run(&["merge", "--abort"]);
                println!("  {} Merge aborted.", "".green());
                println!();
                return;
            }
        };

        println!();

        match selection {
            0 => {
                // Accept theirs
                for f in &conflict_files {
                    git::run(&["checkout", "--theirs", f]);
                    git::run(&["add", f]);
                }
                let commit = git::run(&["commit", "--no-edit"]);
                if commit.success {
                    println!("  {} Conflicts resolved — accepted incoming changes.", "".green().bold());
                } else {
                    println!("  {} Failed to complete merge: {}", "".red(), commit.stderr);
                }
            }
            1 => {
                // Keep ours
                for f in &conflict_files {
                    git::run(&["checkout", "--ours", f]);
                    git::run(&["add", f]);
                }
                let commit = git::run(&["commit", "--no-edit"]);
                if commit.success {
                    println!("  {} Conflicts resolved — kept your version.", "".green().bold());
                } else {
                    println!("  {} Failed to complete merge: {}", "".red(), commit.stderr);
                }
            }
            _ => {
                git::run(&["merge", "--abort"]);
                println!("  {} Merge aborted. You're back to where you started.", "".green());
            }
        }
        println!();
    } else {
        // Some other error
        println!("  {} Sync failed: {}", "".red(), stderr);
        println!();
    }
}

fn find_conflict_files() -> Vec<String> {
    let result = git::run(&["diff", "--name-only", "--diff-filter=U"]);
    if result.success && !result.stdout.is_empty() {
        result.stdout.lines().map(|l| l.to_string()).collect()
    } else {
        vec![]
    }
}