eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::{actions::Action, state::AppState};

pub fn handle(state: &mut AppState, action: &Action) -> bool {
    match action {
        Action::ShowRebaseTodo => {
            state.show_rebase_todo = true;
            state.rebase_todo_selected = 0;
            state.command_palette = false;
            state.rebase_todo_editing = false;
            state.rebase_todo_edit_buffer.clear();
        }
        Action::HideRebaseTodo => {
            state.show_rebase_todo = false;
            state.rebase_todo_editing = false;
            state.rebase_todo_edit_buffer.clear();
        }
        Action::RebaseTodoUp => {
            if state.rebase_todo_selected > 0 {
                state.rebase_todo_selected -= 1;
            }
        }
        Action::RebaseTodoDown => {
            let max_idx = state.rebase_todo.len().saturating_sub(1);
            if state.rebase_todo_selected < max_idx {
                state.rebase_todo_selected += 1;
            }
        }
        Action::SetRebaseTodo(lines) => {
            state.rebase_todo = lines.clone();
            state.rebase_todo_selected = 0;
            state.rebase_todo_original = lines.clone();
            state.rebase_todo_dirty = false;
        }
        Action::SetRebaseTodoWithPath(lines, path) => {
            state.rebase_todo = lines.clone();
            state.rebase_todo_selected = 0;
            state.rebase_todo_original = lines.clone();
            state.rebase_todo_path = Some(path.clone());
            state.rebase_todo_dirty = false;
        }
        Action::SetRebaseTodoPath(path) => {
            state.rebase_todo_path = Some(path.clone());
        }
        Action::StartRebaseTodoEdit(idx, text) => {
            state.rebase_todo_selected = (*idx).min(state.rebase_todo.len().saturating_sub(1));
            state.rebase_todo_editing = true;
            state.rebase_todo_edit_buffer = text.clone();
        }
        Action::RebaseTodoEditBuffer(text) => {
            state.rebase_todo_editing = true;
            state.rebase_todo_edit_buffer = text.clone();
        }
        Action::RebaseTodoToggleKind(idx, kind) => {
            if let Some(line) = state.rebase_todo.get_mut(*idx) {
                let mut parts = line.splitn(2, char::is_whitespace);
                let _ = parts.next();
                let tail = parts.next().unwrap_or("").trim_start();
                let new_line = if tail.is_empty() {
                    kind.clone()
                } else {
                    format!("{} {}", kind, tail)
                };
                *line = new_line;
                state.rebase_todo_dirty = true;
            }
        }
        Action::RebaseTodoRewriteLine(idx, text) => {
            if let Some(line) = state.rebase_todo.get_mut(*idx) {
                *line = text.clone();
                state.rebase_todo_dirty = true;
            }
            state.rebase_todo_editing = false;
            state.rebase_todo_edit_buffer.clear();
        }
        Action::SaveRebaseTodo => {
            if let Some(path) = &state.rebase_todo_path {
                match std::fs::write(path, state.rebase_todo.join("\n")) {
                    Ok(_) => {
                        state.rebase_todo_dirty = false;
                    }
                    Err(e) => {
                        // Store error in state for display
                        // Note: We can't return error from reducer, so we'll set a flag
                        // The error should be handled at a higher level
                        tracing::error!(error = %e, "Failed to save rebase todo file");
                        // Keep dirty flag true to indicate save failed
                    }
                }
            }
        }
        _ => return false,
    }
    true
}

#[cfg(test)]
mod tests {
    use super::*;

    fn create_test_state() -> AppState {
        let mut state = AppState::new();
        state.repo_path = "/tmp/test".to_string();
        state
    }

    #[test]
    fn test_rebase_todo_navigation() {
        let mut state = create_test_state();
        state.rebase_todo = vec![
            "pick abc123 First".to_string(),
            "pick def456 Second".to_string(),
            "pick ghi789 Third".to_string(),
        ];
        state.rebase_todo_selected = 1;

        // Move up
        let action = Action::RebaseTodoUp;
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo_selected, 0);

        // Move down
        let action = Action::RebaseTodoDown;
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo_selected, 1);

        // Move down again
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo_selected, 2);

        // Try to move down past end
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo_selected, 2); // Should stay at max
    }

    #[test]
    fn test_rebase_todo_editing() {
        let mut state = create_test_state();
        state.rebase_todo = vec!["pick abc123 Test".to_string()];

        // Start editing
        let action = Action::StartRebaseTodoEdit(0, "pick abc123 Test".to_string());
        handle(&mut state, &action);
        assert!(state.rebase_todo_editing);
        assert_eq!(state.rebase_todo_edit_buffer, "pick abc123 Test");

        // Update buffer
        let action = Action::RebaseTodoEditBuffer("reword abc123 Modified".to_string());
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo_edit_buffer, "reword abc123 Modified");

        // Rewrite line
        let action = Action::RebaseTodoRewriteLine(0, "reword abc123 Modified".to_string());
        handle(&mut state, &action);
        assert!(!state.rebase_todo_editing);
        assert_eq!(state.rebase_todo[0], "reword abc123 Modified");
        assert!(state.rebase_todo_dirty);
    }

    #[test]
    fn test_rebase_todo_toggle_kind() {
        let mut state = create_test_state();
        state.rebase_todo = vec!["pick abc123 Test commit".to_string()];

        // Toggle to reword
        let action = Action::RebaseTodoToggleKind(0, "reword".to_string());
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo[0], "reword abc123 Test commit");
        assert!(state.rebase_todo_dirty);

        // Toggle to drop
        let action = Action::RebaseTodoToggleKind(0, "drop".to_string());
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo[0], "drop abc123 Test commit");
    }

    #[test]
    fn test_save_rebase_todo() {
        let mut state = create_test_state();
        let temp_file = std::env::temp_dir().join(format!("test_rebase_todo_{}", std::process::id()));
        state.rebase_todo_path = Some(temp_file.to_string_lossy().to_string());
        state.rebase_todo = vec![
            "pick abc123 First".to_string(),
            "pick def456 Second".to_string(),
        ];
        state.rebase_todo_dirty = true;

        // Save
        let action = Action::SaveRebaseTodo;
        handle(&mut state, &action);
        assert!(!state.rebase_todo_dirty);

        // Verify file was written
        let content = std::fs::read_to_string(&temp_file).expect("Failed to read file");
        assert_eq!(content, "pick abc123 First\npick def456 Second");

        // Cleanup
        let _ = std::fs::remove_file(&temp_file);
    }

    #[test]
    fn test_rebase_todo_show_hide() {
        let mut state = create_test_state();

        // Show
        let action = Action::ShowRebaseTodo;
        handle(&mut state, &action);
        assert!(state.show_rebase_todo);
        assert_eq!(state.rebase_todo_selected, 0);
        assert!(!state.command_palette);

        // Hide
        let action = Action::HideRebaseTodo;
        handle(&mut state, &action);
        assert!(!state.show_rebase_todo);
        assert!(!state.rebase_todo_editing);
    }

    #[test]
    fn test_set_rebase_todo() {
        let mut state = create_test_state();
        let lines = vec![
            "pick abc123 First".to_string(),
            "pick def456 Second".to_string(),
        ];

        let action = Action::SetRebaseTodo(lines.clone());
        handle(&mut state, &action);
        assert_eq!(state.rebase_todo, lines);
        assert_eq!(state.rebase_todo_original, lines);
        assert_eq!(state.rebase_todo_selected, 0);
        assert!(!state.rebase_todo_dirty);
    }
}