eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Core data structures for the unified rebase system.
//!
//! This replaces the broken dual-state system with a single source of truth.

use std::path::PathBuf;

/// A single entry in the rebase todo list.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RebaseEntry {
    /// Full commit hash
    pub hash: String,
    /// Short hash (7 characters) for display
    pub short_hash: String,
    /// Commit message
    pub message: String,
    /// Action to perform on this commit
    pub action: RebaseAction,
}

impl RebaseEntry {
    /// Create a new rebase entry
    pub fn new(hash: String, message: String, action: RebaseAction) -> Self {
        let short_hash = hash.chars().take(7).collect();
        Self {
            hash,
            short_hash,
            message,
            action,
        }
    }
}

/// Actions that can be performed on a commit during rebase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RebaseAction {
    /// Use commit as-is
    Pick,
    /// Use commit but edit the message
    Reword,
    /// Use commit but stop to edit files
    Edit,
    /// Use commit but meld into previous commit
    Squash,
    /// Like squash but discard this commit's message
    Fixup,
    /// Remove this commit entirely
    Drop,
}

impl RebaseAction {
    /// Convert to git-rebase-todo format string
    pub fn as_str(&self) -> &'static str {
        match self {
            RebaseAction::Pick => "pick",
            RebaseAction::Reword => "reword",
            RebaseAction::Edit => "edit",
            RebaseAction::Squash => "squash",
            RebaseAction::Fixup => "fixup",
            RebaseAction::Drop => "drop",
        }
    }

    /// Parse from git-rebase-todo format string
    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "pick" => Some(RebaseAction::Pick),
            "reword" => Some(RebaseAction::Reword),
            "edit" => Some(RebaseAction::Edit),
            "squash" => Some(RebaseAction::Squash),
            "fixup" => Some(RebaseAction::Fixup),
            "drop" => Some(RebaseAction::Drop),
            _ => None,
        }
    }

    /// Cycle to the next action in the sequence
    pub fn cycle_next(&self) -> Self {
        match self {
            RebaseAction::Pick => RebaseAction::Reword,
            RebaseAction::Reword => RebaseAction::Edit,
            RebaseAction::Edit => RebaseAction::Squash,
            RebaseAction::Squash => RebaseAction::Fixup,
            RebaseAction::Fixup => RebaseAction::Drop,
            RebaseAction::Drop => RebaseAction::Pick,
        }
    }
}

/// Phases of the rebase process.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RebasePhase {
    /// Building a new rebase interactively (planning phase)
    Planning,
    /// Rebase is actively running (can edit todo)
    Active,
    /// Conflicts detected, resolution needed
    Conflict,
    /// Loading interrupted rebase on startup
    Recovery,
}

/// Complete rebase session state (single source of truth).
#[derive(Debug, Clone)]
pub struct RebaseSession {
    /// Current phase of the rebase
    pub phase: RebasePhase,
    /// List of commits to rebase
    pub entries: Vec<RebaseEntry>,
    /// Current cursor position in the UI
    pub cursor: usize,
    /// Path to the current git-rebase-todo file (if any)
    pub todo_path: Option<PathBuf>,
    /// Base commit hash (what we're rebasing onto)
    pub base_commit: Option<String>,
    /// Whether to use --root flag
    pub use_root: bool,
    /// Whether there are unsaved changes
    pub dirty: bool,
}

impl Default for RebaseSession {
    fn default() -> Self {
        Self {
            phase: RebasePhase::Planning,
            entries: Vec::new(),
            cursor: 0,
            todo_path: None,
            base_commit: None,
            use_root: false,
            dirty: false,
        }
    }
}

impl RebaseSession {
    /// Create a new rebase session
    pub fn new() -> Self {
        Self::default()
    }

    /// Check if the session is in a valid state
    pub fn is_valid(&self) -> bool {
        match self.phase {
            RebasePhase::Planning => {
                // Planning phase: can be empty, but if has entries, must have valid data
                self.entries.iter().all(|entry| {
                    !entry.hash.is_empty() && !entry.short_hash.is_empty()
                })
            }
            RebasePhase::Active | RebasePhase::Conflict => {
                // Active phases: must have todo_path and at least one entry
                self.todo_path.is_some() && !self.entries.is_empty()
            }
            RebasePhase::Recovery => {
                // Recovery: similar to active but we're loading from disk
                true // Validation happens during recovery
            }
        }
    }

    /// Get the current entry under the cursor
    pub fn current_entry(&self) -> Option<&RebaseEntry> {
        self.entries.get(self.cursor)
    }

    /// Move cursor up (with bounds checking)
    pub fn move_cursor_up(&mut self) {
        if self.cursor > 0 {
            self.cursor -= 1;
        }
    }

    /// Move cursor down (with bounds checking)
    pub fn move_cursor_down(&mut self) {
        if self.cursor + 1 < self.entries.len() {
            self.cursor += 1;
        }
    }

    /// Move the current entry up in the list
    pub fn move_entry_up(&mut self) {
        if self.cursor > 0 {
            self.entries.swap(self.cursor, self.cursor - 1);
            self.cursor -= 1;
            self.dirty = true;
        }
    }

    /// Move the current entry down in the list
    pub fn move_entry_down(&mut self) {
        if self.cursor + 1 < self.entries.len() {
            self.entries.swap(self.cursor, self.cursor + 1);
            self.cursor += 1;
            self.dirty = true;
        }
    }
}