eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::{Action, AppState};
use std::path::PathBuf;
use super::types::{WorkflowState, WorkflowStep, WorkflowProgress};

/// Workflow context with state and suggestions
#[derive(Debug, Clone)]
pub struct WorkflowContext {
    pub state: WorkflowState,
    pub message: String,
    pub next_steps: Vec<WorkflowStep>,
    pub progress: Option<WorkflowProgress>,
}

impl WorkflowContext {
    /// Helper to create a workflow step with consistent formatting
    #[inline]
    fn step(key: &str, label: &str, action: Option<Action>, description: &str) -> WorkflowStep {
        WorkflowStep {
            key: key.to_string(),
            label: label.to_string(),
            action,
            description: description.to_string(),
        }
    }
    
    /// Create common conflict resolution steps
    fn conflict_resolution_steps() -> Vec<WorkflowStep> {
        vec![
            Self::step(
                "g",
                "Open guided resolution",
                Some(Action::ShowConflictsGuided),
                "Get step-by-step help resolving conflicts",
            ),
        ]
    }
    
    /// Create common commit step
    fn commit_step(label: &str, description: &str) -> WorkflowStep {
        Self::step("c", label, Some(Action::StartCommit), description)
    }
    
    /// Detect current workflow state from AppState
    pub fn detect(state: &AppState) -> Self {
        let repo_path = PathBuf::from(&state.repo_path);
        
        // Check for rebase in progress
        if repo_path.join(".git/rebase-merge").exists() 
            || repo_path.join(".git/rebase-apply").exists() {
            return Self::rebase_context(state);
        }
        
        // Check for merge in progress
        if repo_path.join(".git/MERGE_HEAD").exists() {
            return Self::merge_context(state);
        }
        
        // Check for cherry-pick in progress
        if repo_path.join(".git/CHERRY_PICK_HEAD").exists() {
            return Self::cherry_pick_context(state);
        }
        
        // Check for revert in progress
        if repo_path.join(".git/REVERT_HEAD").exists() {
            return Self::revert_context(state);
        }
        
        // Check for conflicts
        let has_conflicts = state.status_entries.iter().any(|e| e.conflict);
        if has_conflicts {
            return Self::conflicts_context(state);
        }
        
        // Check if committing
        if state.commit_mode {
            return Self::committing_context(state);
        }
        
        // Check if staging
        let has_staged = state.status_entries.iter().any(|e| e.staged);
        let has_unstaged = state.status_entries.iter().any(|e| e.unstaged);
        if has_staged || has_unstaged {
            return Self::staging_context(state);
        }
        
        // Default: clean state
        Self::clean_context(state)
    }

    /// Stubbed - rebase feature removed
    fn rebase_context(_state: &AppState) -> Self {
        Self {
            state: WorkflowState::RebaseInProgress,
            message: "Rebase feature removed".to_string(),
            next_steps: vec![],
            progress: None,
        }
    }

    fn merge_context(state: &AppState) -> Self {
        let has_conflicts = state.status_entries.iter().any(|e| e.conflict);
        
        if has_conflicts {
            let mut steps = Self::conflict_resolution_steps();
            steps.push(Self::step(
                "o",
                "Open conflicts in editor",
                None,
                "Edit conflict files manually",
            ));
            Self {
                state: WorkflowState::MergeInProgress,
                message: "Merge in progress - conflicts detected".to_string(),
                next_steps: steps,
                progress: None,
            }
        } else {
            Self {
                state: WorkflowState::MergeInProgress,
                message: "Merge in progress - ready to commit".to_string(),
                next_steps: vec![Self::commit_step(
                    "Commit merge",
                    "Complete the merge by committing",
                )],
                progress: None,
            }
        }
    }
    
    fn cherry_pick_context(state: &AppState) -> Self {
        let has_conflicts = state.status_entries.iter().any(|e| e.conflict);
        
        if has_conflicts {
            let mut steps = Self::conflict_resolution_steps();
            steps.push(Self::commit_step(
                "Commit after resolving",
                "Complete the cherry-pick after resolving conflicts",
            ));
            Self {
                state: WorkflowState::CherryPickInProgress,
                message: "Cherry-pick in progress - conflicts detected".to_string(),
                next_steps: steps,
                progress: None,
            }
        } else {
            Self {
                state: WorkflowState::CherryPickInProgress,
                message: "Cherry-pick in progress - ready to commit".to_string(),
                next_steps: vec![Self::commit_step(
                    "Commit cherry-pick",
                    "Complete the cherry-pick",
                )],
                progress: None,
            }
        }
    }
    
    fn revert_context(_state: &AppState) -> Self {
        Self {
            state: WorkflowState::RevertInProgress,
            message: "Revert in progress".to_string(),
            next_steps: vec![Self::commit_step(
                "Commit revert",
                "Complete the revert",
            )],
            progress: None,
        }
    }
    
    fn conflicts_context(state: &AppState) -> Self {
        let conflict_count = state.status_entries.iter().filter(|e| e.conflict).count();
        
        let mut steps = Self::conflict_resolution_steps();
        steps.push(Self::step(
            "n",
            "Next conflict",
            Some(Action::StatusNextConflict),
            "Navigate to next conflict",
        ));
        steps.push(Self::step(
            "p",
            "Previous conflict",
            Some(Action::StatusPrevConflict),
            "Navigate to previous conflict",
        ));
        
        Self {
            state: WorkflowState::Conflicts,
            message: format!("{} conflict(s) detected", conflict_count),
            next_steps: steps,
            progress: None,
        }
    }
    
    fn committing_context(_state: &AppState) -> Self {
        Self {
            state: WorkflowState::Committing,
            message: "Entering commit message".to_string(),
            next_steps: vec![
                Self::step(
                    "Enter",
                    "Commit",
                    Some(Action::CommitSubmit),
                    "Commit with the entered message",
                ),
                Self::step(
                    "Esc",
                    "Cancel",
                    Some(Action::CancelCommit),
                    "Cancel commit and return",
                ),
            ],
            progress: None,
        }
    }
    
    fn staging_context(state: &AppState) -> Self {
        let staged_count = state.status_entries.iter().filter(|e| e.staged).count();
        let unstaged_count = state.status_entries.iter().filter(|e| e.unstaged).count();
        
        let message = if staged_count > 0 && unstaged_count > 0 {
            format!("{} staged, {} unstaged changes", staged_count, unstaged_count)
        } else if staged_count > 0 {
            format!("{} staged change(s) ready to commit", staged_count)
        } else {
            format!("{} unstaged change(s)", unstaged_count)
        };
        
        let mut steps = vec![];
        
        if unstaged_count > 0 {
            steps.push(Self::step(
                "s",
                "Stage selected",
                Some(Action::StageSelectedFile),
                "Stage the selected file",
            ));
            steps.push(Self::step(
                "S",
                "Stage all",
                Some(Action::StageAllFiles),
                "Stage all unstaged files",
            ));
        }
        
        if staged_count > 0 {
            steps.push(Self::commit_step(
                "Commit",
                "Commit staged changes",
            ));
        }
        
        Self {
            state: WorkflowState::Staging,
            message,
            next_steps: steps,
            progress: None,
        }
    }
    
    fn clean_context(_state: &AppState) -> Self {
        Self {
            state: WorkflowState::Clean,
            message: "Working directory clean".to_string(),
            next_steps: vec![
                Self::step(
                    "b",
                    "View branches",
                    Some(Action::FocusNext),
                    "Switch branches or create new ones",
                ),
                Self::step(
                    "l",
                    "View log",
                    Some(Action::FocusNext),
                    "Browse commit history",
                ),
                Self::step(
                    "P",
                    "Push",
                    Some(Action::Push),
                    "Push commits to remote",
                ),
            ],
            progress: None,
        }
    }
    
    /// Format workflow context for display
    pub fn format_display(&self) -> String {
        let mut lines = vec![self.message.clone()];
        
        if let Some(ref progress) = self.progress {
            lines.push(format!("Progress: {}/{} - {}", progress.current, progress.total, progress.step_name));
        }
        
        if !self.next_steps.is_empty() {
            lines.push(String::new());
            lines.push("Next steps:".to_string());
            for step in &self.next_steps {
                lines.push(format!("  [{}] {} - {}", step.key, step.label, step.description));
            }
        }
        
        lines.join("\n")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::git::parsers::status::StatusEntry;

    #[test]
    fn test_detect_clean_state() {
        let state = AppState::new();
        // Default state is clean
        let ctx = WorkflowContext::detect(&state);
        assert_eq!(ctx.state, WorkflowState::Clean);
        assert!(ctx.message.contains("clean"));
    }
    
    #[test]
    fn test_detect_staging_state() {
        let mut state = AppState::new();
        // We need to simulate StatusEntry struct which might be in crate::app::state::types or structs
        // Checked StatusEntry usage in code block above: state.status_entries.iter()
        
        state.status_entries.push(StatusEntry {
            path: "file.txt".to_string(),
            staged: false,
            unstaged: true,
            conflict: false,
        });
        
        let ctx = WorkflowContext::detect(&state);
        assert_eq!(ctx.state, WorkflowState::Staging);
        assert!(ctx.message.contains("unstaged"));
    }
    
    #[test]
    fn test_detect_committing_state() {
        let mut state = AppState::new();
        state.commit_mode = true;
        
        let ctx = WorkflowContext::detect(&state);
        assert_eq!(ctx.state, WorkflowState::Committing);
        assert_eq!(ctx.next_steps[0].key, "Enter");
    }
}