eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Push and pull commands.
//!
//! Handles pushing changes and pulling updates from remote repositories.

use crate::commands::{Command, CommandResult};
use crate::services::GitService;
use crate::app::{AppState, Action, reducer};
use crate::errors::CommandError;
use tracing::instrument;

/// Push current branch
pub struct PushCommand {
    pub force_with_lease: bool,
}

impl Command for PushCommand {
    #[instrument(skip(self, git, state), fields(force = self.force_with_lease))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some("pushing…".into())));
        if state.push_ff_only_enforce {
            if let Ok((_ahead, behind)) = git.ahead_behind_remote(&state.repo_path, &state.merge_base_branch) {
                if behind > 0 {
                    new_state = reducer(new_state, Action::AppendOpLog(format!("push blocked: behind {} on {}", behind, state.merge_base_branch)));
                    new_state = reducer(new_state, Action::SetStatusError(Some(format!("push blocked: behind {} on {}", behind, state.merge_base_branch))));
                    new_state = reducer(new_state, Action::SetOpStatus(None));
                    return Ok(CommandResult::StateUpdate(new_state));
                }
            }
        }
        match git.push(&state.repo_path, self.force_with_lease) {
            Ok(_) => {
                new_state = reducer(new_state, Action::SetFeedback(Some("Pushed".into())));
                new_state = reducer(new_state, Action::AppendOpLog("push ok".into()));
                // Fetch remote refs to update what we see locally
                if let Err(e) = git.fetch_all_prune(&state.repo_path) {
                    tracing::warn!(error = %e, "Failed to fetch after push");
                    // Don't fail the push if fetch fails
                }
                new_state = reducer(new_state, Action::SetRefreshing(true));
                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after push
            }
            Err(e) => {
                let error_msg = format!("{e}");
                // Check if error suggests force push is needed
                let needs_force = error_msg.contains("non-fast-forward") 
                    || error_msg.contains("failed to push") 
                    || error_msg.contains("rejected");
                let full_error = if needs_force && !self.force_with_lease {
                    format!("push error: {e}\nTip: Use force push (fP or :force push) after amending commits")
                } else {
                    format!("push error: {e}")
                };
                new_state = reducer(new_state, Action::AppendOpLog(format!("push error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(full_error)));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Pull (ff-only optional)
pub struct PullCommand {
    pub ff_only: bool,
    pub timeout_secs: u64,
}

impl Command for PullCommand {
    #[instrument(skip(self, git, state), fields(ff_only = self.ff_only))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some(if self.ff_only { "pull ff-only…" } else { "pull…" }.into())));
        match git.pull_ff_only(&state.repo_path, self.ff_only, self.timeout_secs) {
            Ok(_) => {
                new_state = reducer(new_state, Action::SetFeedback(Some("Pulled".into())));
                new_state = reducer(new_state, Action::AppendOpLog("pull ok".into()));
                new_state = reducer(new_state, Action::SetRefreshing(true));
                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after pull
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("pull error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("pull error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Pull with rebase (autostash optional)
pub struct PullRebaseCommand {
    pub autostash: bool,
    pub timeout_secs: u64,
}

impl Command for PullRebaseCommand {
    #[instrument(skip(self, git, state), fields(autostash = self.autostash))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some(if self.autostash { "pull --rebase --autostash…" } else { "pull --rebase…" }.into())));
        match git.pull_rebase(&state.repo_path, self.autostash, self.timeout_secs) {
            Ok(_) => {
                new_state = reducer(new_state, Action::SetFeedback(Some("Pull rebase ok".into())));
                new_state = reducer(new_state, Action::AppendOpLog("pull --rebase ok".into()));
                new_state = reducer(new_state, Action::SetRefreshing(true));
                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after pull
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("pull --rebase error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("pull --rebase error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Pull with merge (regular pull, no --ff-only or --rebase)
pub struct PullMergeCommand {
    pub timeout_secs: u64,
}

impl Command for PullMergeCommand {
    #[instrument(skip(self, git, state))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some("pull…".into())));
        match git.pull(&state.repo_path, self.timeout_secs) {
            Ok(_) => {
                new_state = reducer(new_state, Action::SetFeedback(Some("Pulled".into())));
                new_state = reducer(new_state, Action::AppendOpLog("pull ok".into()));
                new_state = reducer(new_state, Action::SetRefreshing(true));
                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after pull
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("pull error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("pull error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Fetch all remotes with prune
pub struct FetchAllPruneCommand;

impl Command for FetchAllPruneCommand {
    #[instrument(skip(self, git, state))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some("fetching all remotes (prune)…".into())));
        match git.fetch_all_prune(&state.repo_path) {
            Ok(_) => {
                new_state = reducer(new_state, Action::SetFeedback(Some("Fetched all remotes".into())));
                new_state = reducer(new_state, Action::AppendOpLog("fetch --all --prune ok".into()));
                new_state = reducer(new_state, Action::SetRefreshing(true));
                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after fetch
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("fetch --all --prune error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("fetch error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}