eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::{Action, AppState, reducer};
use crate::errors::ComponentError;
use crate::git::parsers::status;
use crate::services::GitService;
use chrono::Local;
use std::sync::Arc;

pub fn refresh_status(
    state: &mut AppState,
    git_service: &Arc<GitService>,
) -> Result<(), ComponentError> {
    let path = state.repo_path.clone();

    if let Ok(branch) = git_service.current_branch(&path) {
        *state = reducer(state.clone(), Action::SetCurrentBranch(branch));
    }

    if state.merge_notifier_enabled {
        let due = state
            .merge_notifier_last_check
            .map(|t| t.elapsed().as_secs() >= state.merge_notifier_interval_secs)
            .unwrap_or(true);
        if due {
            match git_service.ahead_behind_remote(&path, &state.merge_base_branch) {
                Ok((ahead, _behind)) => {
                    let merge_base = state.merge_base_branch.clone();
                    *state = reducer(state.clone(), Action::SetMergeAhead(Some(ahead)));
                    *state = reducer(
                        state.clone(),
                        Action::AppendMergeLog(format!(
                            "{}: ahead {} (auto)",
                            merge_base, ahead
                        )),
                    );
                }
                Err(e) => {
                    tracing::warn!("merge notifier ahead/behind error: {e}");
                    let merge_base = state.merge_base_branch.clone();
                    *state = reducer(state.clone(), Action::SetMergeAhead(None));
                    *state = reducer(
                        state.clone(),
                        Action::AppendMergeLog(format!("{}: error {}", merge_base, e)),
                    );
                }
            }
            state.merge_notifier_last_check = Some(std::time::Instant::now());
            if let Ok(list) = git_service.branch_names(&path) {
                *state = reducer(state.clone(), Action::SetMergeBaseCandidates(list));
            }
        }
    } else {
        *state = reducer(state.clone(), Action::SetMergeAhead(None));
        state.merge_notifier_last_check = None;
    }

    // Auto-fetch cadence
    if state.auto_fetch_enabled {
        let due = state
            .auto_fetch_last_check
            .map(|t| t.elapsed().as_secs() >= state.auto_fetch_interval_secs)
            .unwrap_or(true);
        if due {
            match git_service.fetch_dry_run(&path, &state.auto_fetch_remote) {
                Ok(output) => {
                    let remote = state.auto_fetch_remote.clone();
                    if output.trim().is_empty() {
                        *state = reducer(state.clone(), Action::AppendOpLog(format!("fetch {}: no changes", remote)));
                    } else {
                        *state = reducer(state.clone(), Action::AppendOpLog(format!("fetch {}:", remote)));
                        for line in output.lines().take(5) {
                            *state = reducer(state.clone(), Action::AppendOpLog(format!("  {}", line)));
                        }
                    }
                }
                Err(e) => {
                    tracing::warn!("auto-fetch error: {e}");
                    *state = reducer(state.clone(), Action::AppendOpLog(format!("fetch error: {e}")));
                }
            }
            state.auto_fetch_last_check = Some(std::time::Instant::now());
        }
    } else {
        state.auto_fetch_last_check = None;
    }

    match git_service.status_porcelain(&path) {
        Ok(out) => {
            *state = reducer(state.clone(), Action::SetStatusError(None));
            let now = Local::now().format("%H:%M:%S").to_string();
            *state = reducer(state.clone(), Action::SetLastRefreshed(Some(now)));

            let mut lines: Vec<String> = Vec::new();
            for line in out.lines().take(200) {
                lines.push(line.to_string());
            }
            if out.lines().count() > 200 {
                lines.push(format!("… (+{} more)", out.lines().count() - 200));
            }

            if let Ok(summary) = status::parse_status(&out) {
                *state = reducer(state.clone(), Action::SetStatusSummary(summary));
            }

            let out_clone = out.clone();
            *state = reducer(state.clone(), Action::SetStatus(out));

            let mut entries = status::parse_status_entries(&state.last_status);
            let total_entries = entries.len();
            if total_entries > 200 {
                entries.truncate(200);
                entries.push(status::StatusEntry {
                    staged: false,
                    unstaged: false,
                    conflict: false,
                    path: format!("… (+{} more)", total_entries - 200),
                });
            }
            *state = reducer(state.clone(), Action::SetStatusEntries(entries));
            *state = reducer(state.clone(), Action::SetStatusLines(lines));

            // Auto-enable conflicts-only if conflicts present.
            let has_conflicts = state.status_entries.iter().any(|e| e.conflict);
            if has_conflicts {
                *state = reducer(state.clone(), Action::FilterConflictsOnly);
            } else {
                *state = reducer(state.clone(), Action::ClearConflictFilter);
            }
        }
        Err(e) => {
            tracing::warn!("status error: {e}");
            *state = reducer(state.clone(), Action::SetStatusError(Some(e.to_string())));
            let now = Local::now().format("%H:%M:%S").to_string();
            *state = reducer(state.clone(), Action::SetLastRefreshed(Some(now)));
        }
    }

    *state = reducer(state.clone(), Action::SetRefreshing(false));
    Ok(())
}