cascade_cli/stack/
rebase.rs

1use crate::errors::{CascadeError, Result};
2use crate::git::{ConflictAnalyzer, GitRepository};
3use crate::stack::{Stack, StackManager};
4use chrono::Utc;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tracing::{debug, info, warn};
8use uuid::Uuid;
9
10/// Conflict resolution result
11#[derive(Debug, Clone)]
12enum ConflictResolution {
13    /// Conflict was successfully resolved
14    Resolved,
15    /// Conflict is too complex for automatic resolution
16    TooComplex,
17}
18
19/// Represents a conflict region in a file
20#[derive(Debug, Clone)]
21#[allow(dead_code)]
22struct ConflictRegion {
23    /// Byte position where conflict starts
24    start: usize,
25    /// Byte position where conflict ends  
26    end: usize,
27    /// Line number where conflict starts
28    start_line: usize,
29    /// Line number where conflict ends
30    end_line: usize,
31    /// Content from "our" side (before separator)
32    our_content: String,
33    /// Content from "their" side (after separator)
34    their_content: String,
35}
36
37/// Strategy for rebasing stacks (force-push is the only valid approach for preserving PR history)
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub enum RebaseStrategy {
40    /// Force-push rebased commits to original branches (preserves PR history)
41    /// This is the industry standard used by Graphite, Phabricator, spr, etc.
42    ForcePush,
43    /// Interactive rebase with conflict resolution
44    Interactive,
45}
46
47/// Options for rebase operations
48#[derive(Debug, Clone)]
49pub struct RebaseOptions {
50    /// The rebase strategy to use
51    pub strategy: RebaseStrategy,
52    /// Whether to run interactively (prompt for user input)
53    pub interactive: bool,
54    /// Target base branch to rebase onto
55    pub target_base: Option<String>,
56    /// Whether to preserve merge commits
57    pub preserve_merges: bool,
58    /// Whether to auto-resolve simple conflicts
59    pub auto_resolve: bool,
60    /// Maximum number of retries for conflict resolution
61    pub max_retries: usize,
62    /// Skip pulling latest changes (when already done by caller)
63    pub skip_pull: Option<bool>,
64}
65
66/// Result of a rebase operation
67#[derive(Debug)]
68pub struct RebaseResult {
69    /// Whether the rebase was successful
70    pub success: bool,
71    /// Old branch to new branch mapping
72    pub branch_mapping: HashMap<String, String>,
73    /// Commits that had conflicts
74    pub conflicts: Vec<String>,
75    /// New commit hashes
76    pub new_commits: Vec<String>,
77    /// Error message if rebase failed
78    pub error: Option<String>,
79    /// Summary of changes made
80    pub summary: String,
81}
82
83/// RAII guard to ensure temporary branches are cleaned up even on error/panic
84///
85/// This stores branch names and provides a cleanup method that can be called
86/// with a GitRepository reference. The Drop trait ensures cleanup happens
87/// even if the rebase function panics or returns early with an error.
88#[allow(dead_code)]
89struct TempBranchCleanupGuard {
90    branches: Vec<String>,
91    cleaned: bool,
92}
93
94#[allow(dead_code)]
95impl TempBranchCleanupGuard {
96    fn new() -> Self {
97        Self {
98            branches: Vec::new(),
99            cleaned: false,
100        }
101    }
102
103    fn add_branch(&mut self, branch: String) {
104        self.branches.push(branch);
105    }
106
107    /// Perform cleanup with provided git repository
108    fn cleanup(&mut self, git_repo: &GitRepository) {
109        if self.cleaned || self.branches.is_empty() {
110            return;
111        }
112
113        info!("๐Ÿงน Cleaning up {} temporary branches", self.branches.len());
114        for branch in &self.branches {
115            if let Err(e) = git_repo.delete_branch_unsafe(branch) {
116                warn!("Failed to delete temp branch {}: {}", branch, e);
117                // Continue with cleanup even if one fails
118            }
119        }
120        self.cleaned = true;
121    }
122}
123
124impl Drop for TempBranchCleanupGuard {
125    fn drop(&mut self) {
126        if !self.cleaned && !self.branches.is_empty() {
127            // This path is only hit on panic or unexpected early return
128            // We can't access git_repo here, so just log the branches that need manual cleanup
129            warn!(
130                "โš ๏ธ  {} temporary branches were not cleaned up: {}",
131                self.branches.len(),
132                self.branches.join(", ")
133            );
134            warn!("Run 'ca cleanup' to remove orphaned temporary branches");
135        }
136    }
137}
138
139/// Manages rebase operations for stacks
140pub struct RebaseManager {
141    stack_manager: StackManager,
142    git_repo: GitRepository,
143    options: RebaseOptions,
144    conflict_analyzer: ConflictAnalyzer,
145}
146
147impl Default for RebaseOptions {
148    fn default() -> Self {
149        Self {
150            strategy: RebaseStrategy::ForcePush,
151            interactive: false,
152            target_base: None,
153            preserve_merges: true,
154            auto_resolve: true,
155            max_retries: 3,
156            skip_pull: None,
157        }
158    }
159}
160
161impl RebaseManager {
162    /// Create a new rebase manager
163    pub fn new(
164        stack_manager: StackManager,
165        git_repo: GitRepository,
166        options: RebaseOptions,
167    ) -> Self {
168        Self {
169            stack_manager,
170            git_repo,
171            options,
172            conflict_analyzer: ConflictAnalyzer::new(),
173        }
174    }
175
176    /// Consume the rebase manager and return the updated stack manager
177    pub fn into_stack_manager(self) -> StackManager {
178        self.stack_manager
179    }
180
181    /// Rebase an entire stack onto a new base
182    pub fn rebase_stack(&mut self, stack_id: &Uuid) -> Result<RebaseResult> {
183        debug!("Starting rebase for stack {}", stack_id);
184
185        let stack = self
186            .stack_manager
187            .get_stack(stack_id)
188            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?
189            .clone();
190
191        match self.options.strategy {
192            RebaseStrategy::ForcePush => self.rebase_with_force_push(&stack),
193            RebaseStrategy::Interactive => self.rebase_interactive(&stack),
194        }
195    }
196
197    /// Rebase using force-push strategy (industry standard for stacked diffs)
198    /// This updates local branches in-place, then force-pushes ONLY branches with existing PRs
199    /// to preserve PR history - the approach used by Graphite, Phabricator, spr, etc.
200    fn rebase_with_force_push(&mut self, stack: &Stack) -> Result<RebaseResult> {
201        use crate::cli::output::Output;
202
203        // Check if there's an in-progress cherry-pick from a previous failed sync
204        if self.has_in_progress_cherry_pick()? {
205            return self.handle_in_progress_cherry_pick(stack);
206        }
207
208        Output::section(format!("Rebasing stack: {}", stack.name));
209
210        let mut result = RebaseResult {
211            success: true,
212            branch_mapping: HashMap::new(),
213            conflicts: Vec::new(),
214            new_commits: Vec::new(),
215            error: None,
216            summary: String::new(),
217        };
218
219        let target_base = self
220            .options
221            .target_base
222            .as_ref()
223            .unwrap_or(&stack.base_branch)
224            .clone(); // Clone to avoid borrow issues
225
226        // Save original working branch to restore later
227        let original_branch = self.git_repo.get_current_branch().ok();
228
229        // Note: Caller (sync_stack) has already checked out base branch when skip_pull=true
230        // Only pull if not already done by caller (like sync command)
231        if !self.options.skip_pull.unwrap_or(false) {
232            if let Err(e) = self.pull_latest_changes(&target_base) {
233                Output::warning(format!("Could not pull latest changes: {}", e));
234            }
235        }
236
237        // Reset working directory to clean state before rebase
238        if let Err(e) = self.git_repo.reset_to_head() {
239            Output::warning(format!("Could not reset working directory: {}", e));
240        }
241
242        let mut current_base = target_base.clone();
243        let entry_count = stack.entries.len();
244        let mut temp_branches: Vec<String> = Vec::new(); // Track temp branches for cleanup
245        let mut branches_to_push: Vec<(String, String)> = Vec::new(); // (branch_name, pr_number)
246
247        println!(); // Spacing before tree
248        let plural = if entry_count == 1 { "entry" } else { "entries" };
249        println!("Rebasing {} {}...", entry_count, plural);
250
251        // Phase 1: Rebase all entries locally (libgit2 only - no CLI commands)
252        for (index, entry) in stack.entries.iter().enumerate() {
253            let original_branch = &entry.branch;
254
255            // Create a temporary branch from the current base
256            // This avoids committing directly to protected branches like develop/main
257            let temp_branch = format!("{}-temp-{}", original_branch, Utc::now().timestamp());
258            temp_branches.push(temp_branch.clone()); // Track for cleanup
259            self.git_repo
260                .create_branch(&temp_branch, Some(&current_base))?;
261            self.git_repo.checkout_branch(&temp_branch)?;
262
263            // Cherry-pick the commit onto the temp branch (NOT the protected base!)
264            match self.cherry_pick_commit(&entry.commit_hash) {
265                Ok(new_commit_hash) => {
266                    result.new_commits.push(new_commit_hash.clone());
267
268                    // Get the commit that's now at HEAD (the cherry-picked commit)
269                    let rebased_commit_id = self.git_repo.get_head_commit()?.id().to_string();
270
271                    // Update the original branch to point to this rebased commit
272                    // This is LOCAL ONLY - moves refs/heads/<branch> to the commit on temp branch
273                    self.git_repo
274                        .update_branch_to_commit(original_branch, &rebased_commit_id)?;
275
276                    // Track which branches need to be pushed (only those with PRs)
277                    let tree_char = if index + 1 == entry_count {
278                        "โ””โ”€"
279                    } else {
280                        "โ”œโ”€"
281                    };
282
283                    if let Some(pr_num) = &entry.pull_request_id {
284                        println!("   {} {} (PR #{})", tree_char, original_branch, pr_num);
285                        branches_to_push.push((original_branch.clone(), pr_num.clone()));
286                    } else {
287                        println!("   {} {} (not submitted)", tree_char, original_branch);
288                    }
289
290                    result
291                        .branch_mapping
292                        .insert(original_branch.clone(), original_branch.clone());
293
294                    // Update stack entry with new commit hash
295                    self.update_stack_entry(
296                        stack.id,
297                        &entry.id,
298                        original_branch,
299                        &rebased_commit_id,
300                    )?;
301
302                    // This branch becomes the base for the next entry
303                    current_base = original_branch.clone();
304                }
305                Err(e) => {
306                    println!(); // Spacing before error
307                    Output::error(format!("Conflict in {}: {}", &entry.commit_hash[..8], e));
308                    result.conflicts.push(entry.commit_hash.clone());
309
310                    if !self.options.auto_resolve {
311                        result.success = false;
312                        result.error = Some(format!(
313                            "Conflict in {}: {}\n\n\
314                            MANUAL CONFLICT RESOLUTION REQUIRED\n\
315                            =====================================\n\n\
316                            Step 1: Analyze conflicts\n\
317                            โ†’ Run: ca conflicts\n\
318                            โ†’ This shows which conflicts are in which files\n\n\
319                            Step 2: Resolve conflicts in your editor\n\
320                            โ†’ Open conflicted files and edit them\n\
321                            โ†’ Remove conflict markers (<<<<<<, ======, >>>>>>)\n\
322                            โ†’ Keep the code you want\n\
323                            โ†’ Save the files\n\n\
324                            Step 3: Mark conflicts as resolved\n\
325                            โ†’ Run: git add <resolved-files>\n\
326                            โ†’ Or: git add -A (to stage all resolved files)\n\n\
327                            Step 4: Complete the sync\n\
328                            โ†’ Run: ca sync\n\
329                            โ†’ Cascade will detect resolved conflicts and continue\n\n\
330                            Alternative: Abort and start over\n\
331                            โ†’ Run: git cherry-pick --abort\n\
332                            โ†’ Then: ca sync (starts fresh)\n\n\
333                            TIP: Enable auto-resolution for simple conflicts:\n\
334                            โ†’ Run: ca sync --auto-resolve\n\
335                            โ†’ Only complex conflicts will require manual resolution",
336                            entry.commit_hash, e
337                        ));
338                        break;
339                    }
340
341                    // Try to resolve automatically
342                    match self.auto_resolve_conflicts(&entry.commit_hash) {
343                        Ok(fully_resolved) => {
344                            if !fully_resolved {
345                                result.success = false;
346                                result.error = Some(format!(
347                                    "Could not auto-resolve all conflicts in {}\n\n\
348                                    MANUAL CONFLICT RESOLUTION REQUIRED\n\
349                                    =====================================\n\n\
350                                    Some conflicts are too complex for auto-resolution.\n\n\
351                                    Step 1: Analyze remaining conflicts\n\
352                                    โ†’ Run: ca conflicts\n\
353                                    โ†’ Shows which files still have conflicts\n\
354                                    โ†’ Use --detailed flag for more info\n\n\
355                                    Step 2: Resolve conflicts in your editor\n\
356                                    โ†’ Open conflicted files (marked with โœ‹ in ca conflicts output)\n\
357                                    โ†’ Remove conflict markers (<<<<<<, ======, >>>>>>)\n\
358                                    โ†’ Keep the code you want\n\
359                                    โ†’ Save the files\n\n\
360                                    Step 3: Mark conflicts as resolved\n\
361                                    โ†’ Run: git add <resolved-files>\n\
362                                    โ†’ Or: git add -A (to stage all resolved files)\n\n\
363                                    Step 4: Complete the sync\n\
364                                    โ†’ Run: ca sync\n\
365                                    โ†’ Cascade will continue from where it left off\n\n\
366                                    Alternative: Abort and start over\n\
367                                    โ†’ Run: git cherry-pick --abort\n\
368                                    โ†’ Then: ca sync (starts fresh)\n\n\
369                                    BACKUP: If auto-resolution was wrong\n\
370                                    โ†’ Check for .cascade-backup files in your repo\n\
371                                    โ†’ These contain the original file content before auto-resolution",
372                                    &entry.commit_hash[..8]
373                                ));
374                                break;
375                            }
376
377                            // Commit the resolved changes
378                            let commit_message = format!("Auto-resolved conflicts in {}", &entry.commit_hash[..8]);
379                            match self.git_repo.commit(&commit_message) {
380                                Ok(new_commit_id) => {
381                                    Output::success("Auto-resolved conflicts");
382                                    result.new_commits.push(new_commit_id.clone());
383                                    let rebased_commit_id = new_commit_id;
384
385                                    // Update the original branch to point to this rebased commit
386                                    self.git_repo
387                                        .update_branch_to_commit(original_branch, &rebased_commit_id)?;
388
389                                    // Track which branches need to be pushed (only those with PRs)
390                                    let tree_char = if index + 1 == entry_count {
391                                        "โ””โ”€"
392                                    } else {
393                                        "โ”œโ”€"
394                                    };
395
396                                    if let Some(pr_num) = &entry.pull_request_id {
397                                        println!("   {} {} (PR #{})", tree_char, original_branch, pr_num);
398                                        branches_to_push.push((original_branch.clone(), pr_num.clone()));
399                                    } else {
400                                        println!("   {} {} (not submitted)", tree_char, original_branch);
401                                    }
402
403                                    result
404                                        .branch_mapping
405                                        .insert(original_branch.clone(), original_branch.clone());
406
407                                    // Update stack entry with new commit hash
408                                    self.update_stack_entry(
409                                        stack.id,
410                                        &entry.id,
411                                        original_branch,
412                                        &rebased_commit_id,
413                                    )?;
414
415                                    // This branch becomes the base for the next entry
416                                    current_base = original_branch.clone();
417                                }
418                                Err(commit_err) => {
419                                    result.success = false;
420                                    result.error = Some(format!(
421                                        "Could not commit auto-resolved conflicts: {}\n\n\
422                                        This usually means:\n\
423                                        - Git index is locked (another process accessing repo)\n\
424                                        - File permissions issue\n\
425                                        - Disk space issue\n\n\
426                                        Recovery:\n\
427                                        1. Check if another Git operation is running\n\
428                                        2. Run 'rm -f .git/index.lock' if stale lock exists\n\
429                                        3. Run 'git status' to check repo state\n\
430                                        4. Retry 'ca sync' after fixing the issue",
431                                        commit_err
432                                    ));
433                                    break;
434                                }
435                            }
436                        }
437                        Err(resolve_err) => {
438                            result.success = false;
439                            result.error = Some(format!(
440                                "Could not resolve conflicts: {}\n\n\
441                                Recovery:\n\
442                                1. Check repo state: 'git status'\n\
443                                2. If files are staged, commit or reset them: 'git reset --hard HEAD'\n\
444                                3. Remove any lock files: 'rm -f .git/index.lock'\n\
445                                4. Retry 'ca sync'",
446                                resolve_err
447                            ));
448                            break;
449                        }
450                    }
451                }
452            }
453        }
454
455        // Cleanup temp branches before returning to original branch
456        // Must checkout away from temp branches first
457        if !temp_branches.is_empty() {
458            // Checkout base branch silently to allow temp branch deletion
459            if let Err(e) = self.git_repo.checkout_branch_silent(&target_base) {
460                Output::warning(format!("Could not checkout base for cleanup: {}", e));
461            }
462
463            // Delete all temp branches
464            for temp_branch in &temp_branches {
465                if let Err(e) = self.git_repo.delete_branch_unsafe(temp_branch) {
466                    debug!("Could not delete temp branch {}: {}", temp_branch, e);
467                }
468            }
469        }
470
471        // Phase 2: Push all branches with PRs to remote (git CLI - after all libgit2 operations)
472        // This batch approach prevents index lock conflicts between libgit2 and git CLI
473        let pushed_count = branches_to_push.len();
474        let skipped_count = entry_count - pushed_count;
475
476        if !branches_to_push.is_empty() {
477            println!(); // Spacing before push phase
478            println!("Pushing {} branch{} to remote...", pushed_count, if pushed_count == 1 { "" } else { "es" });
479            
480            for (branch_name, _pr_num) in &branches_to_push {
481                match self.git_repo.force_push_single_branch_auto(branch_name) {
482                    Ok(_) => {
483                        debug!("Pushed {} successfully", branch_name);
484                    }
485                    Err(e) => {
486                        Output::warning(format!("Could not push '{}': {}", branch_name, e));
487                        // Continue pushing other branches even if one fails
488                    }
489                }
490            }
491        }
492
493        // Update working branch to point to the top of the rebased stack
494        // This ensures subsequent `ca push` doesn't re-add old commits
495        if let Some(ref orig_branch) = original_branch {
496            // Get the last entry's branch (top of stack)
497            if let Some(last_entry) = stack.entries.last() {
498                let top_branch = &last_entry.branch;
499                
500                // Force-update working branch to point to same commit as top entry
501                if let Ok(top_commit) = self.git_repo.get_branch_head(top_branch) {
502                    debug!(
503                        "Updating working branch '{}' to match top of stack ({})",
504                        orig_branch, &top_commit[..8]
505                    );
506                    
507                    if let Err(e) = self.git_repo.update_branch_to_commit(orig_branch, &top_commit) {
508                        Output::warning(format!(
509                            "Could not update working branch '{}' to top of stack: {}",
510                            orig_branch, e
511                        ));
512                    }
513                }
514            }
515            
516            // Return to original working branch silently
517            if let Err(e) = self.git_repo.checkout_branch_silent(orig_branch) {
518                Output::warning(format!(
519                    "Could not return to original branch '{}': {}",
520                    orig_branch, e
521                ));
522            }
523        }
524
525        // Build summary message
526        result.summary = if pushed_count > 0 {
527            let pr_plural = if pushed_count == 1 { "" } else { "s" };
528            let entry_plural = if entry_count == 1 { "entry" } else { "entries" };
529
530            if skipped_count > 0 {
531                format!(
532                    "{} {} rebased ({} PR{} updated, {} not yet submitted)",
533                    entry_count, entry_plural, pushed_count, pr_plural, skipped_count
534                )
535            } else {
536                format!(
537                    "{} {} rebased ({} PR{} updated)",
538                    entry_count, entry_plural, pushed_count, pr_plural
539                )
540            }
541        } else {
542            let plural = if entry_count == 1 { "entry" } else { "entries" };
543            format!("{} {} rebased (no PRs to update yet)", entry_count, plural)
544        };
545
546        // Display result with proper formatting
547        println!(); // Spacing after tree
548        if result.success {
549            Output::success(&result.summary);
550        } else {
551            Output::error(format!("Rebase failed: {:?}", result.error));
552        }
553
554        // Save the updated stack metadata to disk
555        self.stack_manager.save_to_disk()?;
556
557        Ok(result)
558    }
559
560    /// Interactive rebase with user input
561    fn rebase_interactive(&mut self, stack: &Stack) -> Result<RebaseResult> {
562        info!("Starting interactive rebase for stack '{}'", stack.name);
563
564        let mut result = RebaseResult {
565            success: true,
566            branch_mapping: HashMap::new(),
567            conflicts: Vec::new(),
568            new_commits: Vec::new(),
569            error: None,
570            summary: String::new(),
571        };
572
573        println!("Interactive Rebase for Stack: {}", stack.name);
574        println!("   Base branch: {}", stack.base_branch);
575        println!("   Entries: {}", stack.entries.len());
576
577        if self.options.interactive {
578            println!("\nChoose action for each commit:");
579            println!("  (p)ick   - apply the commit");
580            println!("  (s)kip   - skip this commit");
581            println!("  (e)dit   - edit the commit message");
582            println!("  (q)uit   - abort the rebase");
583        }
584
585        // For now, automatically pick all commits
586        // In a real implementation, this would prompt the user
587        for entry in &stack.entries {
588            println!(
589                "  {} {} - {}",
590                entry.short_hash(),
591                entry.branch,
592                entry.short_message(50)
593            );
594
595            // Auto-pick for demo purposes
596            match self.cherry_pick_commit(&entry.commit_hash) {
597                Ok(new_commit) => result.new_commits.push(new_commit),
598                Err(_) => result.conflicts.push(entry.commit_hash.clone()),
599            }
600        }
601
602        result.summary = format!(
603            "Interactive rebase processed {} commits",
604            stack.entries.len()
605        );
606        Ok(result)
607    }
608
609    /// Cherry-pick a commit onto the current branch
610    fn cherry_pick_commit(&self, commit_hash: &str) -> Result<String> {
611        // Use the real cherry-pick implementation from GitRepository
612        let new_commit_hash = self.git_repo.cherry_pick(commit_hash)?;
613
614        // Check for any leftover staged changes after successful cherry-pick
615        if let Ok(staged_files) = self.git_repo.get_staged_files() {
616            if !staged_files.is_empty() {
617                // Commit any leftover staged changes silently
618                let cleanup_message = format!("Cleanup after cherry-pick {}", &commit_hash[..8]);
619                let _ = self.git_repo.commit_staged_changes(&cleanup_message);
620            }
621        }
622
623        Ok(new_commit_hash)
624    }
625
626    /// Attempt to automatically resolve conflicts
627    fn auto_resolve_conflicts(&self, commit_hash: &str) -> Result<bool> {
628        debug!("Attempting to auto-resolve conflicts for {}", commit_hash);
629
630        // Check if there are actually conflicts
631        if !self.git_repo.has_conflicts()? {
632            return Ok(true);
633        }
634
635        let conflicted_files = self.git_repo.get_conflicted_files()?;
636
637        if conflicted_files.is_empty() {
638            return Ok(true);
639        }
640
641        info!(
642            "Found conflicts in {} files: {:?}",
643            conflicted_files.len(),
644            conflicted_files
645        );
646
647        // Use the new conflict analyzer for detailed analysis
648        let analysis = self
649            .conflict_analyzer
650            .analyze_conflicts(&conflicted_files, self.git_repo.path())?;
651
652        info!(
653            "๐Ÿ” Conflict analysis: {} total conflicts, {} auto-resolvable",
654            analysis.total_conflicts, analysis.auto_resolvable_count
655        );
656
657        // Display recommendations
658        for recommendation in &analysis.recommendations {
659            info!("๐Ÿ’ก {}", recommendation);
660        }
661
662        let mut resolved_count = 0;
663        let mut failed_files = Vec::new();
664
665        for file_analysis in &analysis.files {
666            if file_analysis.auto_resolvable {
667                match self.resolve_file_conflicts_enhanced(
668                    &file_analysis.file_path,
669                    &file_analysis.conflicts,
670                ) {
671                    Ok(ConflictResolution::Resolved) => {
672                        resolved_count += 1;
673                        info!("โœ… Auto-resolved conflicts in {}", file_analysis.file_path);
674                    }
675                    Ok(ConflictResolution::TooComplex) => {
676                        debug!(
677                            "โš ๏ธ  Conflicts in {} are too complex for auto-resolution",
678                            file_analysis.file_path
679                        );
680                        failed_files.push(file_analysis.file_path.clone());
681                    }
682                    Err(e) => {
683                        warn!(
684                            "โŒ Failed to resolve conflicts in {}: {}",
685                            file_analysis.file_path, e
686                        );
687                        failed_files.push(file_analysis.file_path.clone());
688                    }
689                }
690            } else {
691                failed_files.push(file_analysis.file_path.clone());
692                info!(
693                    "โš ๏ธ  {} requires manual resolution ({} conflicts)",
694                    file_analysis.file_path,
695                    file_analysis.conflicts.len()
696                );
697            }
698        }
699
700        if resolved_count > 0 {
701            info!(
702                "๐ŸŽ‰ Auto-resolved conflicts in {}/{} files",
703                resolved_count,
704                conflicted_files.len()
705            );
706
707            // Stage all resolved files
708            self.git_repo.stage_conflict_resolved_files()?;
709        }
710
711        // Return true only if ALL conflicts were resolved
712        let all_resolved = failed_files.is_empty();
713
714        if !all_resolved {
715            info!(
716                "โš ๏ธ  {} files still need manual resolution: {:?}",
717                failed_files.len(),
718                failed_files
719            );
720        }
721
722        Ok(all_resolved)
723    }
724
725    /// Resolve conflicts using enhanced analysis
726    fn resolve_file_conflicts_enhanced(
727        &self,
728        file_path: &str,
729        conflicts: &[crate::git::ConflictRegion],
730    ) -> Result<ConflictResolution> {
731        let repo_path = self.git_repo.path();
732        let full_path = repo_path.join(file_path);
733
734        // Read the file content with conflict markers
735        let mut content = std::fs::read_to_string(&full_path)
736            .map_err(|e| CascadeError::config(format!("Failed to read file {file_path}: {e}")))?;
737
738        if conflicts.is_empty() {
739            return Ok(ConflictResolution::Resolved);
740        }
741
742        info!(
743            "Resolving {} conflicts in {} using enhanced analysis",
744            conflicts.len(),
745            file_path
746        );
747
748        let mut any_resolved = false;
749
750        // Process conflicts in reverse order to maintain string indices
751        for conflict in conflicts.iter().rev() {
752            match self.resolve_single_conflict_enhanced(conflict) {
753                Ok(Some(resolution)) => {
754                    // Replace the conflict region with the resolved content
755                    let before = &content[..conflict.start_pos];
756                    let after = &content[conflict.end_pos..];
757                    content = format!("{before}{resolution}{after}");
758                    any_resolved = true;
759                    debug!(
760                        "โœ… Resolved {} conflict at lines {}-{} in {}",
761                        format!("{:?}", conflict.conflict_type).to_lowercase(),
762                        conflict.start_line,
763                        conflict.end_line,
764                        file_path
765                    );
766                }
767                Ok(None) => {
768                    debug!(
769                        "โš ๏ธ  {} conflict at lines {}-{} in {} requires manual resolution",
770                        format!("{:?}", conflict.conflict_type).to_lowercase(),
771                        conflict.start_line,
772                        conflict.end_line,
773                        file_path
774                    );
775                    return Ok(ConflictResolution::TooComplex);
776                }
777                Err(e) => {
778                    debug!("โŒ Failed to resolve conflict in {}: {}", file_path, e);
779                    return Ok(ConflictResolution::TooComplex);
780                }
781            }
782        }
783
784        if any_resolved {
785            // Check if we resolved ALL conflicts in this file
786            let remaining_conflicts = self.parse_conflict_markers(&content)?;
787
788            if remaining_conflicts.is_empty() {
789                // SAFETY: Create backup before writing resolved content
790                // This allows recovery if auto-resolution is incorrect
791                let backup_path = full_path.with_extension("cascade-backup");
792                if let Ok(original_content) = std::fs::read_to_string(&full_path) {
793                    let _ = std::fs::write(&backup_path, original_content);
794                    debug!("Created backup at {:?}", backup_path);
795                }
796                
797                // All conflicts resolved - write the file back atomically
798                crate::utils::atomic_file::write_string(&full_path, &content)?;
799                
800                debug!("Successfully resolved all conflicts in {}", file_path);
801                return Ok(ConflictResolution::Resolved);
802            } else {
803                info!(
804                    "โš ๏ธ  Partially resolved conflicts in {} ({} remaining)",
805                    file_path,
806                    remaining_conflicts.len()
807                );
808            }
809        }
810
811        Ok(ConflictResolution::TooComplex)
812    }
813    
814    /// Helper to count whitespace consistency (lower is better)
815    fn count_whitespace_consistency(content: &str) -> usize {
816        let mut inconsistencies = 0;
817        let lines: Vec<&str> = content.lines().collect();
818        
819        for line in &lines {
820            // Check for mixed tabs and spaces
821            if line.contains('\t') && line.contains(' ') {
822                inconsistencies += 1;
823            }
824        }
825        
826        // Penalize for inconsistencies
827        lines.len().saturating_sub(inconsistencies)
828    }
829
830    /// Resolve a single conflict using enhanced analysis
831    fn resolve_single_conflict_enhanced(
832        &self,
833        conflict: &crate::git::ConflictRegion,
834    ) -> Result<Option<String>> {
835        debug!(
836            "Resolving {} conflict in {} (lines {}-{})",
837            format!("{:?}", conflict.conflict_type).to_lowercase(),
838            conflict.file_path,
839            conflict.start_line,
840            conflict.end_line
841        );
842
843        use crate::git::ConflictType;
844
845        match conflict.conflict_type {
846            ConflictType::Whitespace => {
847                // SAFETY: Only resolve if the content is truly identical except for whitespace
848                // Otherwise, it might be intentional formatting changes
849                let our_normalized = conflict.our_content.split_whitespace().collect::<Vec<_>>().join(" ");
850                let their_normalized = conflict.their_content.split_whitespace().collect::<Vec<_>>().join(" ");
851                
852                if our_normalized == their_normalized {
853                    // Content is identical - prefer the version with more consistent formatting
854                    // (fewer mixed spaces/tabs, more consistent indentation)
855                    let our_consistency = Self::count_whitespace_consistency(&conflict.our_content);
856                    let their_consistency = Self::count_whitespace_consistency(&conflict.their_content);
857                    
858                    if our_consistency >= their_consistency {
859                        Ok(Some(conflict.our_content.clone()))
860                    } else {
861                        Ok(Some(conflict.their_content.clone()))
862                    }
863                } else {
864                    // Content differs beyond whitespace - not safe to auto-resolve
865                    debug!("Whitespace conflict has content differences - requires manual resolution");
866                    Ok(None)
867                }
868            }
869            ConflictType::LineEnding => {
870                // Normalize to Unix line endings
871                let normalized = conflict
872                    .our_content
873                    .replace("\r\n", "\n")
874                    .replace('\r', "\n");
875                Ok(Some(normalized))
876            }
877            ConflictType::PureAddition => {
878                // SAFETY: Only merge if one side is empty (true addition)
879                // If both sides have content, it's not a pure addition - require manual resolution
880                if conflict.our_content.is_empty() {
881                    Ok(Some(conflict.their_content.clone()))
882                } else if conflict.their_content.is_empty() {
883                    Ok(Some(conflict.our_content.clone()))
884                } else {
885                    // Both sides have content - this could be:
886                    // - Duplicate function definitions
887                    // - Conflicting logic
888                    // - Different implementations of same feature
889                    // Too risky to auto-merge - require manual resolution
890                    debug!(
891                        "PureAddition conflict has content on both sides - requires manual resolution"
892                    );
893                    Ok(None)
894                }
895            }
896            ConflictType::ImportMerge => {
897                // SAFETY: Only merge simple single-line imports
898                // Multi-line imports or complex cases require manual resolution
899                
900                // Check if all imports are single-line and look like imports
901                let our_lines: Vec<&str> = conflict.our_content.lines().collect();
902                let their_lines: Vec<&str> = conflict.their_content.lines().collect();
903                
904                // Verify all lines look like simple imports (heuristic check)
905                let all_simple = our_lines.iter().chain(their_lines.iter())
906                    .all(|line| {
907                        let trimmed = line.trim();
908                        trimmed.starts_with("import ") || 
909                        trimmed.starts_with("from ") ||
910                        trimmed.starts_with("use ") ||
911                        trimmed.starts_with("#include") ||
912                        trimmed.is_empty()
913                    });
914                
915                if !all_simple {
916                    debug!("ImportMerge contains non-import lines - requires manual resolution");
917                    return Ok(None);
918                }
919                
920                // Merge and deduplicate imports
921                let mut all_imports: Vec<&str> = our_lines.into_iter()
922                    .chain(their_lines.into_iter())
923                    .filter(|line| !line.trim().is_empty())
924                    .collect();
925                all_imports.sort();
926                all_imports.dedup();
927                Ok(Some(all_imports.join("\n")))
928            }
929            ConflictType::Structural | ConflictType::ContentOverlap | ConflictType::Complex => {
930                // These require manual resolution
931                Ok(None)
932            }
933        }
934    }
935
936    /// Resolve conflicts in a single file using smart strategies
937    #[allow(dead_code)]
938    fn resolve_file_conflicts(&self, file_path: &str) -> Result<ConflictResolution> {
939        let repo_path = self.git_repo.path();
940        let full_path = repo_path.join(file_path);
941
942        // Read the file content with conflict markers
943        let content = std::fs::read_to_string(&full_path)
944            .map_err(|e| CascadeError::config(format!("Failed to read file {file_path}: {e}")))?;
945
946        // Parse conflicts from the file
947        let conflicts = self.parse_conflict_markers(&content)?;
948
949        if conflicts.is_empty() {
950            // No conflict markers found - file might already be resolved
951            return Ok(ConflictResolution::Resolved);
952        }
953
954        info!(
955            "Found {} conflict regions in {}",
956            conflicts.len(),
957            file_path
958        );
959
960        // Try to resolve each conflict using our strategies
961        let mut resolved_content = content;
962        let mut any_resolved = false;
963
964        // Process conflicts in reverse order to maintain string indices
965        for conflict in conflicts.iter().rev() {
966            match self.resolve_single_conflict(conflict, file_path) {
967                Ok(Some(resolution)) => {
968                    // Replace the conflict region with the resolved content
969                    let before = &resolved_content[..conflict.start];
970                    let after = &resolved_content[conflict.end..];
971                    resolved_content = format!("{before}{resolution}{after}");
972                    any_resolved = true;
973                    debug!(
974                        "โœ… Resolved conflict at lines {}-{} in {}",
975                        conflict.start_line, conflict.end_line, file_path
976                    );
977                }
978                Ok(None) => {
979                    debug!(
980                        "โš ๏ธ  Conflict at lines {}-{} in {} too complex for auto-resolution",
981                        conflict.start_line, conflict.end_line, file_path
982                    );
983                }
984                Err(e) => {
985                    debug!("โŒ Failed to resolve conflict in {}: {}", file_path, e);
986                }
987            }
988        }
989
990        if any_resolved {
991            // Check if we resolved ALL conflicts in this file
992            let remaining_conflicts = self.parse_conflict_markers(&resolved_content)?;
993
994            if remaining_conflicts.is_empty() {
995                // All conflicts resolved - write the file back atomically
996                crate::utils::atomic_file::write_string(&full_path, &resolved_content)?;
997
998                return Ok(ConflictResolution::Resolved);
999            } else {
1000                info!(
1001                    "โš ๏ธ  Partially resolved conflicts in {} ({} remaining)",
1002                    file_path,
1003                    remaining_conflicts.len()
1004                );
1005            }
1006        }
1007
1008        Ok(ConflictResolution::TooComplex)
1009    }
1010
1011    /// Parse conflict markers from file content
1012    fn parse_conflict_markers(&self, content: &str) -> Result<Vec<ConflictRegion>> {
1013        let lines: Vec<&str> = content.lines().collect();
1014        let mut conflicts = Vec::new();
1015        let mut i = 0;
1016
1017        while i < lines.len() {
1018            if lines[i].starts_with("<<<<<<<") {
1019                // Found start of conflict
1020                let start_line = i + 1;
1021                let mut separator_line = None;
1022                let mut end_line = None;
1023
1024                // Find the separator and end
1025                for (j, line) in lines.iter().enumerate().skip(i + 1) {
1026                    if line.starts_with("=======") {
1027                        separator_line = Some(j + 1);
1028                    } else if line.starts_with(">>>>>>>") {
1029                        end_line = Some(j + 1);
1030                        break;
1031                    }
1032                }
1033
1034                if let (Some(sep), Some(end)) = (separator_line, end_line) {
1035                    // Calculate byte positions
1036                    let start_pos = lines[..i].iter().map(|l| l.len() + 1).sum::<usize>();
1037                    let end_pos = lines[..end].iter().map(|l| l.len() + 1).sum::<usize>();
1038
1039                    let our_content = lines[(i + 1)..(sep - 1)].join("\n");
1040                    let their_content = lines[sep..(end - 1)].join("\n");
1041
1042                    conflicts.push(ConflictRegion {
1043                        start: start_pos,
1044                        end: end_pos,
1045                        start_line,
1046                        end_line: end,
1047                        our_content,
1048                        their_content,
1049                    });
1050
1051                    i = end;
1052                } else {
1053                    i += 1;
1054                }
1055            } else {
1056                i += 1;
1057            }
1058        }
1059
1060        Ok(conflicts)
1061    }
1062
1063    /// Resolve a single conflict using smart strategies
1064    fn resolve_single_conflict(
1065        &self,
1066        conflict: &ConflictRegion,
1067        file_path: &str,
1068    ) -> Result<Option<String>> {
1069        debug!(
1070            "Analyzing conflict in {} (lines {}-{})",
1071            file_path, conflict.start_line, conflict.end_line
1072        );
1073
1074        // Strategy 1: Whitespace-only differences
1075        if let Some(resolved) = self.resolve_whitespace_conflict(conflict)? {
1076            debug!("Resolved as whitespace-only conflict");
1077            return Ok(Some(resolved));
1078        }
1079
1080        // Strategy 2: Line ending differences
1081        if let Some(resolved) = self.resolve_line_ending_conflict(conflict)? {
1082            debug!("Resolved as line ending conflict");
1083            return Ok(Some(resolved));
1084        }
1085
1086        // Strategy 3: Pure addition conflicts (no overlapping changes)
1087        if let Some(resolved) = self.resolve_addition_conflict(conflict)? {
1088            debug!("Resolved as pure addition conflict");
1089            return Ok(Some(resolved));
1090        }
1091
1092        // Strategy 4: Import/dependency reordering
1093        if let Some(resolved) = self.resolve_import_conflict(conflict, file_path)? {
1094            debug!("Resolved as import reordering conflict");
1095            return Ok(Some(resolved));
1096        }
1097
1098        // No strategy could resolve this conflict
1099        Ok(None)
1100    }
1101
1102    /// Resolve conflicts that only differ by whitespace
1103    fn resolve_whitespace_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
1104        let our_normalized = self.normalize_whitespace(&conflict.our_content);
1105        let their_normalized = self.normalize_whitespace(&conflict.their_content);
1106
1107        if our_normalized == their_normalized {
1108            // Only whitespace differences - prefer the version with better formatting
1109            let resolved =
1110                if conflict.our_content.trim().len() >= conflict.their_content.trim().len() {
1111                    conflict.our_content.clone()
1112                } else {
1113                    conflict.their_content.clone()
1114                };
1115
1116            return Ok(Some(resolved));
1117        }
1118
1119        Ok(None)
1120    }
1121
1122    /// Resolve conflicts that only differ by line endings
1123    fn resolve_line_ending_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
1124        let our_normalized = conflict
1125            .our_content
1126            .replace("\r\n", "\n")
1127            .replace('\r', "\n");
1128        let their_normalized = conflict
1129            .their_content
1130            .replace("\r\n", "\n")
1131            .replace('\r', "\n");
1132
1133        if our_normalized == their_normalized {
1134            // Only line ending differences - prefer Unix line endings
1135            return Ok(Some(our_normalized));
1136        }
1137
1138        Ok(None)
1139    }
1140
1141    /// Resolve conflicts where both sides only add lines (no overlapping edits)
1142    fn resolve_addition_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
1143        let our_lines: Vec<&str> = conflict.our_content.lines().collect();
1144        let their_lines: Vec<&str> = conflict.their_content.lines().collect();
1145
1146        // Check if one side is a subset of the other (pure addition)
1147        if our_lines.is_empty() {
1148            return Ok(Some(conflict.their_content.clone()));
1149        }
1150        if their_lines.is_empty() {
1151            return Ok(Some(conflict.our_content.clone()));
1152        }
1153
1154        // Try to merge additions intelligently
1155        let mut merged_lines = Vec::new();
1156        let mut our_idx = 0;
1157        let mut their_idx = 0;
1158
1159        while our_idx < our_lines.len() || their_idx < their_lines.len() {
1160            if our_idx >= our_lines.len() {
1161                // Only their lines left
1162                merged_lines.extend_from_slice(&their_lines[their_idx..]);
1163                break;
1164            } else if their_idx >= their_lines.len() {
1165                // Only our lines left
1166                merged_lines.extend_from_slice(&our_lines[our_idx..]);
1167                break;
1168            } else if our_lines[our_idx] == their_lines[their_idx] {
1169                // Same line - add once
1170                merged_lines.push(our_lines[our_idx]);
1171                our_idx += 1;
1172                their_idx += 1;
1173            } else {
1174                // Different lines - this might be too complex
1175                return Ok(None);
1176            }
1177        }
1178
1179        Ok(Some(merged_lines.join("\n")))
1180    }
1181
1182    /// Resolve import/dependency conflicts by sorting and merging
1183    fn resolve_import_conflict(
1184        &self,
1185        conflict: &ConflictRegion,
1186        file_path: &str,
1187    ) -> Result<Option<String>> {
1188        // Only apply to likely import sections in common file types
1189        let is_import_file = file_path.ends_with(".rs")
1190            || file_path.ends_with(".py")
1191            || file_path.ends_with(".js")
1192            || file_path.ends_with(".ts")
1193            || file_path.ends_with(".go")
1194            || file_path.ends_with(".java")
1195            || file_path.ends_with(".swift")
1196            || file_path.ends_with(".kt")
1197            || file_path.ends_with(".cs");
1198
1199        if !is_import_file {
1200            return Ok(None);
1201        }
1202
1203        let our_lines: Vec<&str> = conflict.our_content.lines().collect();
1204        let their_lines: Vec<&str> = conflict.their_content.lines().collect();
1205
1206        // Check if all lines look like imports/uses
1207        let our_imports = our_lines
1208            .iter()
1209            .all(|line| self.is_import_line(line, file_path));
1210        let their_imports = their_lines
1211            .iter()
1212            .all(|line| self.is_import_line(line, file_path));
1213
1214        if our_imports && their_imports {
1215            // Merge and sort imports
1216            let mut all_imports: Vec<&str> = our_lines.into_iter().chain(their_lines).collect();
1217            all_imports.sort();
1218            all_imports.dedup();
1219
1220            return Ok(Some(all_imports.join("\n")));
1221        }
1222
1223        Ok(None)
1224    }
1225
1226    /// Check if a line looks like an import statement
1227    fn is_import_line(&self, line: &str, file_path: &str) -> bool {
1228        let trimmed = line.trim();
1229
1230        if trimmed.is_empty() {
1231            return true; // Empty lines are OK in import sections
1232        }
1233
1234        if file_path.ends_with(".rs") {
1235            return trimmed.starts_with("use ") || trimmed.starts_with("extern crate");
1236        } else if file_path.ends_with(".py") {
1237            return trimmed.starts_with("import ") || trimmed.starts_with("from ");
1238        } else if file_path.ends_with(".js") || file_path.ends_with(".ts") {
1239            return trimmed.starts_with("import ")
1240                || trimmed.starts_with("const ")
1241                || trimmed.starts_with("require(");
1242        } else if file_path.ends_with(".go") {
1243            return trimmed.starts_with("import ") || trimmed == "import (" || trimmed == ")";
1244        } else if file_path.ends_with(".java") {
1245            return trimmed.starts_with("import ");
1246        } else if file_path.ends_with(".swift") {
1247            return trimmed.starts_with("import ") || trimmed.starts_with("@testable import ");
1248        } else if file_path.ends_with(".kt") {
1249            return trimmed.starts_with("import ") || trimmed.starts_with("@file:");
1250        } else if file_path.ends_with(".cs") {
1251            return trimmed.starts_with("using ") || trimmed.starts_with("extern alias ");
1252        }
1253
1254        false
1255    }
1256
1257    /// Normalize whitespace for comparison
1258    fn normalize_whitespace(&self, content: &str) -> String {
1259        content
1260            .lines()
1261            .map(|line| line.trim())
1262            .filter(|line| !line.is_empty())
1263            .collect::<Vec<_>>()
1264            .join("\n")
1265    }
1266
1267    /// Update a stack entry with new commit information
1268    /// NOTE: We keep the original branch name to preserve PR mapping, only update commit hash
1269    fn update_stack_entry(
1270        &mut self,
1271        stack_id: Uuid,
1272        entry_id: &Uuid,
1273        _new_branch: &str,
1274        new_commit_hash: &str,
1275    ) -> Result<()> {
1276        debug!(
1277            "Updating entry {} in stack {} with new commit {}",
1278            entry_id, stack_id, new_commit_hash
1279        );
1280
1281        // Get the stack and update the entry
1282        let stack = self
1283            .stack_manager
1284            .get_stack_mut(&stack_id)
1285            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
1286
1287        // Find and update the entry
1288        if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == *entry_id) {
1289            debug!(
1290                "Found entry {} - updating commit from '{}' to '{}' (keeping original branch '{}')",
1291                entry_id, entry.commit_hash, new_commit_hash, entry.branch
1292            );
1293
1294            // CRITICAL: Keep the original branch name to preserve PR mapping
1295            // Only update the commit hash to point to the new rebased commit
1296            entry.commit_hash = new_commit_hash.to_string();
1297
1298            // Note: Stack will be saved by the caller (StackManager) after rebase completes
1299
1300            debug!(
1301                "Successfully updated entry {} in stack {}",
1302                entry_id, stack_id
1303            );
1304            Ok(())
1305        } else {
1306            Err(CascadeError::config(format!(
1307                "Entry {entry_id} not found in stack {stack_id}"
1308            )))
1309        }
1310    }
1311
1312    /// Pull latest changes from remote
1313    fn pull_latest_changes(&self, branch: &str) -> Result<()> {
1314        info!("Pulling latest changes for branch {}", branch);
1315
1316        // First try to fetch (this might fail if no remote exists)
1317        match self.git_repo.fetch() {
1318            Ok(_) => {
1319                debug!("Fetch successful");
1320                // Now try to pull the specific branch
1321                match self.git_repo.pull(branch) {
1322                    Ok(_) => {
1323                        info!("Pull completed successfully for {}", branch);
1324                        Ok(())
1325                    }
1326                    Err(e) => {
1327                        warn!("Pull failed for {}: {}", branch, e);
1328                        // Don't fail the entire rebase for pull issues
1329                        Ok(())
1330                    }
1331                }
1332            }
1333            Err(e) => {
1334                warn!("Fetch failed: {}", e);
1335                // Don't fail if there's no remote configured
1336                Ok(())
1337            }
1338        }
1339    }
1340
1341    /// Check if rebase is in progress
1342    pub fn is_rebase_in_progress(&self) -> bool {
1343        // Check for git rebase state files
1344        let git_dir = self.git_repo.path().join(".git");
1345        git_dir.join("REBASE_HEAD").exists()
1346            || git_dir.join("rebase-merge").exists()
1347            || git_dir.join("rebase-apply").exists()
1348    }
1349
1350    /// Abort an in-progress rebase
1351    pub fn abort_rebase(&self) -> Result<()> {
1352        info!("Aborting rebase operation");
1353
1354        let git_dir = self.git_repo.path().join(".git");
1355
1356        // Clean up rebase state files
1357        if git_dir.join("REBASE_HEAD").exists() {
1358            std::fs::remove_file(git_dir.join("REBASE_HEAD")).map_err(|e| {
1359                CascadeError::Git(git2::Error::from_str(&format!(
1360                    "Failed to clean rebase state: {e}"
1361                )))
1362            })?;
1363        }
1364
1365        if git_dir.join("rebase-merge").exists() {
1366            std::fs::remove_dir_all(git_dir.join("rebase-merge")).map_err(|e| {
1367                CascadeError::Git(git2::Error::from_str(&format!(
1368                    "Failed to clean rebase-merge: {e}"
1369                )))
1370            })?;
1371        }
1372
1373        if git_dir.join("rebase-apply").exists() {
1374            std::fs::remove_dir_all(git_dir.join("rebase-apply")).map_err(|e| {
1375                CascadeError::Git(git2::Error::from_str(&format!(
1376                    "Failed to clean rebase-apply: {e}"
1377                )))
1378            })?;
1379        }
1380
1381        info!("Rebase aborted successfully");
1382        Ok(())
1383    }
1384
1385    /// Continue an in-progress rebase after conflict resolution
1386    pub fn continue_rebase(&self) -> Result<()> {
1387        info!("Continuing rebase operation");
1388
1389        // Check if there are still conflicts
1390        if self.git_repo.has_conflicts()? {
1391            return Err(CascadeError::branch(
1392                "Cannot continue rebase: there are unresolved conflicts. Resolve conflicts and stage files first.".to_string()
1393            ));
1394        }
1395
1396        // Stage resolved files
1397        self.git_repo.stage_conflict_resolved_files()?;
1398
1399        info!("Rebase continued successfully");
1400        Ok(())
1401    }
1402    
1403    /// Check if there's an in-progress cherry-pick operation
1404    fn has_in_progress_cherry_pick(&self) -> Result<bool> {
1405        let git_dir = self.git_repo.path().join(".git");
1406        Ok(git_dir.join("CHERRY_PICK_HEAD").exists())
1407    }
1408    
1409    /// Handle resuming an in-progress cherry-pick from a previous failed sync
1410    fn handle_in_progress_cherry_pick(&mut self, stack: &Stack) -> Result<RebaseResult> {
1411        use crate::cli::output::Output;
1412        
1413        let git_dir = self.git_repo.path().join(".git");
1414        
1415        Output::section("Resuming in-progress sync");
1416        println!();
1417        Output::info("Detected unfinished cherry-pick from previous sync");
1418        println!();
1419        
1420        // Check if conflicts are resolved
1421        if self.git_repo.has_conflicts()? {
1422            let conflicted_files = self.git_repo.get_conflicted_files()?;
1423            
1424            let result = RebaseResult {
1425                success: false,
1426                branch_mapping: HashMap::new(),
1427                conflicts: conflicted_files.clone(),
1428                new_commits: Vec::new(),
1429                error: Some(format!(
1430                    "Cannot continue: {} file(s) still have unresolved conflicts\n\n\
1431                    MANUAL CONFLICT RESOLUTION REQUIRED\n\
1432                    =====================================\n\n\
1433                    Conflicted files:\n{}\n\n\
1434                    Step 1: Analyze conflicts\n\
1435                    โ†’ Run: ca conflicts\n\
1436                    โ†’ Shows detailed conflict analysis\n\n\
1437                    Step 2: Resolve conflicts in your editor\n\
1438                    โ†’ Open conflicted files and edit them\n\
1439                    โ†’ Remove conflict markers (<<<<<<, ======, >>>>>>)\n\
1440                    โ†’ Keep the code you want\n\
1441                    โ†’ Save the files\n\n\
1442                    Step 3: Mark conflicts as resolved\n\
1443                    โ†’ Run: git add <resolved-files>\n\
1444                    โ†’ Or: git add -A (to stage all resolved files)\n\n\
1445                    Step 4: Complete the sync\n\
1446                    โ†’ Run: ca sync\n\
1447                    โ†’ Cascade will continue from where it left off\n\n\
1448                    Alternative: Abort and start over\n\
1449                    โ†’ Run: git cherry-pick --abort\n\
1450                    โ†’ Then: ca sync (starts fresh)",
1451                    conflicted_files.len(),
1452                    conflicted_files.iter().map(|f| format!("  - {}", f)).collect::<Vec<_>>().join("\n")
1453                )),
1454                summary: "Sync paused - conflicts need resolution".to_string(),
1455            };
1456            
1457            return Ok(result);
1458        }
1459        
1460        // Conflicts are resolved - continue the cherry-pick
1461        Output::info("Conflicts resolved, continuing cherry-pick...");
1462        
1463        // Stage all resolved files
1464        self.git_repo.stage_conflict_resolved_files()?;
1465        
1466        // Complete the cherry-pick by committing
1467        let cherry_pick_msg_file = git_dir.join("CHERRY_PICK_MSG");
1468        let commit_message = if cherry_pick_msg_file.exists() {
1469            std::fs::read_to_string(&cherry_pick_msg_file)
1470                .unwrap_or_else(|_| "Resolved conflicts".to_string())
1471        } else {
1472            "Resolved conflicts".to_string()
1473        };
1474        
1475        match self.git_repo.commit(&commit_message) {
1476            Ok(new_commit_id) => {
1477                Output::success("Cherry-pick completed");
1478                
1479                // Clean up cherry-pick state
1480                if git_dir.join("CHERRY_PICK_HEAD").exists() {
1481                    let _ = std::fs::remove_file(git_dir.join("CHERRY_PICK_HEAD"));
1482                }
1483                if cherry_pick_msg_file.exists() {
1484                    let _ = std::fs::remove_file(&cherry_pick_msg_file);
1485                }
1486                
1487                println!();
1488                Output::info("Continuing with rest of stack...");
1489                println!();
1490                
1491                // Now continue with the rest of the rebase
1492                // We need to restart the full rebase since we don't track which entry we were on
1493                self.rebase_with_force_push(stack)
1494            }
1495            Err(e) => {
1496                let result = RebaseResult {
1497                    success: false,
1498                    branch_mapping: HashMap::new(),
1499                    conflicts: Vec::new(),
1500                    new_commits: Vec::new(),
1501                    error: Some(format!(
1502                        "Failed to complete cherry-pick: {}\n\n\
1503                        This usually means:\n\
1504                        - Git index is locked (another process accessing repo)\n\
1505                        - File permissions issue\n\
1506                        - Disk space issue\n\n\
1507                        Recovery:\n\
1508                        1. Check if another Git operation is running\n\
1509                        2. Run 'rm -f .git/index.lock' if stale lock exists\n\
1510                        3. Run 'git status' to check repo state\n\
1511                        4. Retry 'ca sync' after fixing the issue\n\n\
1512                        Or abort and start fresh:\n\
1513                        โ†’ Run: git cherry-pick --abort\n\
1514                        โ†’ Then: ca sync",
1515                        e
1516                    )),
1517                    summary: "Failed to complete cherry-pick".to_string(),
1518                };
1519                
1520                Ok(result)
1521            }
1522        }
1523    }
1524}
1525
1526impl RebaseResult {
1527    /// Get a summary of the rebase operation
1528    pub fn get_summary(&self) -> String {
1529        if self.success {
1530            format!("โœ… {}", self.summary)
1531        } else {
1532            format!(
1533                "โŒ Rebase failed: {}",
1534                self.error.as_deref().unwrap_or("Unknown error")
1535            )
1536        }
1537    }
1538
1539    /// Check if any conflicts occurred
1540    pub fn has_conflicts(&self) -> bool {
1541        !self.conflicts.is_empty()
1542    }
1543
1544    /// Get the number of successful operations
1545    pub fn success_count(&self) -> usize {
1546        self.new_commits.len()
1547    }
1548}
1549
1550#[cfg(test)]
1551mod tests {
1552    use super::*;
1553    use std::path::PathBuf;
1554    use std::process::Command;
1555    use tempfile::TempDir;
1556
1557    #[allow(dead_code)]
1558    fn create_test_repo() -> (TempDir, PathBuf) {
1559        let temp_dir = TempDir::new().unwrap();
1560        let repo_path = temp_dir.path().to_path_buf();
1561
1562        // Initialize git repository
1563        Command::new("git")
1564            .args(["init"])
1565            .current_dir(&repo_path)
1566            .output()
1567            .unwrap();
1568        Command::new("git")
1569            .args(["config", "user.name", "Test"])
1570            .current_dir(&repo_path)
1571            .output()
1572            .unwrap();
1573        Command::new("git")
1574            .args(["config", "user.email", "test@test.com"])
1575            .current_dir(&repo_path)
1576            .output()
1577            .unwrap();
1578
1579        // Create initial commit
1580        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
1581        Command::new("git")
1582            .args(["add", "."])
1583            .current_dir(&repo_path)
1584            .output()
1585            .unwrap();
1586        Command::new("git")
1587            .args(["commit", "-m", "Initial"])
1588            .current_dir(&repo_path)
1589            .output()
1590            .unwrap();
1591
1592        (temp_dir, repo_path)
1593    }
1594
1595    #[test]
1596    fn test_conflict_region_creation() {
1597        let region = ConflictRegion {
1598            start: 0,
1599            end: 50,
1600            start_line: 1,
1601            end_line: 3,
1602            our_content: "function test() {\n    return true;\n}".to_string(),
1603            their_content: "function test() {\n  return true;\n}".to_string(),
1604        };
1605
1606        assert_eq!(region.start_line, 1);
1607        assert_eq!(region.end_line, 3);
1608        assert!(region.our_content.contains("return true"));
1609        assert!(region.their_content.contains("return true"));
1610    }
1611
1612    #[test]
1613    fn test_rebase_strategies() {
1614        assert_eq!(RebaseStrategy::ForcePush, RebaseStrategy::ForcePush);
1615        assert_eq!(RebaseStrategy::Interactive, RebaseStrategy::Interactive);
1616    }
1617
1618    #[test]
1619    fn test_rebase_options() {
1620        let options = RebaseOptions::default();
1621        assert_eq!(options.strategy, RebaseStrategy::ForcePush);
1622        assert!(!options.interactive);
1623        assert!(options.auto_resolve);
1624        assert_eq!(options.max_retries, 3);
1625    }
1626
1627    #[test]
1628    fn test_cleanup_guard_tracks_branches() {
1629        let mut guard = TempBranchCleanupGuard::new();
1630        assert!(guard.branches.is_empty());
1631
1632        guard.add_branch("test-branch-1".to_string());
1633        guard.add_branch("test-branch-2".to_string());
1634
1635        assert_eq!(guard.branches.len(), 2);
1636        assert_eq!(guard.branches[0], "test-branch-1");
1637        assert_eq!(guard.branches[1], "test-branch-2");
1638    }
1639
1640    #[test]
1641    fn test_cleanup_guard_prevents_double_cleanup() {
1642        use std::process::Command;
1643        use tempfile::TempDir;
1644
1645        // Create a temporary git repo
1646        let temp_dir = TempDir::new().unwrap();
1647        let repo_path = temp_dir.path();
1648
1649        Command::new("git")
1650            .args(["init"])
1651            .current_dir(repo_path)
1652            .output()
1653            .unwrap();
1654
1655        Command::new("git")
1656            .args(["config", "user.name", "Test"])
1657            .current_dir(repo_path)
1658            .output()
1659            .unwrap();
1660
1661        Command::new("git")
1662            .args(["config", "user.email", "test@test.com"])
1663            .current_dir(repo_path)
1664            .output()
1665            .unwrap();
1666
1667        // Create initial commit
1668        std::fs::write(repo_path.join("test.txt"), "test").unwrap();
1669        Command::new("git")
1670            .args(["add", "."])
1671            .current_dir(repo_path)
1672            .output()
1673            .unwrap();
1674        Command::new("git")
1675            .args(["commit", "-m", "initial"])
1676            .current_dir(repo_path)
1677            .output()
1678            .unwrap();
1679
1680        let git_repo = GitRepository::open(repo_path).unwrap();
1681
1682        // Create a test branch
1683        git_repo.create_branch("test-temp", None).unwrap();
1684
1685        let mut guard = TempBranchCleanupGuard::new();
1686        guard.add_branch("test-temp".to_string());
1687
1688        // First cleanup should work
1689        guard.cleanup(&git_repo);
1690        assert!(guard.cleaned);
1691
1692        // Second cleanup should be a no-op (shouldn't panic)
1693        guard.cleanup(&git_repo);
1694        assert!(guard.cleaned);
1695    }
1696
1697    #[test]
1698    fn test_rebase_result() {
1699        let result = RebaseResult {
1700            success: true,
1701            branch_mapping: std::collections::HashMap::new(),
1702            conflicts: vec!["abc123".to_string()],
1703            new_commits: vec!["def456".to_string()],
1704            error: None,
1705            summary: "Test summary".to_string(),
1706        };
1707
1708        assert!(result.success);
1709        assert!(result.has_conflicts());
1710        assert_eq!(result.success_count(), 1);
1711    }
1712
1713    #[test]
1714    fn test_import_line_detection() {
1715        let (_temp_dir, repo_path) = create_test_repo();
1716        let git_repo = crate::git::GitRepository::open(&repo_path).unwrap();
1717        let stack_manager = crate::stack::StackManager::new(&repo_path).unwrap();
1718        let options = RebaseOptions::default();
1719        let rebase_manager = RebaseManager::new(stack_manager, git_repo, options);
1720
1721        // Test Swift import detection
1722        assert!(rebase_manager.is_import_line("import Foundation", "test.swift"));
1723        assert!(rebase_manager.is_import_line("@testable import MyModule", "test.swift"));
1724        assert!(!rebase_manager.is_import_line("class MyClass {", "test.swift"));
1725
1726        // Test Kotlin import detection
1727        assert!(rebase_manager.is_import_line("import kotlin.collections.*", "test.kt"));
1728        assert!(rebase_manager.is_import_line("@file:JvmName(\"Utils\")", "test.kt"));
1729        assert!(!rebase_manager.is_import_line("fun myFunction() {", "test.kt"));
1730
1731        // Test C# import detection
1732        assert!(rebase_manager.is_import_line("using System;", "test.cs"));
1733        assert!(rebase_manager.is_import_line("using System.Collections.Generic;", "test.cs"));
1734        assert!(rebase_manager.is_import_line("extern alias GridV1;", "test.cs"));
1735        assert!(!rebase_manager.is_import_line("namespace MyNamespace {", "test.cs"));
1736
1737        // Test empty lines are allowed in import sections
1738        assert!(rebase_manager.is_import_line("", "test.swift"));
1739        assert!(rebase_manager.is_import_line("   ", "test.kt"));
1740        assert!(rebase_manager.is_import_line("", "test.cs"));
1741    }
1742}