eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::{Action, AppState};
use crate::commands::{
    Command, ApplyStashCommand, CheckoutBranchCommand, MergeBranchCommand,
    PopStashCommand, RebaseBranchCommand, StageFilesCommand, UnstageFilesCommand,
    PushCommand, CherryPickCommand, RevertCommand, ResetCommand,
    ListMergedBranchesCommand, PruneMergedBranchesCommand, RemotePruneCommand, ShowRemoteUrlCommand, SetRemoteSshCommand, LogBriefCommand, LogStatsCommand,
    PullCommand, PullRebaseCommand, PullMergeCommand, FetchAllPruneCommand, OpenInEditorCommand, CheckMergeStatusCommand, AutoFetchCommand,
    StageHunkCommand, UnstageHunkCommand, StageLinesCommand, UnstageLinesCommand,
    PrListCommand, PrCheckoutCommand, PrOpenCommand, CreateBranchCommand,
    CreateStashCommand, DeleteStashCommand,     RebaseSaveAndRunCommand,
    RebaseContinueCommand, RebaseSkipCommand, RebaseAbortCommand,
    RebaseResolveConflictsCommand, RebaseAbortFromConflictCommand,
    RebaseContinueInterruptedCommand, RebaseAbortInterruptedCommand,
};
use crate::commands::custom::CustomCommand;
use crate::git::parsers::diff;
use super::helpers;

/// Converts an action into a command for execution.
pub fn action_to_command(state: &AppState, action: &Action) -> Option<Box<dyn Command>> {
    match action {
        Action::StageSelectedFile => {
            helpers::create_stage_command(state, true)
        }
        Action::UnstageSelectedFile => {
            helpers::create_stage_command(state, false)
        }
        Action::StageAllFiles => {
            let paths: Vec<String> = state
                .status_entries
                .iter()
                .filter(|e| e.unstaged || (!e.staged && !e.unstaged)) // unstaged or untracked
                .map(|e| e.path.clone())
                .collect();
            if paths.is_empty() {
                None
            } else {
                Some(Box::new(StageFilesCommand { paths }))
            }
        }
        Action::UnstageAllFiles => {
            let paths: Vec<String> = state
                .status_entries
                .iter()
                .filter(|e| e.staged)
                .map(|e| e.path.clone())
                .collect();
            if paths.is_empty() {
                None
            } else {
                Some(Box::new(UnstageFilesCommand { paths }))
            }
        }
        Action::CommitSubmit => {
            helpers::create_commit_command(state, state.amend_mode)
        }
        Action::AmendCommit => {
            None
        }
        Action::Push => {
            Some(Box::new(PushCommand {
                force_with_lease: !state.push_ff_only_enforce,
            }))
        }
        Action::ForcePushAfterAmend => {
            Some(Box::new(PushCommand {
                force_with_lease: true,
            }))
        }
        Action::CherryPickSelected => {
            let hash = state
                .commits
                .get(state.commit_selected)
                .map(|c| c.hash.clone());
            hash.map(|h| Box::new(CherryPickCommand { hash: h }) as Box<dyn Command>)
        }
        Action::RevertSelected => {
            let hash = state
                .commits
                .get(state.commit_selected)
                .map(|c| c.hash.clone());
            hash.map(|h| Box::new(RevertCommand { hash: h }) as Box<dyn Command>)
        }
        Action::CheckAutoFetch => Some(Box::new(AutoFetchCommand {
            remote: state.auto_fetch_remote.clone(),
        })),
        Action::ShowPrHelper => Some(Box::new(PrListCommand {
            remote: state.auto_fetch_remote.clone(),
        })),
        Action::CheckoutPr(pr) => Some(Box::new(PrCheckoutCommand {
            remote: state.auto_fetch_remote.clone(),
            pr: pr.clone(),
        })),
        Action::OpenPrInBrowser(pr) => Some(Box::new(PrOpenCommand {
            remote: state.auto_fetch_remote.clone(),
            pr: pr.clone(),
        })),
        Action::ResetSoft => Some(Box::new(ResetCommand {
            mode: "--soft",
            target: "HEAD~1".to_string(),
        })),
        Action::ResetMixed => Some(Box::new(ResetCommand {
            mode: "--mixed",
            target: "HEAD~1".to_string(),
        })),
        Action::ResetHard => Some(Box::new(ResetCommand {
            mode: "--hard",
            target: "HEAD~1".to_string(),
        })),
        Action::ListMergedBranches => Some(Box::new(ListMergedBranchesCommand {
            base: state.merge_base_branch.clone(),
        })),
        Action::PruneMergedBranches => Some(Box::new(PruneMergedBranchesCommand {
            base: state.merge_base_branch.clone(),
        })),
        Action::RemotePrune(remote) => Some(Box::new(RemotePruneCommand {
            remote: remote.clone(),
        })),
        Action::ShowRemoteUrl(remote) => Some(Box::new(ShowRemoteUrlCommand {
            remote: remote.clone(),
        })),
        Action::SetRemoteSsh(remote) => Some(Box::new(SetRemoteSshCommand {
            remote: remote.clone(),
        })),
        Action::ShowLogBrief => Some(Box::new(LogBriefCommand { limit: 5 })),
        Action::ShowLogStats => Some(Box::new(LogStatsCommand { limit: 5 })),
        Action::Pull => Some(Box::new(PullCommand {
            ff_only: state.pull_ff_only,
            timeout_secs: state.pull_timeout_secs,
        })),
        Action::PullRebase => Some(Box::new(PullRebaseCommand {
            autostash: state.pull_autostash,
            timeout_secs: state.rebase_timeout_secs,
        })),
        Action::PullMerge => Some(Box::new(PullMergeCommand {
            timeout_secs: state.pull_timeout_secs,
        })),
        Action::FetchAllPrune => Some(Box::new(FetchAllPruneCommand)),
        Action::OpenInEditor => {
            if state.status_selected_path.is_empty() {
                None
            } else {
                Some(Box::new(OpenInEditorCommand {
                    path: state.status_selected_path.clone(),
                    line: None,
                }))
            }
        }
        Action::OpenConflictInEditor(path) => {
            Some(Box::new(OpenInEditorCommand {
                path: path.clone(),
                line: None,
            }))
        }
        Action::CheckMergeStatus => Some(Box::new(CheckMergeStatusCommand {
            base: state.merge_base_branch.clone(),
        })),
        Action::ShowMergeBasePicker
        | Action::HideMergeBasePicker
        | Action::MergeBaseUp
        | Action::MergeBaseDown
        | Action::TogglePushFFOnly => None,
        Action::ApplyMergeBase(name) => Some(Box::new(CheckMergeStatusCommand { base: name.clone() })),
        Action::StageCurrentHunk => {
            let parsed = diff::parse_diff(&state.last_diff).ok()?;
            let file = parsed.files.first()?;
            let idx = state.hunk_selected.min(file.hunks.len().saturating_sub(1));
            Some(Box::new(StageHunkCommand {
                hunk_index: idx,
                parsed,
                path: state.last_diff_path.clone(),
                raw_diff: state.last_diff.clone(),
            }))
        }
        Action::UnstageCurrentHunk => {
            let parsed = diff::parse_diff(&state.last_diff).ok()?;
            let file = parsed.files.first()?;
            let idx = state.hunk_selected.min(file.hunks.len().saturating_sub(1));
            Some(Box::new(UnstageHunkCommand {
                hunk_index: idx,
                parsed,
                path: state.last_diff_path.clone(),
                raw_diff: state.last_diff.clone(),
            }))
        }
        Action::StageSelectedLines => {
            if state.selected_lines.is_empty() {
                None
            } else {
                Some(Box::new(StageLinesCommand {
                    raw_diff: state.last_diff.clone(),
                    path: state.last_diff_path.clone(),
                    selected_lines: state.selected_lines.clone(),
                }))
            }
        }
        Action::UnstageSelectedLines => {
            if state.selected_lines.is_empty() {
                None
            } else {
                Some(Box::new(UnstageLinesCommand {
                    raw_diff: state.last_diff.clone(),
                    path: state.last_diff_path.clone(),
                    selected_lines: state.selected_lines.clone(),
                }))
            }
        }
        Action::CheckoutBranch(branch_name) => {
            Some(Box::new(CheckoutBranchCommand { branch_name: branch_name.clone() }))
        }
        Action::MergeBranch(branch_name) => {
            Some(Box::new(MergeBranchCommand { branch_name: branch_name.clone() }))
        }
        Action::RebaseBranch(branch_name) => {
            Some(Box::new(RebaseBranchCommand { branch_name: branch_name.clone() }))
        }
        Action::ApplyStash(stash_name) => {
            Some(Box::new(ApplyStashCommand { name: stash_name.clone() }))
        }
        Action::PopStash(stash_name) => {
            Some(Box::new(PopStashCommand { name: stash_name.clone() }))
        }
        Action::DeleteStash(stash_name) => {
            Some(Box::new(DeleteStashCommand { name: stash_name.clone() }))
        }
        Action::BranchSubmit => {
            Some(Box::new(CreateBranchCommand {
                branch_name: state.branch_name_input.clone(),
            }))
        }
        Action::StashSubmit => {
            Some(Box::new(CreateStashCommand {
                message: state.stash_message_input.clone(),
            }))
        }
        Action::ExecuteCustomCommand(name) => {
            if let Some(command) = state.custom_commands.get(name.as_str()) {
                Some(Box::new(CustomCommand {
                    name: name.clone(),
                    command: command.clone(),
                }))
            } else {
                None
            }
        }
        Action::RebaseSaveAndRun => {
            Some(Box::new(RebaseSaveAndRunCommand))
        }
        Action::RebaseContinue => {
            Some(Box::new(RebaseContinueCommand))
        }
        Action::RebaseSkip => {
            Some(Box::new(RebaseSkipCommand))
        }
        Action::RebaseAbort => {
            Some(Box::new(RebaseAbortCommand))
        }
        Action::RebaseResolveConflicts => {
            Some(Box::new(RebaseResolveConflictsCommand))
        }
        Action::RebaseAbortFromConflict => {
            Some(Box::new(RebaseAbortFromConflictCommand))
        }
        Action::RebaseContinueInterrupted => {
            Some(Box::new(RebaseContinueInterruptedCommand))
        }
        Action::RebaseAbortInterrupted => {
            Some(Box::new(RebaseAbortInterruptedCommand))
        }
        _ => None,
    }
}