eazygit/commands/git_commands/
push_pull.rs

1//! Push and pull commands.
2//!
3//! Handles pushing changes and pulling updates from remote repositories.
4
5use crate::commands::{Command, CommandResult};
6use crate::services::GitService;
7use crate::app::{AppState, Action, reducer};
8use crate::errors::CommandError;
9use tracing::instrument;
10
11/// Push current branch
12pub struct PushCommand {
13    pub force_with_lease: bool,
14}
15
16impl Command for PushCommand {
17    #[instrument(skip(self, git, state), fields(force = self.force_with_lease))]
18    fn execute(
19        &self,
20        git: &GitService,
21        state: &AppState,
22    ) -> Result<CommandResult, CommandError> {
23        let mut new_state = state.clone();
24        new_state = reducer(new_state, Action::SetOpStatus(Some("pushing…".into())));
25        if state.push_ff_only_enforce {
26            if let Ok((_ahead, behind)) = git.ahead_behind_remote(&state.repo_path, &state.merge_base_branch) {
27                if behind > 0 {
28                    new_state = reducer(new_state, Action::AppendOpLog(format!("push blocked: behind {} on {}", behind, state.merge_base_branch)));
29                    new_state = reducer(new_state, Action::SetStatusError(Some(format!("push blocked: behind {} on {}", behind, state.merge_base_branch))));
30                    new_state = reducer(new_state, Action::SetOpStatus(None));
31                    return Ok(CommandResult::StateUpdate(new_state));
32                }
33            }
34        }
35        match git.push(&state.repo_path, self.force_with_lease) {
36            Ok(_) => {
37                new_state = reducer(new_state, Action::SetFeedback(Some("Pushed".into())));
38                new_state = reducer(new_state, Action::AppendOpLog("push ok".into()));
39                // Fetch remote refs to update what we see locally
40                if let Err(e) = git.fetch_all_prune(&state.repo_path) {
41                    tracing::warn!(error = %e, "Failed to fetch after push");
42                    // Don't fail the push if fetch fails
43                }
44                new_state = reducer(new_state, Action::SetRefreshing(true));
45                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after push
46            }
47            Err(e) => {
48                let error_msg = format!("{e}");
49                // Check if error suggests force push is needed
50                let needs_force = error_msg.contains("non-fast-forward") 
51                    || error_msg.contains("failed to push") 
52                    || error_msg.contains("rejected");
53                let full_error = if needs_force && !self.force_with_lease {
54                    format!("push error: {e}\nTip: Use force push (fP or :force push) after amending commits")
55                } else {
56                    format!("push error: {e}")
57                };
58                new_state = reducer(new_state, Action::AppendOpLog(format!("push error: {e}")));
59                new_state = reducer(new_state, Action::SetStatusError(Some(full_error)));
60            }
61        }
62        new_state = reducer(new_state, Action::SetOpStatus(None));
63        Ok(CommandResult::StateUpdate(new_state))
64    }
65}
66
67/// Pull (ff-only optional)
68pub struct PullCommand {
69    pub ff_only: bool,
70    pub timeout_secs: u64,
71}
72
73impl Command for PullCommand {
74    #[instrument(skip(self, git, state), fields(ff_only = self.ff_only))]
75    fn execute(
76        &self,
77        git: &GitService,
78        state: &AppState,
79    ) -> Result<CommandResult, CommandError> {
80        let mut new_state = state.clone();
81        new_state = reducer(new_state, Action::SetOpStatus(Some(if self.ff_only { "pull ff-only…" } else { "pull…" }.into())));
82        match git.pull_ff_only(&state.repo_path, self.ff_only, self.timeout_secs) {
83            Ok(_) => {
84                new_state = reducer(new_state, Action::SetFeedback(Some("Pulled".into())));
85                new_state = reducer(new_state, Action::AppendOpLog("pull ok".into()));
86                new_state = reducer(new_state, Action::SetRefreshing(true));
87                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after pull
88            }
89            Err(e) => {
90                new_state = reducer(new_state, Action::AppendOpLog(format!("pull error: {e}")));
91                new_state = reducer(new_state, Action::SetStatusError(Some(format!("pull error: {e}"))));
92            }
93        }
94        new_state = reducer(new_state, Action::SetOpStatus(None));
95        Ok(CommandResult::StateUpdate(new_state))
96    }
97}
98
99/// Pull with rebase (autostash optional)
100pub struct PullRebaseCommand {
101    pub autostash: bool,
102    pub timeout_secs: u64,
103}
104
105impl Command for PullRebaseCommand {
106    #[instrument(skip(self, git, state), fields(autostash = self.autostash))]
107    fn execute(
108        &self,
109        git: &GitService,
110        state: &AppState,
111    ) -> Result<CommandResult, CommandError> {
112        let mut new_state = state.clone();
113        new_state = reducer(new_state, Action::SetOpStatus(Some(if self.autostash { "pull --rebase --autostash…" } else { "pull --rebase…" }.into())));
114        match git.pull_rebase(&state.repo_path, self.autostash, self.timeout_secs) {
115            Ok(_) => {
116                new_state = reducer(new_state, Action::SetFeedback(Some("Pull rebase ok".into())));
117                new_state = reducer(new_state, Action::AppendOpLog("pull --rebase ok".into()));
118                new_state = reducer(new_state, Action::SetRefreshing(true));
119                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after pull
120            }
121            Err(e) => {
122                new_state = reducer(new_state, Action::AppendOpLog(format!("pull --rebase error: {e}")));
123                new_state = reducer(new_state, Action::SetStatusError(Some(format!("pull --rebase error: {e}"))));
124            }
125        }
126        new_state = reducer(new_state, Action::SetOpStatus(None));
127        Ok(CommandResult::StateUpdate(new_state))
128    }
129}
130
131/// Pull with merge (regular pull, no --ff-only or --rebase)
132pub struct PullMergeCommand {
133    pub timeout_secs: u64,
134}
135
136impl Command for PullMergeCommand {
137    #[instrument(skip(self, git, state))]
138    fn execute(
139        &self,
140        git: &GitService,
141        state: &AppState,
142    ) -> Result<CommandResult, CommandError> {
143        let mut new_state = state.clone();
144        new_state = reducer(new_state, Action::SetOpStatus(Some("pull…".into())));
145        match git.pull(&state.repo_path, self.timeout_secs) {
146            Ok(_) => {
147                new_state = reducer(new_state, Action::SetFeedback(Some("Pulled".into())));
148                new_state = reducer(new_state, Action::AppendOpLog("pull ok".into()));
149                new_state = reducer(new_state, Action::SetRefreshing(true));
150                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after pull
151            }
152            Err(e) => {
153                new_state = reducer(new_state, Action::AppendOpLog(format!("pull error: {e}")));
154                new_state = reducer(new_state, Action::SetStatusError(Some(format!("pull error: {e}"))));
155            }
156        }
157        new_state = reducer(new_state, Action::SetOpStatus(None));
158        Ok(CommandResult::StateUpdate(new_state))
159    }
160}
161
162/// Fetch all remotes with prune
163pub struct FetchAllPruneCommand;
164
165impl Command for FetchAllPruneCommand {
166    #[instrument(skip(self, git, state))]
167    fn execute(
168        &self,
169        git: &GitService,
170        state: &AppState,
171    ) -> Result<CommandResult, CommandError> {
172        let mut new_state = state.clone();
173        new_state = reducer(new_state, Action::SetOpStatus(Some("fetching all remotes (prune)…".into())));
174        match git.fetch_all_prune(&state.repo_path) {
175            Ok(_) => {
176                new_state = reducer(new_state, Action::SetFeedback(Some("Fetched all remotes".into())));
177                new_state = reducer(new_state, Action::AppendOpLog("fetch --all --prune ok".into()));
178                new_state = reducer(new_state, Action::SetRefreshing(true));
179                new_state = reducer(new_state, Action::RefreshCommits); // Refresh commit list after fetch
180            }
181            Err(e) => {
182                new_state = reducer(new_state, Action::AppendOpLog(format!("fetch --all --prune error: {e}")));
183                new_state = reducer(new_state, Action::SetStatusError(Some(format!("fetch error: {e}"))));
184            }
185        }
186        new_state = reducer(new_state, Action::SetOpStatus(None));
187        Ok(CommandResult::StateUpdate(new_state))
188    }
189}
190