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 pub skip_pull: Option<bool>,
66}
67
68#[derive(Debug)]
70pub struct RebaseResult {
71 pub success: bool,
73 pub branch_mapping: HashMap<String, String>,
75 pub conflicts: Vec<String>,
77 pub new_commits: Vec<String>,
79 pub error: Option<String>,
81 pub summary: String,
83}
84
85pub 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 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 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 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 if self.git_repo.get_current_branch()? != *target_base {
162 self.git_repo.checkout_branch(target_base)?;
163 }
164
165 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 let new_branch = self.generate_versioned_branch_name(&entry.branch)?;
182
183 self.git_repo
185 .create_branch(&new_branch, Some(¤t_base))?;
186 self.git_repo.checkout_branch(&new_branch)?;
187
188 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 self.update_stack_entry(stack.id, &entry.id, &new_branch)?;
198
199 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 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 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 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 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 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 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 result.summary = "Three-way merge strategy implemented".to_string();
327
328 Ok(result)
329 }
330
331 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 entry in &stack.entries {
359 println!(
360 " {} {} - {}",
361 entry.short_hash(),
362 entry.branch,
363 entry.short_message(50)
364 );
365
366 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 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 fn cherry_pick_commit(&self, commit_hash: &str) -> Result<String> {
404 debug!("Cherry-picking commit {}", commit_hash);
405
406 self.git_repo.cherry_pick(commit_hash)
408 }
409
410 fn auto_resolve_conflicts(&self, commit_hash: &str) -> Result<bool> {
412 debug!("Attempting to auto-resolve conflicts for {}", commit_hash);
413
414 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 self.git_repo.stage_conflict_resolved_files()?;
463 }
464
465 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 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 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 let conflicts = self.parse_conflict_markers(&content)?;
490
491 if conflicts.is_empty() {
492 return Ok(ConflictResolution::Resolved);
494 }
495
496 info!(
497 "Found {} conflict regions in {}",
498 conflicts.len(),
499 file_path
500 );
501
502 let mut resolved_content = content;
504 let mut any_resolved = false;
505
506 for conflict in conflicts.iter().rev() {
508 match self.resolve_single_conflict(conflict, file_path) {
509 Ok(Some(resolution)) => {
510 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 let remaining_conflicts = self.parse_conflict_markers(&resolved_content)?;
535
536 if remaining_conflicts.is_empty() {
537 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 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 let start_line = i + 1;
565 let mut separator_line = None;
566 let mut end_line = None;
567
568 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 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 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 if let Some(resolved) = self.resolve_whitespace_conflict(conflict)? {
620 debug!("Resolved as whitespace-only conflict");
621 return Ok(Some(resolved));
622 }
623
624 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 if let Some(resolved) = self.resolve_addition_conflict(conflict)? {
632 debug!("Resolved as pure addition conflict");
633 return Ok(Some(resolved));
634 }
635
636 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 Ok(None)
644 }
645
646 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 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 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 return Ok(Some(our_normalized));
680 }
681
682 Ok(None)
683 }
684
685 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 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 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 merged_lines.extend_from_slice(&their_lines[their_idx..]);
707 break;
708 } else if their_idx >= their_lines.len() {
709 merged_lines.extend_from_slice(&our_lines[our_idx..]);
711 break;
712 } else if our_lines[our_idx] == their_lines[their_idx] {
713 merged_lines.push(our_lines[our_idx]);
715 our_idx += 1;
716 their_idx += 1;
717 } else {
718 return Ok(None);
720 }
721 }
722
723 Ok(Some(merged_lines.join("\n")))
724 }
725
726 fn resolve_import_conflict(
728 &self,
729 conflict: &ConflictRegion,
730 file_path: &str,
731 ) -> Result<Option<String>> {
732 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 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 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 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; }
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 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 fn update_stack_entry(
813 &mut self,
814 stack_id: Uuid,
815 entry_id: &Uuid,
816 new_branch: &str,
817 ) -> Result<()> {
818 debug!(
821 "Updating entry {} in stack {} with new branch {}",
822 entry_id, stack_id, new_branch
823 );
824 Ok(())
825 }
826
827 fn pull_latest_changes(&self, branch: &str) -> Result<()> {
829 info!("Pulling latest changes for branch {}", branch);
830
831 match self.git_repo.fetch() {
833 Ok(_) => {
834 debug!("Fetch successful");
835 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 Ok(())
845 }
846 }
847 }
848 Err(e) => {
849 warn!("Fetch failed: {}", e);
850 Ok(())
852 }
853 }
854 }
855
856 pub fn is_rebase_in_progress(&self) -> bool {
858 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 pub fn abort_rebase(&self) -> Result<()> {
867 info!("Aborting rebase operation");
868
869 let git_dir = self.git_repo.path().join(".git");
870
871 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 pub fn continue_rebase(&self) -> Result<()> {
902 info!("Continuing rebase operation");
903
904 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 self.git_repo.stage_conflict_resolved_files()?;
913
914 info!("Rebase continued successfully");
915 Ok(())
916 }
917}
918
919impl RebaseResult {
920 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 pub fn has_conflicts(&self) -> bool {
934 !self.conflicts.is_empty()
935 }
936
937 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 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 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 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 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 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 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}