eazygit/commands/git_commands/
rebase.rs

1//! Rebase command implementations.
2
3use crate::commands::{Command, CommandResult};
4use crate::services::GitService;
5use crate::app::{AppState, Action, reducer};
6use crate::app::rebase::{RebaseOperations, operations::{RebaseResult, RebaseRecovery}};
7use crate::errors::CommandError;
8use tracing::instrument;
9
10#[cfg(test)]
11mod tests {
12    use super::*;
13    use crate::app::AppState;
14    use crate::services::GitService;
15    use crate::commands::CommandResult;
16    use std::sync::Arc;
17
18    fn create_test_state() -> AppState {
19        let mut state = AppState::new();
20        state.repo_path = ".".to_string();
21        state
22    }
23
24    fn create_mock_git_service() -> Arc<GitService> {
25        Arc::new(GitService::new())
26    }
27
28    #[test]
29    fn test_rebase_save_and_run_command_creation() {
30        let command = RebaseSaveAndRunCommand;
31        // Command struct is just a marker, no fields to test
32        assert!(true); // Placeholder test
33    }
34
35    #[test]
36    fn test_rebase_continue_command_creation() {
37        let command = RebaseContinueCommand;
38        assert!(true); // Placeholder test
39    }
40
41    #[test]
42    fn test_rebase_skip_command_creation() {
43        let command = RebaseSkipCommand;
44        assert!(true); // Placeholder test
45    }
46
47    #[test]
48    fn test_rebase_abort_command_creation() {
49        let command = RebaseAbortCommand;
50        assert!(true); // Placeholder test
51    }
52
53    #[test]
54    fn test_rebase_resolve_conflicts_command_creation() {
55        let command = RebaseResolveConflictsCommand;
56        assert!(true); // Placeholder test
57    }
58
59    #[test]
60    fn test_rebase_abort_from_conflict_command_creation() {
61        let command = RebaseAbortFromConflictCommand;
62        assert!(true); // Placeholder test
63    }
64
65    #[test]
66    fn test_rebase_continue_interrupted_command_creation() {
67        let command = RebaseContinueInterruptedCommand;
68        assert!(true); // Placeholder test
69    }
70
71    #[test]
72    fn test_rebase_abort_interrupted_command_creation() {
73        let command = RebaseAbortInterruptedCommand;
74        assert!(true); // Placeholder test
75    }
76}
77
78/// Command to save and run interactive rebase
79pub struct RebaseSaveAndRunCommand;
80
81/// Command to continue active rebase to next step
82pub struct RebaseContinueCommand;
83
84/// Command to skip current commit during active rebase
85pub struct RebaseSkipCommand;
86
87/// Command to abort current rebase
88pub struct RebaseAbortCommand;
89
90/// Command to resolve conflicts and continue rebase
91pub struct RebaseResolveConflictsCommand;
92
93/// Command to abort rebase from conflict state
94pub struct RebaseAbortFromConflictCommand;
95
96/// Command to continue interrupted rebase
97pub struct RebaseContinueInterruptedCommand;
98
99/// Command to abort interrupted rebase
100pub struct RebaseAbortInterruptedCommand;
101
102impl Command for RebaseSaveAndRunCommand {
103    #[instrument(skip(self, git, state))]
104    fn execute(
105        &self,
106        git: &GitService,
107        state: &AppState,
108    ) -> Result<CommandResult, CommandError> {
109        let mut new_state = state.clone();
110
111        // Execute rebase using the new operations system
112        match RebaseOperations::execute_rebase(git, &state.repo_path, &mut new_state.rebase_session) {
113            Ok(result) => {
114                match result {
115                    RebaseResult::Success => {
116                        // Rebase started successfully
117                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase started successfully".to_string())));
118                        new_state.rebase_editor_open = false;
119                    }
120                    RebaseResult::Conflicts { .. } => {
121                        // Conflicts detected
122                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase started but conflicts detected".to_string())));
123                        new_state.rebase_editor_open = false;
124                    }
125                    _ => {
126                        // Other results not expected during initial execution
127                        new_state = reducer(new_state, Action::SetStatusError(Some("Unexpected rebase result".to_string())));
128                    }
129                }
130                Ok(CommandResult::StateUpdate(new_state))
131            }
132            Err(e) => {
133                // Handle validation or execution errors
134                let error_msg = format!("Failed to start rebase: {}", e);
135                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
136                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
137            }
138        }
139    }
140}
141
142impl Command for RebaseContinueCommand {
143    #[instrument(skip(self, git, state))]
144    fn execute(
145        &self,
146        git: &GitService,
147        state: &AppState,
148    ) -> Result<CommandResult, CommandError> {
149        let mut new_state = state.clone();
150
151        match RebaseOperations::continue_rebase(git, &state.repo_path, &mut new_state.rebase_session) {
152            Ok(result) => {
153                match result {
154                    RebaseResult::Success => {
155                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase continued successfully".to_string())));
156                    }
157                    RebaseResult::Completed => {
158                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase completed successfully".to_string())));
159                        // Reset session to planning state
160                        new_state.rebase_session = crate::app::rebase::RebaseSession::default();
161                    }
162                    RebaseResult::Conflicts { .. } => {
163                        new_state = reducer(new_state, Action::SetFeedback(Some("Conflicts detected during continue".to_string())));
164                    }
165                    _ => {
166                        new_state = reducer(new_state, Action::SetStatusError(Some("Unexpected rebase result during continue".to_string())));
167                    }
168                }
169                Ok(CommandResult::StateUpdate(new_state))
170            }
171            Err(e) => {
172                let error_msg = format!("Failed to continue rebase: {}", e);
173                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
174                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
175            }
176        }
177    }
178}
179
180impl Command for RebaseSkipCommand {
181    #[instrument(skip(self, git, state))]
182    fn execute(
183        &self,
184        git: &GitService,
185        state: &AppState,
186    ) -> Result<CommandResult, CommandError> {
187        let mut new_state = state.clone();
188
189        match RebaseOperations::skip_rebase_step(git, &state.repo_path, &mut new_state.rebase_session) {
190            Ok(result) => {
191                match result {
192                    RebaseResult::Success => {
193                        new_state = reducer(new_state, Action::SetFeedback(Some("Skipped current commit, rebase continued".to_string())));
194                    }
195                    RebaseResult::Completed => {
196                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase completed successfully".to_string())));
197                        new_state.rebase_session = crate::app::rebase::RebaseSession::default();
198                    }
199                    RebaseResult::Conflicts { .. } => {
200                        new_state = reducer(new_state, Action::SetFeedback(Some("Conflicts detected after skipping".to_string())));
201                    }
202                    _ => {
203                        new_state = reducer(new_state, Action::SetStatusError(Some("Unexpected rebase result during skip".to_string())));
204                    }
205                }
206                Ok(CommandResult::StateUpdate(new_state))
207            }
208            Err(e) => {
209                let error_msg = format!("Failed to skip rebase step: {}", e);
210                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
211                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
212            }
213        }
214    }
215}
216
217impl Command for RebaseAbortCommand {
218    #[instrument(skip(self, git, state))]
219    fn execute(
220        &self,
221        git: &GitService,
222        state: &AppState,
223    ) -> Result<CommandResult, CommandError> {
224        let mut new_state = state.clone();
225
226        match RebaseOperations::abort_rebase(git, &state.repo_path, &mut new_state.rebase_session) {
227            Ok(result) => {
228                match result {
229                    RebaseResult::Aborted => {
230                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase aborted successfully".to_string())));
231                        // Reset session to planning state
232                        new_state.rebase_session = crate::app::rebase::RebaseSession::default();
233                    }
234                    _ => {
235                        new_state = reducer(new_state, Action::SetStatusError(Some("Unexpected rebase result during abort".to_string())));
236                    }
237                }
238                Ok(CommandResult::StateUpdate(new_state))
239            }
240            Err(e) => {
241                let error_msg = format!("Failed to abort rebase: {}", e);
242                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
243                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
244            }
245        }
246    }
247}
248
249impl Command for RebaseResolveConflictsCommand {
250    #[instrument(skip(self, git, state))]
251    fn execute(
252        &self,
253        git: &GitService,
254        state: &AppState,
255    ) -> Result<CommandResult, CommandError> {
256        let mut new_state = state.clone();
257
258        // Check if there are any uncommitted changes (conflicts should be resolved)
259        match git.status_porcelain(&state.repo_path) {
260            Ok(status) => {
261                // Check if there are still conflict markers
262                if status.contains("U ") || status.contains("AA ") || status.contains("DD ") {
263                    new_state = reducer(new_state, Action::SetStatusError(Some("Conflicts still exist. Please resolve all conflicts before continuing.".to_string())));
264                    return Ok(CommandResult::StateUpdate(new_state));
265                }
266
267                // Check if there are staged changes (user should have staged resolved files)
268                if !status.lines().any(|line| line.starts_with("M ") || line.starts_with("A ") || line.starts_with("D ")) {
269                    new_state = reducer(new_state, Action::SetStatusError(Some("No staged changes found. Please stage your conflict resolutions.".to_string())));
270                    return Ok(CommandResult::StateUpdate(new_state));
271                }
272
273                // Conflicts appear resolved, try to continue rebase
274                match RebaseOperations::continue_rebase(git, &state.repo_path, &mut new_state.rebase_session) {
275                    Ok(result) => {
276                        match result {
277                            RebaseResult::Success => {
278                                new_state = reducer(new_state, Action::SetFeedback(Some("Conflicts resolved, rebase continued".to_string())));
279                            }
280                            RebaseResult::Completed => {
281                                new_state = reducer(new_state, Action::SetFeedback(Some("Rebase completed successfully after conflict resolution".to_string())));
282                                new_state.rebase_session = crate::app::rebase::RebaseSession::default();
283                            }
284                            RebaseResult::Conflicts { .. } => {
285                                new_state = reducer(new_state, Action::SetFeedback(Some("Additional conflicts detected".to_string())));
286                            }
287                            _ => {
288                                new_state = reducer(new_state, Action::SetStatusError(Some("Unexpected rebase result after conflict resolution".to_string())));
289                            }
290                        }
291                        Ok(CommandResult::StateUpdate(new_state))
292                    }
293                    Err(e) => {
294                        let error_msg = format!("Failed to continue rebase after conflict resolution: {}", e);
295                        new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
296                        Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
297                    }
298                }
299            }
300            Err(e) => {
301                let error_msg = format!("Failed to check repository status: {}", e);
302                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
303                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
304            }
305        }
306    }
307}
308
309impl Command for RebaseAbortFromConflictCommand {
310    #[instrument(skip(self, git, state))]
311    fn execute(
312        &self,
313        git: &GitService,
314        state: &AppState,
315    ) -> Result<CommandResult, CommandError> {
316        let mut new_state = state.clone();
317
318        match RebaseOperations::abort_rebase(git, &state.repo_path, &mut new_state.rebase_session) {
319            Ok(result) => {
320                match result {
321                    RebaseResult::Aborted => {
322                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase aborted from conflict state".to_string())));
323                        // Reset session to planning state
324                        new_state.rebase_session = crate::app::rebase::RebaseSession::default();
325                    }
326                    _ => {
327                        new_state = reducer(new_state, Action::SetStatusError(Some("Unexpected rebase result during abort from conflict".to_string())));
328                    }
329                }
330                Ok(CommandResult::StateUpdate(new_state))
331            }
332            Err(e) => {
333                let error_msg = format!("Failed to abort rebase from conflict state: {}", e);
334                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
335                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
336            }
337        }
338    }
339}
340
341impl Command for RebaseContinueInterruptedCommand {
342    #[instrument(skip(self, git, state))]
343    fn execute(
344        &self,
345        git: &GitService,
346        state: &AppState,
347    ) -> Result<CommandResult, CommandError> {
348        let mut new_state = state.clone();
349
350        match RebaseRecovery::continue_interrupted_rebase(git, &state.repo_path, &mut new_state.rebase_session) {
351            Ok(result) => {
352                match result {
353                    RebaseResult::Success => {
354                        new_state = reducer(new_state, Action::SetFeedback(Some("Continued interrupted rebase".to_string())));
355                        new_state.rebase_recovery_open = false;
356                    }
357                    RebaseResult::Completed => {
358                        new_state = reducer(new_state, Action::SetFeedback(Some("Rebase completed successfully".to_string())));
359                        new_state.rebase_session = crate::app::rebase::RebaseSession::default();
360                        new_state.rebase_recovery_open = false;
361                    }
362                    RebaseResult::Conflicts { .. } => {
363                        new_state = reducer(new_state, Action::SetFeedback(Some("Conflicts detected in interrupted rebase".to_string())));
364                        new_state.rebase_recovery_open = false;
365                    }
366                    _ => {
367                        new_state = reducer(new_state, Action::SetStatusError(Some("Unexpected rebase result during recovery".to_string())));
368                    }
369                }
370                Ok(CommandResult::StateUpdate(new_state))
371            }
372            Err(e) => {
373                let error_msg = format!("Failed to continue interrupted rebase: {}", e);
374                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
375                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
376            }
377        }
378    }
379}
380
381impl Command for RebaseAbortInterruptedCommand {
382    #[instrument(skip(self, git, state))]
383    fn execute(
384        &self,
385        git: &GitService,
386        state: &AppState,
387    ) -> Result<CommandResult, CommandError> {
388        let mut new_state = state.clone();
389
390        match RebaseRecovery::abort_interrupted_rebase(git, &state.repo_path) {
391            Ok(()) => {
392                new_state = reducer(new_state, Action::SetFeedback(Some("Aborted interrupted rebase".to_string())));
393                // Reset session to planning state
394                new_state.rebase_session = crate::app::rebase::RebaseSession::default();
395                new_state.rebase_recovery_open = false;
396                Ok(CommandResult::StateUpdate(new_state))
397            }
398            Err(e) => {
399                let error_msg = format!("Failed to abort interrupted rebase: {}", e);
400                new_state = reducer(new_state, Action::SetStatusError(Some(error_msg.clone())));
401                Err(CommandError::GitError(crate::errors::GitError::OperationError(anyhow::anyhow!(error_msg))))
402            }
403        }
404    }
405}
406