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};
use crate::app::state::{RebaseAction as OldRebaseAction, RebaseEntry as OldRebaseEntry};

pub fn handle_rebase(mut state: AppState, action: &Action) -> Option<AppState> {
    match action {
        Action::LogToggleSelect => {
            if let Some(commit) = state.commits.get(state.commit_selected) {
                if state.log_selected_hashes.contains(&commit.hash) {
                    state.log_selected_hashes.remove(&commit.hash);
                } else {
                    state.log_selected_hashes.insert(commit.hash.clone());
                }
            }
        }
        Action::RebaseSetBase(hash) => {
            state.rebase_base = Some(hash.clone());
        }
        Action::StartInteractiveRebase(commits) => {
            // Initialize new rebase session
            let entries = commits
                .iter()
                .map(|h| {
                    if let Some(c) = state.commits.iter().find(|c| c.hash == *h) {
                        crate::app::rebase::RebaseEntry::new(
                            c.hash.clone(),
                            c.message.clone(),
                            crate::app::rebase::RebaseAction::Pick,
                        )
                    } else {
                        crate::app::rebase::RebaseEntry::new(
                            h.clone(),
                            "(not loaded)".to_string(),
                            crate::app::rebase::RebaseAction::Pick,
                        )
                    }
                })
                .collect();

            state.rebase_session = crate::app::rebase::RebaseSession {
                phase: crate::app::rebase::RebasePhase::Planning,
                entries,
                cursor: 0,
                todo_path: None,
                base_commit: commits.first().cloned(),
                use_root: false,
                dirty: true,
            };

            state.rebase_editor_open = true;
        }
        Action::RebaseToggleUseRoot => {
            state.rebase_use_root = !state.rebase_use_root;
            state.rebase_dirty = true;
        }
        Action::RebaseCancel => {
            state.rebase_editor_open = false;
            state.rebase_session.entries.clear();
            state.rebase_session.cursor = 0;
            state.rebase_session.dirty = false;
            state.rebase_session.phase = crate::app::rebase::RebasePhase::Planning;
        }
        Action::RebaseNext => {
            state.rebase_session.move_cursor_down();
        }
        Action::RebasePrev => {
            state.rebase_session.move_cursor_up();
        }
        Action::RebaseMoveUp => {
            state.rebase_session.move_entry_up();
        }
        Action::RebaseMoveDown => {
            state.rebase_session.move_entry_down();
        }
        Action::RebaseCycleAction => {
            if let Some(entry) = state.rebase_session.entries.get_mut(state.rebase_session.cursor) {
                entry.action = entry.action.cycle_next();
                state.rebase_session.dirty = true;
            }
        }
        Action::RebaseSetAction(action) => {
            if let Some(entry) = state.rebase_session.entries.get_mut(state.rebase_session.cursor) {
                entry.action = *action;
                state.rebase_session.dirty = true;
            }
        }
        Action::RebaseLoadTodo(lines) => {
            let entries = lines
                .iter()
                .filter_map(|line| {
                    if line.trim_start().starts_with('#') {
                        return None;
                    }
                    let parts: Vec<&str> = line.split_whitespace().collect();
                    if parts.len() >= 2 {
                        let verb = parts[0];
                        let hash = parts[1].to_string();
                        let message = line
                            .split_once(parts[1])
                            .map(|(_, msg)| msg.trim().to_string())
                            .unwrap_or_default();
                        let action = match verb {
                            "pick" => crate::app::rebase::RebaseAction::Pick,
                            "reword" => crate::app::rebase::RebaseAction::Reword,
                            "edit" => crate::app::rebase::RebaseAction::Edit,
                            "squash" => crate::app::rebase::RebaseAction::Squash,
                            "fixup" => crate::app::rebase::RebaseAction::Fixup,
                            "drop" => crate::app::rebase::RebaseAction::Drop,
                            _ => crate::app::rebase::RebaseAction::Pick,
                        };
                        Some(crate::app::rebase::RebaseEntry::new(hash, message, action))
                    } else {
                        None
                    }
                })
                .collect();

            state.rebase_session.entries = entries;
            state.rebase_session.cursor = 0;
            state.rebase_session.dirty = false;
            state.rebase_session.phase = crate::app::rebase::RebasePhase::Recovery;
            state.rebase_session.base_commit = state.rebase_session.entries.last().map(|e| e.hash.clone());
            state.rebase_editor_open = true;
        }
        Action::RebaseSaveAndRun => {
            // This action is now handled by RebaseSaveAndRunCommand
            // The reducer just updates UI state - actual execution happens in command
            state.rebase_editor_open = false;
            state.rebase_dirty = false;
        }

        Action::SetRewordAuthorName(name) => {
            state.reword_author_name = Some(name.clone());
        }
        Action::SetRewordAuthorEmail(email) => {
            state.reword_author_email = Some(email.clone());
        }
        Action::FocusAndClearRewordAuthorName => {
            state.reword_focus_field = crate::app::state::RewordField::AuthorName;
            state.reword_author_name = Some(String::new());
        }
        Action::FocusAndClearRewordAuthorEmail => {
            state.reword_focus_field = crate::app::state::RewordField::AuthorEmail;
            state.reword_author_email = Some(String::new());
        }

        Action::SetRewordFocus(field) => {
            state.reword_focus_field = *field;
        }
        Action::SetEditAuthorName(name) => {
            state.edit_author_name = Some(name.clone());
        }
        Action::SetEditAuthorEmail(email) => {
            state.edit_author_email = Some(email.clone());
        }

        Action::FocusEditMessage => {
            state.edit_focus_field = crate::app::state::RewordField::Message;
        }
        Action::FocusEditAuthorName => {
            state.edit_focus_field = crate::app::state::RewordField::AuthorName;
        }
        Action::FocusEditAuthorEmail => {
            state.edit_focus_field = crate::app::state::RewordField::AuthorEmail;
        }
        Action::FocusAndClearEditAuthorName => {
            state.edit_focus_field = crate::app::state::RewordField::AuthorName;
            state.edit_author_name = Some(String::new());
        }
        Action::FocusAndClearEditAuthorEmail => {
            state.edit_focus_field = crate::app::state::RewordField::AuthorEmail;
            state.edit_author_email = Some(String::new());
        }

        Action::RebaseAbortFromBuilder => {
            // no-op here; handled in event loop
        }
        Action::ShowRebaseRecovery => {
            state.rebase_recovery_open = true;
        }
        Action::HideRebaseRecovery => {
            state.rebase_recovery_open = false;
        }
        _ => return None,
    }
    Some(state)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::state::{RebaseAction, RebaseEntry};

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

    #[test]
    fn test_rebase_builder_creation() {
        let mut state = create_test_state();
        state.commits = vec![
            crate::app::state::CommitEntry {
                hash: "abc123".to_string(),
                short_hash: "abc123".to_string(),
                message: "Test commit".to_string(),
                author: "Test".to_string(),
            },
        ];

        let commits = vec!["abc123".to_string()];
        let action = Action::StartInteractiveRebase(commits);
        let result = handle_rebase(state, &action);

        assert!(result.is_some());
        let new_state = result.unwrap();
        assert!(new_state.rebase_editor_open);
        assert_eq!(new_state.rebase_session.entries.len(), 1);
        assert_eq!(new_state.rebase_session.entries[0].action, crate::app::rebase::RebaseAction::Pick);
    }

    #[test]
    fn test_rebase_action_cycling() {
        let mut state = create_test_state();
        state.rebase_session.entries = vec![crate::app::rebase::RebaseEntry::new(
            "abc123".to_string(),
            "Test".to_string(),
            crate::app::rebase::RebaseAction::Pick,
        )];
        state.rebase_session.cursor = 0;

        // Cycle through actions
        let action = Action::RebaseCycleAction;
        let result = handle_rebase(state, &action);
        assert!(result.is_some());
        let new_state = result.unwrap();
        assert_eq!(new_state.rebase_session.entries[0].action, crate::app::rebase::RebaseAction::Reword);

        // Cycle again
        let state2 = new_state;
        let result2 = handle_rebase(state2, &action);
        assert!(result2.is_some());
        let new_state2 = result2.unwrap();
        assert_eq!(new_state2.rebase_session.entries[0].action, crate::app::rebase::RebaseAction::Edit);
    }

    #[test]
    fn test_rebase_entry_movement() {
        let mut state = create_test_state();
        state.rebase_session.entries = vec![
            crate::app::rebase::RebaseEntry::new(
                "abc123".to_string(),
                "First".to_string(),
                crate::app::rebase::RebaseAction::Pick,
            ),
            crate::app::rebase::RebaseEntry::new(
                "def456".to_string(),
                "Second".to_string(),
                crate::app::rebase::RebaseAction::Pick,
            ),
        ];
        state.rebase_session.cursor = 1;

        // Move up
        let action = Action::RebaseMoveUp;
        let result = handle_rebase(state, &action);
        assert!(result.is_some());
        let new_state = result.unwrap();
        assert_eq!(new_state.rebase_session.cursor, 0);
        assert_eq!(new_state.rebase_session.entries[0].hash, "def456");
        assert_eq!(new_state.rebase_session.entries[1].hash, "abc123");

        // Move down
        let state2 = new_state;
        let action2 = Action::RebaseMoveDown;
        let result2 = handle_rebase(state2, &action2);
        assert!(result2.is_some());
        let new_state2 = result2.unwrap();
        assert_eq!(new_state2.rebase_session.cursor, 1);
    }

    #[test]
    fn test_rebase_load_todo() {
        let mut state = create_test_state();
        let lines = vec![
            "pick abc123 First commit".to_string(),
            "reword def456 Second commit".to_string(),
            "drop ghi789 Third commit".to_string(),
        ];

        let action = Action::RebaseLoadTodo(lines);
        let result = handle_rebase(state, &action);

        assert!(result.is_some());
        let new_state = result.unwrap();
        assert_eq!(new_state.rebase_session.entries.len(), 3);
        assert_eq!(new_state.rebase_session.entries[0].action, crate::app::rebase::RebaseAction::Pick);
        assert_eq!(new_state.rebase_session.entries[1].action, crate::app::rebase::RebaseAction::Reword);
        assert_eq!(new_state.rebase_session.entries[2].action, crate::app::rebase::RebaseAction::Drop);
        assert!(new_state.rebase_editor_open);
    }

    #[test]
    fn test_rebase_cancel() {
        let mut state = create_test_state();
        state.rebase_editor_open = true;
        state.rebase_session.entries = vec![crate::app::rebase::RebaseEntry::new(
            "abc123".to_string(),
            "Test".to_string(),
            crate::app::rebase::RebaseAction::Pick,
        )];
        state.rebase_session.dirty = true;

        let action = Action::RebaseCancel;
        let result = handle_rebase(state, &action);

        assert!(result.is_some());
        let new_state = result.unwrap();
        assert!(!new_state.rebase_editor_open);
        assert!(new_state.rebase_session.entries.is_empty());
        assert!(!new_state.rebase_dirty);
    }

    #[test]
    fn test_rebase_navigation() {
        let mut state = create_test_state();
        state.rebase_session.entries = vec![
            crate::app::rebase::RebaseEntry::new(
                "abc123".to_string(),
                "First".to_string(),
                crate::app::rebase::RebaseAction::Pick,
            ),
            crate::app::rebase::RebaseEntry::new(
                "def456".to_string(),
                "Second".to_string(),
                crate::app::rebase::RebaseAction::Pick,
            ),
        ];
        state.rebase_session.cursor = 0;

        // Move next
        let action = Action::RebaseNext;
        let result = handle_rebase(state, &action);
        assert!(result.is_some());
        let new_state = result.unwrap();
        assert_eq!(new_state.rebase_session.cursor, 1);

        // Move prev
        let state2 = new_state;
        let action2 = Action::RebasePrev;
        let result2 = handle_rebase(state2, &action2);
        assert!(result2.is_some());
        let new_state2 = result2.unwrap();
        assert_eq!(new_state2.rebase_session.cursor, 0);
    }

    #[test]
    fn test_rebase_recovery_actions() {
        let mut state = create_test_state();
        state.rebase_recovery_open = false;

        // Test ShowRebaseRecovery
        let action = Action::ShowRebaseRecovery;
        let result = handle_rebase(state, &action);
        assert!(result.is_some());
        let new_state = result.unwrap();
        assert!(new_state.rebase_recovery_open);

        // Test HideRebaseRecovery
        let state2 = new_state;
        let action2 = Action::HideRebaseRecovery;
        let result2 = handle_rebase(state2, &action2);
        assert!(result2.is_some());
        let new_state2 = result2.unwrap();
        assert!(!new_state2.rebase_recovery_open);
    }
}