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#[derive(Debug, Clone)]
12enum ConflictResolution {
13 Resolved,
15 TooComplex,
17}
18
19#[derive(Debug, Clone)]
21struct ConflictRegion {
22 start: usize,
24 end: usize,
26 start_line: usize,
28 end_line: usize,
30 our_content: String,
32 their_content: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub enum RebaseStrategy {
39 BranchVersioning,
41 CherryPick,
43 ThreeWayMerge,
45 Interactive,
47}
48
49#[derive(Debug, Clone)]
51pub struct RebaseOptions {
52 pub strategy: RebaseStrategy,
54 pub interactive: bool,
56 pub target_base: Option<String>,
58 pub preserve_merges: bool,
60 pub auto_resolve: bool,
62 pub max_retries: usize,
64}
65
66#[derive(Debug)]
68pub struct RebaseResult {
69 pub success: bool,
71 pub branch_mapping: HashMap<String, String>,
73 pub conflicts: Vec<String>,
75 pub new_commits: Vec<String>,
77 pub error: Option<String>,
79 pub summary: String,
81}
82
83pub 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 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 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 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 if self.git_repo.get_current_branch()? != *target_base {
159 self.git_repo.checkout_branch(target_base)?;
160 }
161
162 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 let new_branch = self.generate_versioned_branch_name(&entry.branch)?;
174
175 self.git_repo
177 .create_branch(&new_branch, Some(¤t_base))?;
178 self.git_repo.checkout_branch(&new_branch)?;
179
180 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 self.update_stack_entry(stack.id, &entry.id, &new_branch)?;
190
191 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 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 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 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 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 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 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 result.summary = "Three-way merge strategy implemented".to_string();
319
320 Ok(result)
321 }
322
323 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 entry in &stack.entries {
351 println!(
352 " {} {} - {}",
353 entry.short_hash(),
354 entry.branch,
355 entry.short_message(50)
356 );
357
358 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 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 fn cherry_pick_commit(&self, commit_hash: &str) -> Result<String> {
396 debug!("Cherry-picking commit {}", commit_hash);
397
398 self.git_repo.cherry_pick(commit_hash)
400 }
401
402 fn auto_resolve_conflicts(&self, commit_hash: &str) -> Result<bool> {
404 debug!("Attempting to auto-resolve conflicts for {}", commit_hash);
405
406 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 self.git_repo.stage_all()?;
455 }
456
457 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 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 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 let conflicts = self.parse_conflict_markers(&content)?;
482
483 if conflicts.is_empty() {
484 return Ok(ConflictResolution::Resolved);
486 }
487
488 info!(
489 "Found {} conflict regions in {}",
490 conflicts.len(),
491 file_path
492 );
493
494 let mut resolved_content = content;
496 let mut any_resolved = false;
497
498 for conflict in conflicts.iter().rev() {
500 match self.resolve_single_conflict(conflict, file_path) {
501 Ok(Some(resolution)) => {
502 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 let remaining_conflicts = self.parse_conflict_markers(&resolved_content)?;
527
528 if remaining_conflicts.is_empty() {
529 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 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 let start_line = i + 1;
557 let mut separator_line = None;
558 let mut end_line = None;
559
560 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 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 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 if let Some(resolved) = self.resolve_whitespace_conflict(conflict)? {
612 debug!("Resolved as whitespace-only conflict");
613 return Ok(Some(resolved));
614 }
615
616 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 if let Some(resolved) = self.resolve_addition_conflict(conflict)? {
624 debug!("Resolved as pure addition conflict");
625 return Ok(Some(resolved));
626 }
627
628 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 Ok(None)
636 }
637
638 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 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 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 return Ok(Some(our_normalized));
672 }
673
674 Ok(None)
675 }
676
677 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 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 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 merged_lines.extend_from_slice(&their_lines[their_idx..]);
699 break;
700 } else if their_idx >= their_lines.len() {
701 merged_lines.extend_from_slice(&our_lines[our_idx..]);
703 break;
704 } else if our_lines[our_idx] == their_lines[their_idx] {
705 merged_lines.push(our_lines[our_idx]);
707 our_idx += 1;
708 their_idx += 1;
709 } else {
710 return Ok(None);
712 }
713 }
714
715 Ok(Some(merged_lines.join("\n")))
716 }
717
718 fn resolve_import_conflict(
720 &self,
721 conflict: &ConflictRegion,
722 file_path: &str,
723 ) -> Result<Option<String>> {
724 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 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 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 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; }
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 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 fn update_stack_entry(
805 &mut self,
806 stack_id: Uuid,
807 entry_id: &Uuid,
808 new_branch: &str,
809 ) -> Result<()> {
810 debug!(
813 "Updating entry {} in stack {} with new branch {}",
814 entry_id, stack_id, new_branch
815 );
816 Ok(())
817 }
818
819 fn pull_latest_changes(&self, branch: &str) -> Result<()> {
821 info!("Pulling latest changes for branch {}", branch);
822
823 match self.git_repo.fetch() {
825 Ok(_) => {
826 debug!("Fetch successful");
827 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 Ok(())
837 }
838 }
839 }
840 Err(e) => {
841 warn!("Fetch failed: {}", e);
842 Ok(())
844 }
845 }
846 }
847
848 pub fn is_rebase_in_progress(&self) -> bool {
850 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 pub fn abort_rebase(&self) -> Result<()> {
859 info!("Aborting rebase operation");
860
861 let git_dir = self.git_repo.path().join(".git");
862
863 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 pub fn continue_rebase(&self) -> Result<()> {
894 info!("Continuing rebase operation");
895
896 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 self.git_repo.stage_all()?;
905
906 info!("Rebase continued successfully");
907 Ok(())
908 }
909}
910
911impl RebaseResult {
912 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 pub fn has_conflicts(&self) -> bool {
926 !self.conflicts.is_empty()
927 }
928
929 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 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 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 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 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 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 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}