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);
192                    result
193                        .branch_mapping
194                        .insert(entry.branch.clone(), new_branch.clone());
195
196                    // Update stack entry with new branch
197                    self.update_stack_entry(stack.id, &entry.id, &new_branch)?;
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        self.git_repo.cherry_pick(commit_hash)
408    }
409
410    /// Attempt to automatically resolve conflicts
411    fn auto_resolve_conflicts(&self, commit_hash: &str) -> Result<bool> {
412        debug!("Attempting to auto-resolve conflicts for {}", commit_hash);
413
414        // Check if there are actually conflicts
415        if !self.git_repo.has_conflicts()? {
416            return Ok(true);
417        }
418
419        let conflicted_files = self.git_repo.get_conflicted_files()?;
420
421        if conflicted_files.is_empty() {
422            return Ok(true);
423        }
424
425        info!(
426            "Found conflicts in {} files: {:?}",
427            conflicted_files.len(),
428            conflicted_files
429        );
430
431        let mut resolved_count = 0;
432        let mut failed_files = Vec::new();
433
434        for file_path in &conflicted_files {
435            match self.resolve_file_conflicts(file_path) {
436                Ok(ConflictResolution::Resolved) => {
437                    resolved_count += 1;
438                    info!("✅ Auto-resolved conflicts in {}", file_path);
439                }
440                Ok(ConflictResolution::TooComplex) => {
441                    debug!(
442                        "⚠️  Conflicts in {} are too complex for auto-resolution",
443                        file_path
444                    );
445                    failed_files.push(file_path.clone());
446                }
447                Err(e) => {
448                    warn!("❌ Failed to analyze conflicts in {}: {}", file_path, e);
449                    failed_files.push(file_path.clone());
450                }
451            }
452        }
453
454        if resolved_count > 0 {
455            info!(
456                "🎉 Auto-resolved conflicts in {}/{} files",
457                resolved_count,
458                conflicted_files.len()
459            );
460
461            // Stage all resolved files
462            self.git_repo.stage_conflict_resolved_files()?;
463        }
464
465        // Return true only if ALL conflicts were resolved
466        let all_resolved = failed_files.is_empty();
467
468        if !all_resolved {
469            info!(
470                "⚠️  {} files still need manual resolution: {:?}",
471                failed_files.len(),
472                failed_files
473            );
474        }
475
476        Ok(all_resolved)
477    }
478
479    /// Resolve conflicts in a single file using smart strategies
480    fn resolve_file_conflicts(&self, file_path: &str) -> Result<ConflictResolution> {
481        let repo_path = self.git_repo.path();
482        let full_path = repo_path.join(file_path);
483
484        // Read the file content with conflict markers
485        let content = std::fs::read_to_string(&full_path)
486            .map_err(|e| CascadeError::config(format!("Failed to read file {file_path}: {e}")))?;
487
488        // Parse conflicts from the file
489        let conflicts = self.parse_conflict_markers(&content)?;
490
491        if conflicts.is_empty() {
492            // No conflict markers found - file might already be resolved
493            return Ok(ConflictResolution::Resolved);
494        }
495
496        info!(
497            "Found {} conflict regions in {}",
498            conflicts.len(),
499            file_path
500        );
501
502        // Try to resolve each conflict using our strategies
503        let mut resolved_content = content;
504        let mut any_resolved = false;
505
506        // Process conflicts in reverse order to maintain string indices
507        for conflict in conflicts.iter().rev() {
508            match self.resolve_single_conflict(conflict, file_path) {
509                Ok(Some(resolution)) => {
510                    // Replace the conflict region with the resolved content
511                    let before = &resolved_content[..conflict.start];
512                    let after = &resolved_content[conflict.end..];
513                    resolved_content = format!("{before}{resolution}{after}");
514                    any_resolved = true;
515                    debug!(
516                        "✅ Resolved conflict at lines {}-{} in {}",
517                        conflict.start_line, conflict.end_line, file_path
518                    );
519                }
520                Ok(None) => {
521                    debug!(
522                        "⚠️  Conflict at lines {}-{} in {} too complex for auto-resolution",
523                        conflict.start_line, conflict.end_line, file_path
524                    );
525                }
526                Err(e) => {
527                    debug!("❌ Failed to resolve conflict in {}: {}", file_path, e);
528                }
529            }
530        }
531
532        if any_resolved {
533            // Check if we resolved ALL conflicts in this file
534            let remaining_conflicts = self.parse_conflict_markers(&resolved_content)?;
535
536            if remaining_conflicts.is_empty() {
537                // All conflicts resolved - write the file back
538                std::fs::write(&full_path, resolved_content).map_err(|e| {
539                    CascadeError::config(format!("Failed to write resolved file {file_path}: {e}"))
540                })?;
541
542                return Ok(ConflictResolution::Resolved);
543            } else {
544                info!(
545                    "⚠️  Partially resolved conflicts in {} ({} remaining)",
546                    file_path,
547                    remaining_conflicts.len()
548                );
549            }
550        }
551
552        Ok(ConflictResolution::TooComplex)
553    }
554
555    /// Parse conflict markers from file content
556    fn parse_conflict_markers(&self, content: &str) -> Result<Vec<ConflictRegion>> {
557        let lines: Vec<&str> = content.lines().collect();
558        let mut conflicts = Vec::new();
559        let mut i = 0;
560
561        while i < lines.len() {
562            if lines[i].starts_with("<<<<<<<") {
563                // Found start of conflict
564                let start_line = i + 1;
565                let mut separator_line = None;
566                let mut end_line = None;
567
568                // Find the separator and end
569                for (j, line) in lines.iter().enumerate().skip(i + 1) {
570                    if line.starts_with("=======") {
571                        separator_line = Some(j + 1);
572                    } else if line.starts_with(">>>>>>>") {
573                        end_line = Some(j + 1);
574                        break;
575                    }
576                }
577
578                if let (Some(sep), Some(end)) = (separator_line, end_line) {
579                    // Calculate byte positions
580                    let start_pos = lines[..i].iter().map(|l| l.len() + 1).sum::<usize>();
581                    let end_pos = lines[..end].iter().map(|l| l.len() + 1).sum::<usize>();
582
583                    let our_content = lines[(i + 1)..(sep - 1)].join("\n");
584                    let their_content = lines[sep..(end - 1)].join("\n");
585
586                    conflicts.push(ConflictRegion {
587                        start: start_pos,
588                        end: end_pos,
589                        start_line,
590                        end_line: end,
591                        our_content,
592                        their_content,
593                    });
594
595                    i = end;
596                } else {
597                    i += 1;
598                }
599            } else {
600                i += 1;
601            }
602        }
603
604        Ok(conflicts)
605    }
606
607    /// Resolve a single conflict using smart strategies
608    fn resolve_single_conflict(
609        &self,
610        conflict: &ConflictRegion,
611        file_path: &str,
612    ) -> Result<Option<String>> {
613        debug!(
614            "Analyzing conflict in {} (lines {}-{})",
615            file_path, conflict.start_line, conflict.end_line
616        );
617
618        // Strategy 1: Whitespace-only differences
619        if let Some(resolved) = self.resolve_whitespace_conflict(conflict)? {
620            debug!("Resolved as whitespace-only conflict");
621            return Ok(Some(resolved));
622        }
623
624        // Strategy 2: Line ending differences
625        if let Some(resolved) = self.resolve_line_ending_conflict(conflict)? {
626            debug!("Resolved as line ending conflict");
627            return Ok(Some(resolved));
628        }
629
630        // Strategy 3: Pure addition conflicts (no overlapping changes)
631        if let Some(resolved) = self.resolve_addition_conflict(conflict)? {
632            debug!("Resolved as pure addition conflict");
633            return Ok(Some(resolved));
634        }
635
636        // Strategy 4: Import/dependency reordering
637        if let Some(resolved) = self.resolve_import_conflict(conflict, file_path)? {
638            debug!("Resolved as import reordering conflict");
639            return Ok(Some(resolved));
640        }
641
642        // No strategy could resolve this conflict
643        Ok(None)
644    }
645
646    /// Resolve conflicts that only differ by whitespace
647    fn resolve_whitespace_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
648        let our_normalized = self.normalize_whitespace(&conflict.our_content);
649        let their_normalized = self.normalize_whitespace(&conflict.their_content);
650
651        if our_normalized == their_normalized {
652            // Only whitespace differences - prefer the version with better formatting
653            let resolved =
654                if conflict.our_content.trim().len() >= conflict.their_content.trim().len() {
655                    conflict.our_content.clone()
656                } else {
657                    conflict.their_content.clone()
658                };
659
660            return Ok(Some(resolved));
661        }
662
663        Ok(None)
664    }
665
666    /// Resolve conflicts that only differ by line endings
667    fn resolve_line_ending_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
668        let our_normalized = conflict
669            .our_content
670            .replace("\r\n", "\n")
671            .replace('\r', "\n");
672        let their_normalized = conflict
673            .their_content
674            .replace("\r\n", "\n")
675            .replace('\r', "\n");
676
677        if our_normalized == their_normalized {
678            // Only line ending differences - prefer Unix line endings
679            return Ok(Some(our_normalized));
680        }
681
682        Ok(None)
683    }
684
685    /// Resolve conflicts where both sides only add lines (no overlapping edits)
686    fn resolve_addition_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
687        let our_lines: Vec<&str> = conflict.our_content.lines().collect();
688        let their_lines: Vec<&str> = conflict.their_content.lines().collect();
689
690        // Check if one side is a subset of the other (pure addition)
691        if our_lines.is_empty() {
692            return Ok(Some(conflict.their_content.clone()));
693        }
694        if their_lines.is_empty() {
695            return Ok(Some(conflict.our_content.clone()));
696        }
697
698        // Try to merge additions intelligently
699        let mut merged_lines = Vec::new();
700        let mut our_idx = 0;
701        let mut their_idx = 0;
702
703        while our_idx < our_lines.len() || their_idx < their_lines.len() {
704            if our_idx >= our_lines.len() {
705                // Only their lines left
706                merged_lines.extend_from_slice(&their_lines[their_idx..]);
707                break;
708            } else if their_idx >= their_lines.len() {
709                // Only our lines left
710                merged_lines.extend_from_slice(&our_lines[our_idx..]);
711                break;
712            } else if our_lines[our_idx] == their_lines[their_idx] {
713                // Same line - add once
714                merged_lines.push(our_lines[our_idx]);
715                our_idx += 1;
716                their_idx += 1;
717            } else {
718                // Different lines - this might be too complex
719                return Ok(None);
720            }
721        }
722
723        Ok(Some(merged_lines.join("\n")))
724    }
725
726    /// Resolve import/dependency conflicts by sorting and merging
727    fn resolve_import_conflict(
728        &self,
729        conflict: &ConflictRegion,
730        file_path: &str,
731    ) -> Result<Option<String>> {
732        // Only apply to likely import sections in common file types
733        let is_import_file = file_path.ends_with(".rs")
734            || file_path.ends_with(".py")
735            || file_path.ends_with(".js")
736            || file_path.ends_with(".ts")
737            || file_path.ends_with(".go")
738            || file_path.ends_with(".java")
739            || file_path.ends_with(".swift")
740            || file_path.ends_with(".kt")
741            || file_path.ends_with(".cs");
742
743        if !is_import_file {
744            return Ok(None);
745        }
746
747        let our_lines: Vec<&str> = conflict.our_content.lines().collect();
748        let their_lines: Vec<&str> = conflict.their_content.lines().collect();
749
750        // Check if all lines look like imports/uses
751        let our_imports = our_lines
752            .iter()
753            .all(|line| self.is_import_line(line, file_path));
754        let their_imports = their_lines
755            .iter()
756            .all(|line| self.is_import_line(line, file_path));
757
758        if our_imports && their_imports {
759            // Merge and sort imports
760            let mut all_imports: Vec<&str> = our_lines.into_iter().chain(their_lines).collect();
761            all_imports.sort();
762            all_imports.dedup();
763
764            return Ok(Some(all_imports.join("\n")));
765        }
766
767        Ok(None)
768    }
769
770    /// Check if a line looks like an import statement
771    fn is_import_line(&self, line: &str, file_path: &str) -> bool {
772        let trimmed = line.trim();
773
774        if trimmed.is_empty() {
775            return true; // Empty lines are OK in import sections
776        }
777
778        if file_path.ends_with(".rs") {
779            return trimmed.starts_with("use ") || trimmed.starts_with("extern crate");
780        } else if file_path.ends_with(".py") {
781            return trimmed.starts_with("import ") || trimmed.starts_with("from ");
782        } else if file_path.ends_with(".js") || file_path.ends_with(".ts") {
783            return trimmed.starts_with("import ")
784                || trimmed.starts_with("const ")
785                || trimmed.starts_with("require(");
786        } else if file_path.ends_with(".go") {
787            return trimmed.starts_with("import ") || trimmed == "import (" || trimmed == ")";
788        } else if file_path.ends_with(".java") {
789            return trimmed.starts_with("import ");
790        } else if file_path.ends_with(".swift") {
791            return trimmed.starts_with("import ") || trimmed.starts_with("@testable import ");
792        } else if file_path.ends_with(".kt") {
793            return trimmed.starts_with("import ") || trimmed.starts_with("@file:");
794        } else if file_path.ends_with(".cs") {
795            return trimmed.starts_with("using ") || trimmed.starts_with("extern alias ");
796        }
797
798        false
799    }
800
801    /// Normalize whitespace for comparison
802    fn normalize_whitespace(&self, content: &str) -> String {
803        content
804            .lines()
805            .map(|line| line.trim())
806            .filter(|line| !line.is_empty())
807            .collect::<Vec<_>>()
808            .join("\n")
809    }
810
811    /// Update a stack entry with new branch information
812    fn update_stack_entry(
813        &mut self,
814        stack_id: Uuid,
815        entry_id: &Uuid,
816        new_branch: &str,
817    ) -> Result<()> {
818        // This would update the stack entry in the stack manager
819        // For now, just log the operation
820        debug!(
821            "Updating entry {} in stack {} with new branch {}",
822            entry_id, stack_id, new_branch
823        );
824        Ok(())
825    }
826
827    /// Pull latest changes from remote
828    fn pull_latest_changes(&self, branch: &str) -> Result<()> {
829        info!("Pulling latest changes for branch {}", branch);
830
831        // First try to fetch (this might fail if no remote exists)
832        match self.git_repo.fetch() {
833            Ok(_) => {
834                debug!("Fetch successful");
835                // Now try to pull the specific branch
836                match self.git_repo.pull(branch) {
837                    Ok(_) => {
838                        info!("Pull completed successfully for {}", branch);
839                        Ok(())
840                    }
841                    Err(e) => {
842                        warn!("Pull failed for {}: {}", branch, e);
843                        // Don't fail the entire rebase for pull issues
844                        Ok(())
845                    }
846                }
847            }
848            Err(e) => {
849                warn!("Fetch failed: {}", e);
850                // Don't fail if there's no remote configured
851                Ok(())
852            }
853        }
854    }
855
856    /// Check if rebase is in progress
857    pub fn is_rebase_in_progress(&self) -> bool {
858        // Check for git rebase state files
859        let git_dir = self.git_repo.path().join(".git");
860        git_dir.join("REBASE_HEAD").exists()
861            || git_dir.join("rebase-merge").exists()
862            || git_dir.join("rebase-apply").exists()
863    }
864
865    /// Abort an in-progress rebase
866    pub fn abort_rebase(&self) -> Result<()> {
867        info!("Aborting rebase operation");
868
869        let git_dir = self.git_repo.path().join(".git");
870
871        // Clean up rebase state files
872        if git_dir.join("REBASE_HEAD").exists() {
873            std::fs::remove_file(git_dir.join("REBASE_HEAD")).map_err(|e| {
874                CascadeError::Git(git2::Error::from_str(&format!(
875                    "Failed to clean rebase state: {e}"
876                )))
877            })?;
878        }
879
880        if git_dir.join("rebase-merge").exists() {
881            std::fs::remove_dir_all(git_dir.join("rebase-merge")).map_err(|e| {
882                CascadeError::Git(git2::Error::from_str(&format!(
883                    "Failed to clean rebase-merge: {e}"
884                )))
885            })?;
886        }
887
888        if git_dir.join("rebase-apply").exists() {
889            std::fs::remove_dir_all(git_dir.join("rebase-apply")).map_err(|e| {
890                CascadeError::Git(git2::Error::from_str(&format!(
891                    "Failed to clean rebase-apply: {e}"
892                )))
893            })?;
894        }
895
896        info!("Rebase aborted successfully");
897        Ok(())
898    }
899
900    /// Continue an in-progress rebase after conflict resolution
901    pub fn continue_rebase(&self) -> Result<()> {
902        info!("Continuing rebase operation");
903
904        // Check if there are still conflicts
905        if self.git_repo.has_conflicts()? {
906            return Err(CascadeError::branch(
907                "Cannot continue rebase: there are unresolved conflicts. Resolve conflicts and stage files first.".to_string()
908            ));
909        }
910
911        // Stage resolved files
912        self.git_repo.stage_conflict_resolved_files()?;
913
914        info!("Rebase continued successfully");
915        Ok(())
916    }
917}
918
919impl RebaseResult {
920    /// Get a summary of the rebase operation
921    pub fn get_summary(&self) -> String {
922        if self.success {
923            format!("✅ {}", self.summary)
924        } else {
925            format!(
926                "❌ Rebase failed: {}",
927                self.error.as_deref().unwrap_or("Unknown error")
928            )
929        }
930    }
931
932    /// Check if any conflicts occurred
933    pub fn has_conflicts(&self) -> bool {
934        !self.conflicts.is_empty()
935    }
936
937    /// Get the number of successful operations
938    pub fn success_count(&self) -> usize {
939        self.new_commits.len()
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946    use std::path::PathBuf;
947    use std::process::Command;
948    use tempfile::TempDir;
949
950    #[allow(dead_code)]
951    fn create_test_repo() -> (TempDir, PathBuf) {
952        let temp_dir = TempDir::new().unwrap();
953        let repo_path = temp_dir.path().to_path_buf();
954
955        // Initialize git repository
956        Command::new("git")
957            .args(["init"])
958            .current_dir(&repo_path)
959            .output()
960            .unwrap();
961        Command::new("git")
962            .args(["config", "user.name", "Test"])
963            .current_dir(&repo_path)
964            .output()
965            .unwrap();
966        Command::new("git")
967            .args(["config", "user.email", "test@test.com"])
968            .current_dir(&repo_path)
969            .output()
970            .unwrap();
971
972        // Create initial commit
973        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
974        Command::new("git")
975            .args(["add", "."])
976            .current_dir(&repo_path)
977            .output()
978            .unwrap();
979        Command::new("git")
980            .args(["commit", "-m", "Initial"])
981            .current_dir(&repo_path)
982            .output()
983            .unwrap();
984
985        (temp_dir, repo_path)
986    }
987
988    #[test]
989    fn test_conflict_region_creation() {
990        let region = ConflictRegion {
991            start: 0,
992            end: 50,
993            start_line: 1,
994            end_line: 3,
995            our_content: "function test() {\n    return true;\n}".to_string(),
996            their_content: "function test() {\n  return true;\n}".to_string(),
997        };
998
999        assert_eq!(region.start_line, 1);
1000        assert_eq!(region.end_line, 3);
1001        assert!(region.our_content.contains("return true"));
1002        assert!(region.their_content.contains("return true"));
1003    }
1004
1005    #[test]
1006    fn test_rebase_strategies() {
1007        assert_eq!(
1008            RebaseStrategy::BranchVersioning,
1009            RebaseStrategy::BranchVersioning
1010        );
1011        assert_eq!(RebaseStrategy::CherryPick, RebaseStrategy::CherryPick);
1012        assert_eq!(RebaseStrategy::ThreeWayMerge, RebaseStrategy::ThreeWayMerge);
1013        assert_eq!(RebaseStrategy::Interactive, RebaseStrategy::Interactive);
1014    }
1015
1016    #[test]
1017    fn test_rebase_options() {
1018        let options = RebaseOptions::default();
1019        assert_eq!(options.strategy, RebaseStrategy::BranchVersioning);
1020        assert!(!options.interactive);
1021        assert!(options.auto_resolve);
1022        assert_eq!(options.max_retries, 3);
1023    }
1024
1025    #[test]
1026    fn test_rebase_result() {
1027        let result = RebaseResult {
1028            success: true,
1029            branch_mapping: std::collections::HashMap::new(),
1030            conflicts: vec!["abc123".to_string()],
1031            new_commits: vec!["def456".to_string()],
1032            error: None,
1033            summary: "Test summary".to_string(),
1034        };
1035
1036        assert!(result.success);
1037        assert!(result.has_conflicts());
1038        assert_eq!(result.success_count(), 1);
1039    }
1040
1041    #[test]
1042    fn test_import_line_detection() {
1043        let (_temp_dir, repo_path) = create_test_repo();
1044        let git_repo = crate::git::GitRepository::open(&repo_path).unwrap();
1045        let stack_manager = crate::stack::StackManager::new(&repo_path).unwrap();
1046        let options = RebaseOptions::default();
1047        let rebase_manager = RebaseManager::new(stack_manager, git_repo, options);
1048
1049        // Test Swift import detection
1050        assert!(rebase_manager.is_import_line("import Foundation", "test.swift"));
1051        assert!(rebase_manager.is_import_line("@testable import MyModule", "test.swift"));
1052        assert!(!rebase_manager.is_import_line("class MyClass {", "test.swift"));
1053
1054        // Test Kotlin import detection
1055        assert!(rebase_manager.is_import_line("import kotlin.collections.*", "test.kt"));
1056        assert!(rebase_manager.is_import_line("@file:JvmName(\"Utils\")", "test.kt"));
1057        assert!(!rebase_manager.is_import_line("fun myFunction() {", "test.kt"));
1058
1059        // Test C# import detection
1060        assert!(rebase_manager.is_import_line("using System;", "test.cs"));
1061        assert!(rebase_manager.is_import_line("using System.Collections.Generic;", "test.cs"));
1062        assert!(rebase_manager.is_import_line("extern alias GridV1;", "test.cs"));
1063        assert!(!rebase_manager.is_import_line("namespace MyNamespace {", "test.cs"));
1064
1065        // Test empty lines are allowed in import sections
1066        assert!(rebase_manager.is_import_line("", "test.swift"));
1067        assert!(rebase_manager.is_import_line("   ", "test.kt"));
1068        assert!(rebase_manager.is_import_line("", "test.cs"));
1069    }
1070}