cascade_cli/stack/
rebase.rs

1use crate::errors::{CascadeError, Result};
2use crate::git::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)]
21struct ConflictRegion {
22    /// Byte position where conflict starts
23    start: usize,
24    /// Byte position where conflict ends  
25    end: usize,
26    /// Line number where conflict starts
27    start_line: usize,
28    /// Line number where conflict ends
29    end_line: usize,
30    /// Content from "our" side (before separator)
31    our_content: String,
32    /// Content from "their" side (after separator)
33    their_content: String,
34}
35
36/// Different strategies for rebasing stacks without force-pushing
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub enum RebaseStrategy {
39    /// Create new branches with version suffixes (e.g., feature-v2, feature-v3)
40    BranchVersioning,
41    /// Use cherry-pick to apply commits on new base
42    CherryPick,
43    /// Create new commits that merge changes from base
44    ThreeWayMerge,
45    /// Interactive rebase with conflict resolution
46    Interactive,
47}
48
49/// Options for rebase operations
50#[derive(Debug, Clone)]
51pub struct RebaseOptions {
52    /// The rebase strategy to use
53    pub strategy: RebaseStrategy,
54    /// Whether to run interactively (prompt for user input)
55    pub interactive: bool,
56    /// Target base branch to rebase onto
57    pub target_base: Option<String>,
58    /// Whether to preserve merge commits
59    pub preserve_merges: bool,
60    /// Whether to auto-resolve simple conflicts
61    pub auto_resolve: bool,
62    /// Maximum number of retries for conflict resolution
63    pub max_retries: usize,
64    /// Skip pulling latest changes (when already done by caller)
65    pub skip_pull: Option<bool>,
66}
67
68/// Result of a rebase operation
69#[derive(Debug)]
70pub struct RebaseResult {
71    /// Whether the rebase was successful
72    pub success: bool,
73    /// Old branch to new branch mapping
74    pub branch_mapping: HashMap<String, String>,
75    /// Commits that had conflicts
76    pub conflicts: Vec<String>,
77    /// New commit hashes
78    pub new_commits: Vec<String>,
79    /// Error message if rebase failed
80    pub error: Option<String>,
81    /// Summary of changes made
82    pub summary: String,
83}
84
85/// Manages rebase operations for stacks
86pub struct RebaseManager {
87    stack_manager: StackManager,
88    git_repo: GitRepository,
89    options: RebaseOptions,
90}
91
92impl Default for RebaseOptions {
93    fn default() -> Self {
94        Self {
95            strategy: RebaseStrategy::BranchVersioning,
96            interactive: false,
97            target_base: None,
98            preserve_merges: true,
99            auto_resolve: true,
100            max_retries: 3,
101            skip_pull: None,
102        }
103    }
104}
105
106impl RebaseManager {
107    /// Create a new rebase manager
108    pub fn new(
109        stack_manager: StackManager,
110        git_repo: GitRepository,
111        options: RebaseOptions,
112    ) -> Self {
113        Self {
114            stack_manager,
115            git_repo,
116            options,
117        }
118    }
119
120    /// Rebase an entire stack onto a new base
121    pub fn rebase_stack(&mut self, stack_id: &Uuid) -> Result<RebaseResult> {
122        info!("Starting rebase for stack {}", stack_id);
123
124        let stack = self
125            .stack_manager
126            .get_stack(stack_id)
127            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?
128            .clone();
129
130        match self.options.strategy {
131            RebaseStrategy::BranchVersioning => self.rebase_with_versioning(&stack),
132            RebaseStrategy::CherryPick => self.rebase_with_cherry_pick(&stack),
133            RebaseStrategy::ThreeWayMerge => self.rebase_with_three_way_merge(&stack),
134            RebaseStrategy::Interactive => self.rebase_interactive(&stack),
135        }
136    }
137
138    /// Rebase using branch versioning strategy (no force-push needed)
139    fn rebase_with_versioning(&mut self, stack: &Stack) -> Result<RebaseResult> {
140        info!(
141            "Rebasing stack '{}' using branch versioning strategy",
142            stack.name
143        );
144
145        let mut result = RebaseResult {
146            success: true,
147            branch_mapping: HashMap::new(),
148            conflicts: Vec::new(),
149            new_commits: Vec::new(),
150            error: None,
151            summary: String::new(),
152        };
153
154        let target_base = self
155            .options
156            .target_base
157            .as_ref()
158            .unwrap_or(&stack.base_branch);
159
160        // Ensure we're on the base branch
161        if self.git_repo.get_current_branch()? != *target_base {
162            self.git_repo.checkout_branch(target_base)?;
163        }
164
165        // Only pull if not already done by caller (like sync command)
166        // Note: This prevents redundant pulls when called from sync
167        if !self.options.skip_pull.unwrap_or(false) {
168            if let Err(e) = self.pull_latest_changes(target_base) {
169                warn!("Failed to pull latest changes: {}", e);
170            }
171        } else {
172            debug!("Skipping pull - already done by caller");
173        }
174
175        let mut current_base = target_base.clone();
176
177        for (index, entry) in stack.entries.iter().enumerate() {
178            debug!("Processing entry {}: {}", index, entry.short_hash());
179
180            // Generate new branch name with version
181            let new_branch = self.generate_versioned_branch_name(&entry.branch)?;
182
183            // Create new branch from current base
184            self.git_repo
185                .create_branch(&new_branch, Some(&current_base))?;
186            self.git_repo.checkout_branch(&new_branch)?;
187
188            // Cherry-pick the commit onto the new branch
189            match self.cherry_pick_commit(&entry.commit_hash) {
190                Ok(new_commit_hash) => {
191                    result.new_commits.push(new_commit_hash.clone());
192                    result
193                        .branch_mapping
194                        .insert(entry.branch.clone(), new_branch.clone());
195
196                    // Update stack entry with new branch and commit hash
197                    self.update_stack_entry(stack.id, &entry.id, &new_branch, &new_commit_hash)?;
198
199                    // This branch becomes the base for the next entry
200                    current_base = new_branch;
201                }
202                Err(e) => {
203                    warn!("Failed to cherry-pick {}: {}", entry.commit_hash, e);
204                    result.conflicts.push(entry.commit_hash.clone());
205
206                    if !self.options.auto_resolve {
207                        result.success = false;
208                        result.error = Some(format!("Conflict in {}: {}", entry.commit_hash, e));
209                        break;
210                    }
211
212                    // Try to resolve automatically
213                    match self.auto_resolve_conflicts(&entry.commit_hash) {
214                        Ok(_) => {
215                            info!("Auto-resolved conflicts for {}", entry.commit_hash);
216                        }
217                        Err(resolve_err) => {
218                            result.success = false;
219                            result.error =
220                                Some(format!("Could not resolve conflicts: {resolve_err}"));
221                            break;
222                        }
223                    }
224                }
225            }
226        }
227
228        result.summary = format!(
229            "Rebased {} entries using branch versioning. {} new branches created.",
230            stack.entries.len(),
231            result.branch_mapping.len()
232        );
233
234        if result.success {
235            info!("✅ Rebase completed successfully");
236        } else {
237            warn!("❌ Rebase failed: {:?}", result.error);
238        }
239
240        Ok(result)
241    }
242
243    /// Rebase using cherry-pick strategy
244    fn rebase_with_cherry_pick(&mut self, stack: &Stack) -> Result<RebaseResult> {
245        info!("Rebasing stack '{}' using cherry-pick strategy", stack.name);
246
247        let mut result = RebaseResult {
248            success: true,
249            branch_mapping: HashMap::new(),
250            conflicts: Vec::new(),
251            new_commits: Vec::new(),
252            error: None,
253            summary: String::new(),
254        };
255
256        let target_base = self
257            .options
258            .target_base
259            .as_ref()
260            .unwrap_or(&stack.base_branch);
261
262        // Create a temporary rebase branch
263        let rebase_branch = format!("{}-rebase-{}", stack.name, Utc::now().timestamp());
264        self.git_repo
265            .create_branch(&rebase_branch, Some(target_base))?;
266        self.git_repo.checkout_branch(&rebase_branch)?;
267
268        // Cherry-pick all commits in order
269        for entry in &stack.entries {
270            match self.cherry_pick_commit(&entry.commit_hash) {
271                Ok(new_commit_hash) => {
272                    result.new_commits.push(new_commit_hash);
273                }
274                Err(e) => {
275                    result.conflicts.push(entry.commit_hash.clone());
276                    if !self.auto_resolve_conflicts(&entry.commit_hash)? {
277                        result.success = false;
278                        result.error = Some(format!(
279                            "Unresolved conflict in {}: {}",
280                            entry.commit_hash, e
281                        ));
282                        break;
283                    }
284                }
285            }
286        }
287
288        if result.success {
289            // Replace the original branches with the rebased versions
290            for entry in &stack.entries {
291                let new_branch = format!("{}-rebased", entry.branch);
292                self.git_repo
293                    .create_branch(&new_branch, Some(&rebase_branch))?;
294                result
295                    .branch_mapping
296                    .insert(entry.branch.clone(), new_branch);
297            }
298        }
299
300        result.summary = format!(
301            "Cherry-picked {} commits onto new base. {} conflicts resolved.",
302            result.new_commits.len(),
303            result.conflicts.len()
304        );
305
306        Ok(result)
307    }
308
309    /// Rebase using three-way merge strategy
310    fn rebase_with_three_way_merge(&mut self, stack: &Stack) -> Result<RebaseResult> {
311        info!(
312            "Rebasing stack '{}' using three-way merge strategy",
313            stack.name
314        );
315
316        let mut result = RebaseResult {
317            success: true,
318            branch_mapping: HashMap::new(),
319            conflicts: Vec::new(),
320            new_commits: Vec::new(),
321            error: None,
322            summary: String::new(),
323        };
324
325        // Implementation for three-way merge strategy
326        result.summary = "Three-way merge strategy implemented".to_string();
327
328        Ok(result)
329    }
330
331    /// Interactive rebase with user input
332    fn rebase_interactive(&mut self, stack: &Stack) -> Result<RebaseResult> {
333        info!("Starting interactive rebase for stack '{}'", stack.name);
334
335        let mut result = RebaseResult {
336            success: true,
337            branch_mapping: HashMap::new(),
338            conflicts: Vec::new(),
339            new_commits: Vec::new(),
340            error: None,
341            summary: String::new(),
342        };
343
344        println!("🔄 Interactive Rebase for Stack: {}", stack.name);
345        println!("   Base branch: {}", stack.base_branch);
346        println!("   Entries: {}", stack.entries.len());
347
348        if self.options.interactive {
349            println!("\nChoose action for each commit:");
350            println!("  (p)ick   - apply the commit");
351            println!("  (s)kip   - skip this commit");
352            println!("  (e)dit   - edit the commit message");
353            println!("  (q)uit   - abort the rebase");
354        }
355
356        // For now, automatically pick all commits
357        // In a real implementation, this would prompt the user
358        for entry in &stack.entries {
359            println!(
360                "  {} {} - {}",
361                entry.short_hash(),
362                entry.branch,
363                entry.short_message(50)
364            );
365
366            // Auto-pick for demo purposes
367            match self.cherry_pick_commit(&entry.commit_hash) {
368                Ok(new_commit) => result.new_commits.push(new_commit),
369                Err(_) => result.conflicts.push(entry.commit_hash.clone()),
370            }
371        }
372
373        result.summary = format!(
374            "Interactive rebase processed {} commits",
375            stack.entries.len()
376        );
377        Ok(result)
378    }
379
380    /// Generate a versioned branch name
381    fn generate_versioned_branch_name(&self, original_branch: &str) -> Result<String> {
382        let mut version = 2;
383        let base_name = if original_branch.ends_with("-v1") {
384            original_branch.trim_end_matches("-v1")
385        } else {
386            original_branch
387        };
388
389        loop {
390            let candidate = format!("{base_name}-v{version}");
391            if !self.git_repo.branch_exists(&candidate) {
392                return Ok(candidate);
393            }
394            version += 1;
395
396            if version > 100 {
397                return Err(CascadeError::branch("Too many branch versions".to_string()));
398            }
399        }
400    }
401
402    /// Cherry-pick a commit onto the current branch
403    fn cherry_pick_commit(&self, commit_hash: &str) -> Result<String> {
404        debug!("Cherry-picking commit {}", commit_hash);
405
406        // Use the real cherry-pick implementation from GitRepository
407        match self.git_repo.cherry_pick(commit_hash) {
408            Ok(new_commit_hash) => {
409                info!("✅ Cherry-picked {} -> {}", commit_hash, new_commit_hash);
410
411                // Check for any leftover staged changes after successful cherry-pick
412                match self.git_repo.get_staged_files() {
413                    Ok(staged_files) if !staged_files.is_empty() => {
414                        warn!(
415                            "Found {} staged files after successful cherry-pick. Committing them.",
416                            staged_files.len()
417                        );
418
419                        // Commit any leftover staged changes
420                        let cleanup_message =
421                            format!("Cleanup after cherry-pick {}", &commit_hash[..8]);
422                        match self.git_repo.commit_staged_changes(&cleanup_message) {
423                            Ok(Some(cleanup_commit)) => {
424                                info!(
425                                    "✅ Committed {} leftover staged files as {}",
426                                    staged_files.len(),
427                                    &cleanup_commit[..8]
428                                );
429                                Ok(new_commit_hash) // Return the original cherry-pick commit
430                            }
431                            Ok(None) => {
432                                // Staged files disappeared, this is fine
433                                Ok(new_commit_hash)
434                            }
435                            Err(commit_err) => {
436                                warn!("Failed to commit leftover staged changes: {}", commit_err);
437                                // Don't fail the whole operation for this
438                                Ok(new_commit_hash)
439                            }
440                        }
441                    }
442                    _ => {
443                        // No staged changes, normal case
444                        Ok(new_commit_hash)
445                    }
446                }
447            }
448            Err(e) => {
449                // Check if there are staged changes that need to be committed
450                match self.git_repo.get_staged_files() {
451                    Ok(staged_files) if !staged_files.is_empty() => {
452                        warn!(
453                            "Cherry-pick failed but found {} staged files. Attempting to commit them.",
454                            staged_files.len()
455                        );
456
457                        // Try to commit the staged changes
458                        let commit_message =
459                            format!("Cherry-pick (partial): {}", &commit_hash[..8]);
460                        match self.git_repo.commit_staged_changes(&commit_message) {
461                            Ok(Some(commit_hash)) => {
462                                info!("✅ Committed staged changes as {}", &commit_hash[..8]);
463                                Ok(commit_hash)
464                            }
465                            Ok(None) => {
466                                // No staged changes after all
467                                Err(e)
468                            }
469                            Err(commit_err) => {
470                                warn!("Failed to commit staged changes: {}", commit_err);
471                                Err(e)
472                            }
473                        }
474                    }
475                    _ => {
476                        // No staged changes, return original error
477                        Err(e)
478                    }
479                }
480            }
481        }
482    }
483
484    /// Attempt to automatically resolve conflicts
485    fn auto_resolve_conflicts(&self, commit_hash: &str) -> Result<bool> {
486        debug!("Attempting to auto-resolve conflicts for {}", commit_hash);
487
488        // Check if there are actually conflicts
489        if !self.git_repo.has_conflicts()? {
490            return Ok(true);
491        }
492
493        let conflicted_files = self.git_repo.get_conflicted_files()?;
494
495        if conflicted_files.is_empty() {
496            return Ok(true);
497        }
498
499        info!(
500            "Found conflicts in {} files: {:?}",
501            conflicted_files.len(),
502            conflicted_files
503        );
504
505        let mut resolved_count = 0;
506        let mut failed_files = Vec::new();
507
508        for file_path in &conflicted_files {
509            match self.resolve_file_conflicts(file_path) {
510                Ok(ConflictResolution::Resolved) => {
511                    resolved_count += 1;
512                    info!("✅ Auto-resolved conflicts in {}", file_path);
513                }
514                Ok(ConflictResolution::TooComplex) => {
515                    debug!(
516                        "⚠️  Conflicts in {} are too complex for auto-resolution",
517                        file_path
518                    );
519                    failed_files.push(file_path.clone());
520                }
521                Err(e) => {
522                    warn!("❌ Failed to analyze conflicts in {}: {}", file_path, e);
523                    failed_files.push(file_path.clone());
524                }
525            }
526        }
527
528        if resolved_count > 0 {
529            info!(
530                "🎉 Auto-resolved conflicts in {}/{} files",
531                resolved_count,
532                conflicted_files.len()
533            );
534
535            // Stage all resolved files
536            self.git_repo.stage_conflict_resolved_files()?;
537        }
538
539        // Return true only if ALL conflicts were resolved
540        let all_resolved = failed_files.is_empty();
541
542        if !all_resolved {
543            info!(
544                "⚠️  {} files still need manual resolution: {:?}",
545                failed_files.len(),
546                failed_files
547            );
548        }
549
550        Ok(all_resolved)
551    }
552
553    /// Resolve conflicts in a single file using smart strategies
554    fn resolve_file_conflicts(&self, file_path: &str) -> Result<ConflictResolution> {
555        let repo_path = self.git_repo.path();
556        let full_path = repo_path.join(file_path);
557
558        // Read the file content with conflict markers
559        let content = std::fs::read_to_string(&full_path)
560            .map_err(|e| CascadeError::config(format!("Failed to read file {file_path}: {e}")))?;
561
562        // Parse conflicts from the file
563        let conflicts = self.parse_conflict_markers(&content)?;
564
565        if conflicts.is_empty() {
566            // No conflict markers found - file might already be resolved
567            return Ok(ConflictResolution::Resolved);
568        }
569
570        info!(
571            "Found {} conflict regions in {}",
572            conflicts.len(),
573            file_path
574        );
575
576        // Try to resolve each conflict using our strategies
577        let mut resolved_content = content;
578        let mut any_resolved = false;
579
580        // Process conflicts in reverse order to maintain string indices
581        for conflict in conflicts.iter().rev() {
582            match self.resolve_single_conflict(conflict, file_path) {
583                Ok(Some(resolution)) => {
584                    // Replace the conflict region with the resolved content
585                    let before = &resolved_content[..conflict.start];
586                    let after = &resolved_content[conflict.end..];
587                    resolved_content = format!("{before}{resolution}{after}");
588                    any_resolved = true;
589                    debug!(
590                        "✅ Resolved conflict at lines {}-{} in {}",
591                        conflict.start_line, conflict.end_line, file_path
592                    );
593                }
594                Ok(None) => {
595                    debug!(
596                        "⚠️  Conflict at lines {}-{} in {} too complex for auto-resolution",
597                        conflict.start_line, conflict.end_line, file_path
598                    );
599                }
600                Err(e) => {
601                    debug!("❌ Failed to resolve conflict in {}: {}", file_path, e);
602                }
603            }
604        }
605
606        if any_resolved {
607            // Check if we resolved ALL conflicts in this file
608            let remaining_conflicts = self.parse_conflict_markers(&resolved_content)?;
609
610            if remaining_conflicts.is_empty() {
611                // All conflicts resolved - write the file back
612                std::fs::write(&full_path, resolved_content).map_err(|e| {
613                    CascadeError::config(format!("Failed to write resolved file {file_path}: {e}"))
614                })?;
615
616                return Ok(ConflictResolution::Resolved);
617            } else {
618                info!(
619                    "⚠️  Partially resolved conflicts in {} ({} remaining)",
620                    file_path,
621                    remaining_conflicts.len()
622                );
623            }
624        }
625
626        Ok(ConflictResolution::TooComplex)
627    }
628
629    /// Parse conflict markers from file content
630    fn parse_conflict_markers(&self, content: &str) -> Result<Vec<ConflictRegion>> {
631        let lines: Vec<&str> = content.lines().collect();
632        let mut conflicts = Vec::new();
633        let mut i = 0;
634
635        while i < lines.len() {
636            if lines[i].starts_with("<<<<<<<") {
637                // Found start of conflict
638                let start_line = i + 1;
639                let mut separator_line = None;
640                let mut end_line = None;
641
642                // Find the separator and end
643                for (j, line) in lines.iter().enumerate().skip(i + 1) {
644                    if line.starts_with("=======") {
645                        separator_line = Some(j + 1);
646                    } else if line.starts_with(">>>>>>>") {
647                        end_line = Some(j + 1);
648                        break;
649                    }
650                }
651
652                if let (Some(sep), Some(end)) = (separator_line, end_line) {
653                    // Calculate byte positions
654                    let start_pos = lines[..i].iter().map(|l| l.len() + 1).sum::<usize>();
655                    let end_pos = lines[..end].iter().map(|l| l.len() + 1).sum::<usize>();
656
657                    let our_content = lines[(i + 1)..(sep - 1)].join("\n");
658                    let their_content = lines[sep..(end - 1)].join("\n");
659
660                    conflicts.push(ConflictRegion {
661                        start: start_pos,
662                        end: end_pos,
663                        start_line,
664                        end_line: end,
665                        our_content,
666                        their_content,
667                    });
668
669                    i = end;
670                } else {
671                    i += 1;
672                }
673            } else {
674                i += 1;
675            }
676        }
677
678        Ok(conflicts)
679    }
680
681    /// Resolve a single conflict using smart strategies
682    fn resolve_single_conflict(
683        &self,
684        conflict: &ConflictRegion,
685        file_path: &str,
686    ) -> Result<Option<String>> {
687        debug!(
688            "Analyzing conflict in {} (lines {}-{})",
689            file_path, conflict.start_line, conflict.end_line
690        );
691
692        // Strategy 1: Whitespace-only differences
693        if let Some(resolved) = self.resolve_whitespace_conflict(conflict)? {
694            debug!("Resolved as whitespace-only conflict");
695            return Ok(Some(resolved));
696        }
697
698        // Strategy 2: Line ending differences
699        if let Some(resolved) = self.resolve_line_ending_conflict(conflict)? {
700            debug!("Resolved as line ending conflict");
701            return Ok(Some(resolved));
702        }
703
704        // Strategy 3: Pure addition conflicts (no overlapping changes)
705        if let Some(resolved) = self.resolve_addition_conflict(conflict)? {
706            debug!("Resolved as pure addition conflict");
707            return Ok(Some(resolved));
708        }
709
710        // Strategy 4: Import/dependency reordering
711        if let Some(resolved) = self.resolve_import_conflict(conflict, file_path)? {
712            debug!("Resolved as import reordering conflict");
713            return Ok(Some(resolved));
714        }
715
716        // No strategy could resolve this conflict
717        Ok(None)
718    }
719
720    /// Resolve conflicts that only differ by whitespace
721    fn resolve_whitespace_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
722        let our_normalized = self.normalize_whitespace(&conflict.our_content);
723        let their_normalized = self.normalize_whitespace(&conflict.their_content);
724
725        if our_normalized == their_normalized {
726            // Only whitespace differences - prefer the version with better formatting
727            let resolved =
728                if conflict.our_content.trim().len() >= conflict.their_content.trim().len() {
729                    conflict.our_content.clone()
730                } else {
731                    conflict.their_content.clone()
732                };
733
734            return Ok(Some(resolved));
735        }
736
737        Ok(None)
738    }
739
740    /// Resolve conflicts that only differ by line endings
741    fn resolve_line_ending_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
742        let our_normalized = conflict
743            .our_content
744            .replace("\r\n", "\n")
745            .replace('\r', "\n");
746        let their_normalized = conflict
747            .their_content
748            .replace("\r\n", "\n")
749            .replace('\r', "\n");
750
751        if our_normalized == their_normalized {
752            // Only line ending differences - prefer Unix line endings
753            return Ok(Some(our_normalized));
754        }
755
756        Ok(None)
757    }
758
759    /// Resolve conflicts where both sides only add lines (no overlapping edits)
760    fn resolve_addition_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
761        let our_lines: Vec<&str> = conflict.our_content.lines().collect();
762        let their_lines: Vec<&str> = conflict.their_content.lines().collect();
763
764        // Check if one side is a subset of the other (pure addition)
765        if our_lines.is_empty() {
766            return Ok(Some(conflict.their_content.clone()));
767        }
768        if their_lines.is_empty() {
769            return Ok(Some(conflict.our_content.clone()));
770        }
771
772        // Try to merge additions intelligently
773        let mut merged_lines = Vec::new();
774        let mut our_idx = 0;
775        let mut their_idx = 0;
776
777        while our_idx < our_lines.len() || their_idx < their_lines.len() {
778            if our_idx >= our_lines.len() {
779                // Only their lines left
780                merged_lines.extend_from_slice(&their_lines[their_idx..]);
781                break;
782            } else if their_idx >= their_lines.len() {
783                // Only our lines left
784                merged_lines.extend_from_slice(&our_lines[our_idx..]);
785                break;
786            } else if our_lines[our_idx] == their_lines[their_idx] {
787                // Same line - add once
788                merged_lines.push(our_lines[our_idx]);
789                our_idx += 1;
790                their_idx += 1;
791            } else {
792                // Different lines - this might be too complex
793                return Ok(None);
794            }
795        }
796
797        Ok(Some(merged_lines.join("\n")))
798    }
799
800    /// Resolve import/dependency conflicts by sorting and merging
801    fn resolve_import_conflict(
802        &self,
803        conflict: &ConflictRegion,
804        file_path: &str,
805    ) -> Result<Option<String>> {
806        // Only apply to likely import sections in common file types
807        let is_import_file = file_path.ends_with(".rs")
808            || file_path.ends_with(".py")
809            || file_path.ends_with(".js")
810            || file_path.ends_with(".ts")
811            || file_path.ends_with(".go")
812            || file_path.ends_with(".java")
813            || file_path.ends_with(".swift")
814            || file_path.ends_with(".kt")
815            || file_path.ends_with(".cs");
816
817        if !is_import_file {
818            return Ok(None);
819        }
820
821        let our_lines: Vec<&str> = conflict.our_content.lines().collect();
822        let their_lines: Vec<&str> = conflict.their_content.lines().collect();
823
824        // Check if all lines look like imports/uses
825        let our_imports = our_lines
826            .iter()
827            .all(|line| self.is_import_line(line, file_path));
828        let their_imports = their_lines
829            .iter()
830            .all(|line| self.is_import_line(line, file_path));
831
832        if our_imports && their_imports {
833            // Merge and sort imports
834            let mut all_imports: Vec<&str> = our_lines.into_iter().chain(their_lines).collect();
835            all_imports.sort();
836            all_imports.dedup();
837
838            return Ok(Some(all_imports.join("\n")));
839        }
840
841        Ok(None)
842    }
843
844    /// Check if a line looks like an import statement
845    fn is_import_line(&self, line: &str, file_path: &str) -> bool {
846        let trimmed = line.trim();
847
848        if trimmed.is_empty() {
849            return true; // Empty lines are OK in import sections
850        }
851
852        if file_path.ends_with(".rs") {
853            return trimmed.starts_with("use ") || trimmed.starts_with("extern crate");
854        } else if file_path.ends_with(".py") {
855            return trimmed.starts_with("import ") || trimmed.starts_with("from ");
856        } else if file_path.ends_with(".js") || file_path.ends_with(".ts") {
857            return trimmed.starts_with("import ")
858                || trimmed.starts_with("const ")
859                || trimmed.starts_with("require(");
860        } else if file_path.ends_with(".go") {
861            return trimmed.starts_with("import ") || trimmed == "import (" || trimmed == ")";
862        } else if file_path.ends_with(".java") {
863            return trimmed.starts_with("import ");
864        } else if file_path.ends_with(".swift") {
865            return trimmed.starts_with("import ") || trimmed.starts_with("@testable import ");
866        } else if file_path.ends_with(".kt") {
867            return trimmed.starts_with("import ") || trimmed.starts_with("@file:");
868        } else if file_path.ends_with(".cs") {
869            return trimmed.starts_with("using ") || trimmed.starts_with("extern alias ");
870        }
871
872        false
873    }
874
875    /// Normalize whitespace for comparison
876    fn normalize_whitespace(&self, content: &str) -> String {
877        content
878            .lines()
879            .map(|line| line.trim())
880            .filter(|line| !line.is_empty())
881            .collect::<Vec<_>>()
882            .join("\n")
883    }
884
885    /// Update a stack entry with new commit information
886    /// NOTE: We keep the original branch name to preserve PR mapping, only update commit hash
887    fn update_stack_entry(
888        &mut self,
889        stack_id: Uuid,
890        entry_id: &Uuid,
891        _new_branch: &str,
892        new_commit_hash: &str,
893    ) -> Result<()> {
894        debug!(
895            "Updating entry {} in stack {} with new commit {}",
896            entry_id, stack_id, new_commit_hash
897        );
898
899        // Get the stack and update the entry
900        let stack = self
901            .stack_manager
902            .get_stack_mut(&stack_id)
903            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
904
905        // Find and update the entry
906        if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == *entry_id) {
907            debug!(
908                "Found entry {} - updating commit from '{}' to '{}' (keeping original branch '{}')",
909                entry_id, entry.commit_hash, new_commit_hash, entry.branch
910            );
911
912            // CRITICAL: Keep the original branch name to preserve PR mapping
913            // Only update the commit hash to point to the new rebased commit
914            entry.commit_hash = new_commit_hash.to_string();
915
916            // Note: Stack will be saved by the caller (StackManager) after rebase completes
917
918            debug!(
919                "Successfully updated entry {} in stack {}",
920                entry_id, stack_id
921            );
922            Ok(())
923        } else {
924            Err(CascadeError::config(format!(
925                "Entry {entry_id} not found in stack {stack_id}"
926            )))
927        }
928    }
929
930    /// Pull latest changes from remote
931    fn pull_latest_changes(&self, branch: &str) -> Result<()> {
932        info!("Pulling latest changes for branch {}", branch);
933
934        // First try to fetch (this might fail if no remote exists)
935        match self.git_repo.fetch() {
936            Ok(_) => {
937                debug!("Fetch successful");
938                // Now try to pull the specific branch
939                match self.git_repo.pull(branch) {
940                    Ok(_) => {
941                        info!("Pull completed successfully for {}", branch);
942                        Ok(())
943                    }
944                    Err(e) => {
945                        warn!("Pull failed for {}: {}", branch, e);
946                        // Don't fail the entire rebase for pull issues
947                        Ok(())
948                    }
949                }
950            }
951            Err(e) => {
952                warn!("Fetch failed: {}", e);
953                // Don't fail if there's no remote configured
954                Ok(())
955            }
956        }
957    }
958
959    /// Check if rebase is in progress
960    pub fn is_rebase_in_progress(&self) -> bool {
961        // Check for git rebase state files
962        let git_dir = self.git_repo.path().join(".git");
963        git_dir.join("REBASE_HEAD").exists()
964            || git_dir.join("rebase-merge").exists()
965            || git_dir.join("rebase-apply").exists()
966    }
967
968    /// Abort an in-progress rebase
969    pub fn abort_rebase(&self) -> Result<()> {
970        info!("Aborting rebase operation");
971
972        let git_dir = self.git_repo.path().join(".git");
973
974        // Clean up rebase state files
975        if git_dir.join("REBASE_HEAD").exists() {
976            std::fs::remove_file(git_dir.join("REBASE_HEAD")).map_err(|e| {
977                CascadeError::Git(git2::Error::from_str(&format!(
978                    "Failed to clean rebase state: {e}"
979                )))
980            })?;
981        }
982
983        if git_dir.join("rebase-merge").exists() {
984            std::fs::remove_dir_all(git_dir.join("rebase-merge")).map_err(|e| {
985                CascadeError::Git(git2::Error::from_str(&format!(
986                    "Failed to clean rebase-merge: {e}"
987                )))
988            })?;
989        }
990
991        if git_dir.join("rebase-apply").exists() {
992            std::fs::remove_dir_all(git_dir.join("rebase-apply")).map_err(|e| {
993                CascadeError::Git(git2::Error::from_str(&format!(
994                    "Failed to clean rebase-apply: {e}"
995                )))
996            })?;
997        }
998
999        info!("Rebase aborted successfully");
1000        Ok(())
1001    }
1002
1003    /// Continue an in-progress rebase after conflict resolution
1004    pub fn continue_rebase(&self) -> Result<()> {
1005        info!("Continuing rebase operation");
1006
1007        // Check if there are still conflicts
1008        if self.git_repo.has_conflicts()? {
1009            return Err(CascadeError::branch(
1010                "Cannot continue rebase: there are unresolved conflicts. Resolve conflicts and stage files first.".to_string()
1011            ));
1012        }
1013
1014        // Stage resolved files
1015        self.git_repo.stage_conflict_resolved_files()?;
1016
1017        info!("Rebase continued successfully");
1018        Ok(())
1019    }
1020}
1021
1022impl RebaseResult {
1023    /// Get a summary of the rebase operation
1024    pub fn get_summary(&self) -> String {
1025        if self.success {
1026            format!("✅ {}", self.summary)
1027        } else {
1028            format!(
1029                "❌ Rebase failed: {}",
1030                self.error.as_deref().unwrap_or("Unknown error")
1031            )
1032        }
1033    }
1034
1035    /// Check if any conflicts occurred
1036    pub fn has_conflicts(&self) -> bool {
1037        !self.conflicts.is_empty()
1038    }
1039
1040    /// Get the number of successful operations
1041    pub fn success_count(&self) -> usize {
1042        self.new_commits.len()
1043    }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use super::*;
1049    use std::path::PathBuf;
1050    use std::process::Command;
1051    use tempfile::TempDir;
1052
1053    #[allow(dead_code)]
1054    fn create_test_repo() -> (TempDir, PathBuf) {
1055        let temp_dir = TempDir::new().unwrap();
1056        let repo_path = temp_dir.path().to_path_buf();
1057
1058        // Initialize git repository
1059        Command::new("git")
1060            .args(["init"])
1061            .current_dir(&repo_path)
1062            .output()
1063            .unwrap();
1064        Command::new("git")
1065            .args(["config", "user.name", "Test"])
1066            .current_dir(&repo_path)
1067            .output()
1068            .unwrap();
1069        Command::new("git")
1070            .args(["config", "user.email", "test@test.com"])
1071            .current_dir(&repo_path)
1072            .output()
1073            .unwrap();
1074
1075        // Create initial commit
1076        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
1077        Command::new("git")
1078            .args(["add", "."])
1079            .current_dir(&repo_path)
1080            .output()
1081            .unwrap();
1082        Command::new("git")
1083            .args(["commit", "-m", "Initial"])
1084            .current_dir(&repo_path)
1085            .output()
1086            .unwrap();
1087
1088        (temp_dir, repo_path)
1089    }
1090
1091    #[test]
1092    fn test_conflict_region_creation() {
1093        let region = ConflictRegion {
1094            start: 0,
1095            end: 50,
1096            start_line: 1,
1097            end_line: 3,
1098            our_content: "function test() {\n    return true;\n}".to_string(),
1099            their_content: "function test() {\n  return true;\n}".to_string(),
1100        };
1101
1102        assert_eq!(region.start_line, 1);
1103        assert_eq!(region.end_line, 3);
1104        assert!(region.our_content.contains("return true"));
1105        assert!(region.their_content.contains("return true"));
1106    }
1107
1108    #[test]
1109    fn test_rebase_strategies() {
1110        assert_eq!(
1111            RebaseStrategy::BranchVersioning,
1112            RebaseStrategy::BranchVersioning
1113        );
1114        assert_eq!(RebaseStrategy::CherryPick, RebaseStrategy::CherryPick);
1115        assert_eq!(RebaseStrategy::ThreeWayMerge, RebaseStrategy::ThreeWayMerge);
1116        assert_eq!(RebaseStrategy::Interactive, RebaseStrategy::Interactive);
1117    }
1118
1119    #[test]
1120    fn test_rebase_options() {
1121        let options = RebaseOptions::default();
1122        assert_eq!(options.strategy, RebaseStrategy::BranchVersioning);
1123        assert!(!options.interactive);
1124        assert!(options.auto_resolve);
1125        assert_eq!(options.max_retries, 3);
1126    }
1127
1128    #[test]
1129    fn test_rebase_result() {
1130        let result = RebaseResult {
1131            success: true,
1132            branch_mapping: std::collections::HashMap::new(),
1133            conflicts: vec!["abc123".to_string()],
1134            new_commits: vec!["def456".to_string()],
1135            error: None,
1136            summary: "Test summary".to_string(),
1137        };
1138
1139        assert!(result.success);
1140        assert!(result.has_conflicts());
1141        assert_eq!(result.success_count(), 1);
1142    }
1143
1144    #[test]
1145    fn test_import_line_detection() {
1146        let (_temp_dir, repo_path) = create_test_repo();
1147        let git_repo = crate::git::GitRepository::open(&repo_path).unwrap();
1148        let stack_manager = crate::stack::StackManager::new(&repo_path).unwrap();
1149        let options = RebaseOptions::default();
1150        let rebase_manager = RebaseManager::new(stack_manager, git_repo, options);
1151
1152        // Test Swift import detection
1153        assert!(rebase_manager.is_import_line("import Foundation", "test.swift"));
1154        assert!(rebase_manager.is_import_line("@testable import MyModule", "test.swift"));
1155        assert!(!rebase_manager.is_import_line("class MyClass {", "test.swift"));
1156
1157        // Test Kotlin import detection
1158        assert!(rebase_manager.is_import_line("import kotlin.collections.*", "test.kt"));
1159        assert!(rebase_manager.is_import_line("@file:JvmName(\"Utils\")", "test.kt"));
1160        assert!(!rebase_manager.is_import_line("fun myFunction() {", "test.kt"));
1161
1162        // Test C# import detection
1163        assert!(rebase_manager.is_import_line("using System;", "test.cs"));
1164        assert!(rebase_manager.is_import_line("using System.Collections.Generic;", "test.cs"));
1165        assert!(rebase_manager.is_import_line("extern alias GridV1;", "test.cs"));
1166        assert!(!rebase_manager.is_import_line("namespace MyNamespace {", "test.cs"));
1167
1168        // Test empty lines are allowed in import sections
1169        assert!(rebase_manager.is_import_line("", "test.swift"));
1170        assert!(rebase_manager.is_import_line("   ", "test.kt"));
1171        assert!(rebase_manager.is_import_line("", "test.cs"));
1172    }
1173}