git-send 0.1.6

Commit and push changes with a single command
//! Repository status operations

use anyhow::{Context, Result};

use crate::git::{git, GitRunner, RealGitRunner};
use crate::output::Output;
use crate::types::RepoStatus;

/// Parses git status --porcelain output into `RepoStatus`
pub fn parse_repo_status(output: &str) -> RepoStatus {
    let mut status = RepoStatus::default();

    for line in output.lines() {
        if line.len() < 3 {
            continue;
        }

        let index_status = line.chars().next().unwrap();
        let work_tree_status = line.chars().nth(1).unwrap();
        let file = line[3..].to_string();

        match (index_status, work_tree_status) {
            ('?', '?') => status.untracked.push(file),
            (idx, _) if idx != ' ' && idx != '?' => status.staged.push(file),
            (_, wt) if wt != ' ' && wt != '?' => status.unstaged.push(file),
            _ => {}
        }
    }

    status
}

/// Gets repository status using git runner (testable)
pub fn get_repo_status(git: &dyn GitRunner) -> Result<RepoStatus> {
    let output = git
        .run_output(&["status", "--porcelain"])
        .context("Failed to get repository status")?;
    Ok(parse_repo_status(&output))
}

/// Checks if there are any staged changes (uses `RepoStatus` when available)
pub fn has_staged_changes() -> Result<bool> {
    let runner = RealGitRunner;
    let status = get_repo_status(&runner)?;
    Ok(status.has_staged())
}

/// Checks if there are any uncommitted changes
pub fn has_uncommitted_changes() -> Result<bool> {
    let runner = RealGitRunner;
    let status = get_repo_status(&runner)?;
    Ok(status.has_any_changes())
}

/// Checks if there are any changes to stage (tracked or untracked)
pub fn has_changes_to_stage() -> Result<bool> {
    let runner = RealGitRunner;
    let status = get_repo_status(&runner)?;
    Ok(status.has_any_changes())
}

/// Counts the number of files that would be staged
pub fn count_files_to_stage() -> Result<usize> {
    let runner = RealGitRunner;
    let status = get_repo_status(&runner)?;
    Ok(status.total_files())
}

/// Shows verbose git status.
pub fn show_verbose_status(output: &Output) -> Result<()> {
    output.info("Current git status:");
    git(&["status"])?;
    Ok(())
}

/// Shows short git status.
pub fn show_short_status(output: &Output) -> Result<()> {
    let (stdout, _) = crate::git::git_output_full(&["status", "--short"])?;
    if stdout.is_empty() {
        return Ok(());
    }

    output.println("Changes:");
    output.println(&stdout);
    Ok(())
}

/// Shows git status.
pub fn show_status(verbose: bool, output: &Output) -> Result<()> {
    if verbose {
        return show_verbose_status(output);
    }

    show_short_status(output)
}