eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Pure update function following TEA pattern.
//!
//! The update function is the ONLY place where state can change.
//! It is a pure function: (State, Msg) -> (State, Option<Effect>)
//!
//! ## Properties
//! 
//! - **Pure**: No side effects, only state transformation
//! - **Exhaustive**: Must handle every Msg variant
//! - **Traceable**: Every state change is triggered by a Msg

use super::effects::Effect;
use super::msg::{Msg, RebaseOutcome};

/// Pure update function that transforms state based on messages.
/// 
/// Returns the effect to execute (if any) after the state update.
/// The actual state mutation happens via the `state` mutable reference,
/// which is acceptable in Rust's ownership model.
/// 
/// # Design Decision
/// 
/// We use `&mut AppState` instead of returning a new state because:
/// 1. Rust's ownership model ensures exclusive access
/// 2. Large state cloning would be expensive
/// 3. The function is still logically pure (same input → same output)
pub fn update(state: &mut crate::app::AppState, msg: Msg) -> Option<Effect> {
    // Log every message for debugging (TEA principle: observability)
    tracing::debug!("TEA update: {:?}", msg);
    
    match msg {
        // =====================================================================
        // Lifecycle
        // =====================================================================
        Msg::Tick => {
            // Periodic tick - no state change, no effect
            None
        }
        
        Msg::RequestQuit => {
            state.running = false;
            None
        }
        
        // =====================================================================
        // Git Operations - Request (trigger effects)
        // =====================================================================
        Msg::RequestStage { path } => {
            state.feedback_message = Some(format!("Staging {}...", path));
            Some(Effect::GitStage { path: path.into() })
        }
        
        Msg::RequestUnstage { path } => {
            state.feedback_message = Some(format!("Unstaging {}...", path));
            Some(Effect::GitUnstage { path: path.into() })
        }
        
        Msg::RequestCommit { message, amend } => {
            state.feedback_message = Some("Committing...".to_string());
            Some(Effect::GitCommit {
                message,
                amend,
                author_name: None,
                author_email: None,
            })
        }
        
        Msg::RequestPush { force } => {
            state.feedback_message = Some("Pushing...".to_string());
            Some(Effect::GitPush { force_with_lease: force })
        }
        
        Msg::RequestRebaseContinue { message, author_name, author_email } => {
            state.feedback_message = Some("Continuing rebase...".to_string());
            Some(Effect::GitRebaseContinue { message, author_name, author_email })
        }
        
        Msg::RequestRebaseAbort => {
            state.feedback_message = Some("Aborting rebase...".to_string());
            Some(Effect::GitRebaseAbort)
        }
        
        // =====================================================================
        // Git Operations - Results (from effect handlers)
        // =====================================================================
        Msg::StageStarted { path } => {
            state.feedback_message = Some(format!("Staging {}...", path));
            None
        }
        
        Msg::StageSuccess { path: _ } => {
            state.feedback_message = Some("Staged successfully".to_string());
            Some(Effect::RefreshStatus)
        }
        
        Msg::StageFailed { path, error } => {
            state.feedback_message = None;
            state.last_status_error = Some(format!("Failed to stage {}: {}", path, error));
            None
        }
        
        Msg::CommitStarted => {
            state.feedback_message = Some("Committing...".to_string());
            None
        }
        
        Msg::CommitSuccess { hash } => {
            state.feedback_message = Some(format!("Committed: {}", &hash[..7.min(hash.len())]));
            state.commit_input.clear();
            state.commit_mode = false;
            Some(Effect::RefreshAll)
        }
        
        Msg::CommitFailed { error } => {
            state.feedback_message = None;
            state.last_status_error = Some(error);
            None
        }
        
        Msg::RebaseStarted => {
            state.feedback_message = Some("Rebase in progress...".to_string());
            None
        }
        
        Msg::RebaseSuccess { outcome } => {
            match outcome {
                RebaseOutcome::Completed => {
                    state.feedback_message = Some("Rebase completed".to_string());
                }
                RebaseOutcome::PausedForEdit => {
                    state.feedback_message = Some("Rebase paused for edit".to_string());
                }
                RebaseOutcome::PausedForReword { commit_hash: _, current_message: _ } => {
                    // Legacy TEA code - modal state now derived from FSM
                    // state.reword_modal_open = true;
                    // state.reword_message = Some(current_message);
                    state.feedback_message = Some("Rebase paused for reword (use FSM)".to_string());
                }
                RebaseOutcome::Conflicts { files } => {
                    state.last_status_error = Some(format!("Conflicts in: {}", files.join(", ")));
                }
            }
            Some(Effect::RefreshAll)
        }
        
        Msg::RebaseFailed { error } => {
            state.feedback_message = None;
            state.last_status_error = Some(error);
            None
        }
        
        Msg::StatusRefreshed { entries } => {
            // Convert StatusEntryData to StatusEntry
            state.status_entries = entries.into_iter().map(|e| {
                crate::git::parsers::status::StatusEntry {
                    path: e.path,
                    staged: e.staged,
                    unstaged: e.unstaged,
                    conflict: e.conflict,
                }
            }).collect();
            None
        }
        
        Msg::StatusRefreshFailed { error } => {
            state.last_status_error = Some(error);
            None
        }
        
        Msg::DiffComputed { path, content, truncated } => {
            state.last_diff = content;
            state.last_diff_path = path;
            state.last_diff_truncated = truncated;
            None
        }
        
        // =====================================================================
        // Navigation (pure state updates)
        // =====================================================================
        Msg::NavigateUp | Msg::NavigateDown | Msg::NavigateTop | 
        Msg::NavigateBottom | Msg::PageUp | Msg::PageDown => {
            // Navigation is handled by existing focus-aware code
            // TEA migration will fully implement these in Phase 2
            None
        }
        
        Msg::FocusNext => {
            // Cycle focus to next pane - delegate to existing logic
            None
        }
        
        Msg::FocusPrev => {
            // Cycle focus to previous pane - delegate to existing logic
            None
        }
        
        // =====================================================================
        // UI State
        // =====================================================================
        Msg::ToggleHelp => {
            state.show_help = !state.show_help;
            None
        }
        
        Msg::ShowThemePicker => {
            state.theme_picker = true;
            None
        }
        
        Msg::HideThemePicker => {
            state.theme_picker = false;
            None
        }
        
        Msg::ApplyTheme { name } => {
            let theme = crate::config::load_theme_by_name(&name);
            state.theme = theme;
            state.theme_picker = false;
            Some(Effect::SaveConfig {
                key: "theme".to_string(),
                value: name,
            })
        }
        
        Msg::SetFeedback { message } => {
            state.feedback_message = message;
            None
        }
        
        Msg::SetError { error, guidance: _ } => {
            state.last_status_error = error;
            None
        }
        
        Msg::ClearError => {
            state.last_status_error = None;
            state.error_guidance = None;
            None
        }
        
        // =====================================================================
        // Input Mode
        // =====================================================================
        Msg::StartCommitInput => {
            state.commit_mode = true;
            state.commit_input.clear();
            None
        }
        
        Msg::CancelCommitInput => {
            state.commit_mode = false;
            state.commit_input.clear();
            None
        }
        
        Msg::UpdateCommitInput { text } => {
            state.commit_input = text;
            None
        }
        
        Msg::CharInput { ch } => {
            // Append to active input field based on mode
            if state.commit_mode {
                state.commit_input.push(ch);
            }
            None
        }
        
        Msg::Backspace => {
            // Remove last char from active input field
            if state.commit_mode {
                state.commit_input.pop();
            }
            None
        }
    }
}

// Tests are temporarily disabled until AppState migration is complete.
// They will be re-enabled in Phase 2 when we have full control of state structure.
#[cfg(test)]
mod tests {
    #[test]
    fn placeholder() {
        // Placeholder to ensure test module compiles
        // Full tests will be added in Phase 2 when AppState is migrated
        assert!(true);
    }
}