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