eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::{AppState, state::DiffMode};
use super::ComponentManager;
use tracing::{debug, warn};

/// Synchronizes the status selection with the diff view.
pub fn sync_status_selection(mgr: &ComponentManager, state: &mut AppState) {
    // Step 1: Ensure selected index is within bounds
    let max_idx = state.status_entries.len().saturating_sub(1);
    let selected = state.status_selected.min(max_idx);
    state.status_selected = selected;
    
    // Step 2: Get the selected entry, or clear diff if none selected
    let Some(entry) = state.status_entries.get(selected) else {
        // No entry selected or out of bounds - clear diff
        state.last_diff.clear();
        state.last_diff_path.clear();
        state.last_status_error = None;
        debug!("No status entry selected, cleared diff");
        return;
    };
    
    // Step 3: Update selected path
    state.status_selected_path = entry.path.clone();
    let current_diff_mode_is_staged = matches!(state.diff_mode, DiffMode::Staged);
    
    // Step 4: Determine which diff type to show based on file state
    let should_show_staged_diff = determine_diff_mode(entry, current_diff_mode_is_staged);
    
    // Step 5: Fetch the diff
    let mut diff_result = mgr.git_service.diff(
        &state.repo_path,
        &state.status_selected_path,
        should_show_staged_diff,
        state.diff_context,
    );

    // Step 6: Fallback logic - if working diff is empty but file has staged changes, try staged diff
    let final_diff_mode = if should_fallback_to_staged_diff(&diff_result, should_show_staged_diff, entry) {
        state.diff_mode = DiffMode::Staged;
        debug!(path = %state.status_selected_path, "Working diff empty, falling back to staged diff");
        diff_result = mgr.git_service.diff(
            &state.repo_path,
            &state.status_selected_path,
            true, // Force staged diff
            state.diff_context,
        );
        true
    } else {
        should_show_staged_diff
    };

    // Step 7: Update state with diff result
    update_diff_state(state, diff_result, final_diff_mode);
}

/// Determines which diff mode to use based on file entry state.
fn determine_diff_mode(entry: &crate::git::parsers::status::StatusEntry, current_mode_is_staged: bool) -> bool {
    if entry.unstaged {
        // Has unstaged changes → show working diff (most relevant)
        false
    } else if entry.staged {
        // Only staged → show staged diff
        true
    } else {
        // Untracked or no changes → use current mode
        current_mode_is_staged
    }
}

/// Checks if we should fallback to staged diff when working diff is empty.
fn should_fallback_to_staged_diff(
    diff_result: &Result<String, crate::errors::GitError>,
    currently_showing_staged: bool,
    entry: &crate::git::parsers::status::StatusEntry,
) -> bool {
    // Only fallback if:
    // 1. We're currently showing working diff (not already showing staged)
    // 2. The working diff is empty
    // 3. The file has staged changes
    !currently_showing_staged
        && diff_result.as_ref().map(|d| d.is_empty()).unwrap_or(false)
        && entry.staged
}

/// Updates the application state with the diff result.
fn update_diff_state(
    state: &mut AppState,
    diff_result: Result<String, crate::errors::GitError>,
    is_staged: bool,
) {
    match diff_result {
        Ok(diff) => {
            state.last_diff = diff;
            state.last_diff_path = state.status_selected_path.clone();
            state.detail_scroll = 0;
            state.load_full_diff = false;
            state.last_diff_truncated = false;
            state.last_diff_total = state.last_diff.lines().count();
            state.last_status_error = None;
            debug!(
                path = %state.status_selected_path,
                staged = is_staged,
                lines = state.last_diff_total,
                "Diff refreshed successfully"
            );
        }
        Err(e) => {
            state.last_diff.clear();
            // Enhanced error message with actionable context
            let error_msg = format!(
                "Failed to load {} diff for '{}': {}. \
                Check file permissions and Git repository state.",
                if is_staged { "staged" } else { "working" },
                state.status_selected_path,
                e
            );
            state.last_status_error = Some(error_msg.clone());
            warn!(
                error = %e,
                path = %state.status_selected_path,
                staged = is_staged,
                operation = "diff_refresh",
                "Diff refresh failed - file may be deleted or repository corrupted"
            );
        }
    }
}