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.clone());
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, &new_commit_hash)?;
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 match self.git_repo.cherry_pick(commit_hash) {
408 Ok(new_commit_hash) => {
409 info!("✅ Cherry-picked {} -> {}", commit_hash, new_commit_hash);
410
411 match self.git_repo.get_staged_files() {
413 Ok(staged_files) if !staged_files.is_empty() => {
414 warn!(
415 "Found {} staged files after successful cherry-pick. Committing them.",
416 staged_files.len()
417 );
418
419 let cleanup_message =
421 format!("Cleanup after cherry-pick {}", &commit_hash[..8]);
422 match self.git_repo.commit_staged_changes(&cleanup_message) {
423 Ok(Some(cleanup_commit)) => {
424 info!(
425 "✅ Committed {} leftover staged files as {}",
426 staged_files.len(),
427 &cleanup_commit[..8]
428 );
429 Ok(new_commit_hash) }
431 Ok(None) => {
432 Ok(new_commit_hash)
434 }
435 Err(commit_err) => {
436 warn!("Failed to commit leftover staged changes: {}", commit_err);
437 Ok(new_commit_hash)
439 }
440 }
441 }
442 _ => {
443 Ok(new_commit_hash)
445 }
446 }
447 }
448 Err(e) => {
449 match self.git_repo.get_staged_files() {
451 Ok(staged_files) if !staged_files.is_empty() => {
452 warn!(
453 "Cherry-pick failed but found {} staged files. Attempting to commit them.",
454 staged_files.len()
455 );
456
457 let commit_message =
459 format!("Cherry-pick (partial): {}", &commit_hash[..8]);
460 match self.git_repo.commit_staged_changes(&commit_message) {
461 Ok(Some(commit_hash)) => {
462 info!("✅ Committed staged changes as {}", &commit_hash[..8]);
463 Ok(commit_hash)
464 }
465 Ok(None) => {
466 Err(e)
468 }
469 Err(commit_err) => {
470 warn!("Failed to commit staged changes: {}", commit_err);
471 Err(e)
472 }
473 }
474 }
475 _ => {
476 Err(e)
478 }
479 }
480 }
481 }
482 }
483
484 fn auto_resolve_conflicts(&self, commit_hash: &str) -> Result<bool> {
486 debug!("Attempting to auto-resolve conflicts for {}", commit_hash);
487
488 if !self.git_repo.has_conflicts()? {
490 return Ok(true);
491 }
492
493 let conflicted_files = self.git_repo.get_conflicted_files()?;
494
495 if conflicted_files.is_empty() {
496 return Ok(true);
497 }
498
499 info!(
500 "Found conflicts in {} files: {:?}",
501 conflicted_files.len(),
502 conflicted_files
503 );
504
505 let mut resolved_count = 0;
506 let mut failed_files = Vec::new();
507
508 for file_path in &conflicted_files {
509 match self.resolve_file_conflicts(file_path) {
510 Ok(ConflictResolution::Resolved) => {
511 resolved_count += 1;
512 info!("✅ Auto-resolved conflicts in {}", file_path);
513 }
514 Ok(ConflictResolution::TooComplex) => {
515 debug!(
516 "⚠️ Conflicts in {} are too complex for auto-resolution",
517 file_path
518 );
519 failed_files.push(file_path.clone());
520 }
521 Err(e) => {
522 warn!("❌ Failed to analyze conflicts in {}: {}", file_path, e);
523 failed_files.push(file_path.clone());
524 }
525 }
526 }
527
528 if resolved_count > 0 {
529 info!(
530 "🎉 Auto-resolved conflicts in {}/{} files",
531 resolved_count,
532 conflicted_files.len()
533 );
534
535 self.git_repo.stage_conflict_resolved_files()?;
537 }
538
539 let all_resolved = failed_files.is_empty();
541
542 if !all_resolved {
543 info!(
544 "⚠️ {} files still need manual resolution: {:?}",
545 failed_files.len(),
546 failed_files
547 );
548 }
549
550 Ok(all_resolved)
551 }
552
553 fn resolve_file_conflicts(&self, file_path: &str) -> Result<ConflictResolution> {
555 let repo_path = self.git_repo.path();
556 let full_path = repo_path.join(file_path);
557
558 let content = std::fs::read_to_string(&full_path)
560 .map_err(|e| CascadeError::config(format!("Failed to read file {file_path}: {e}")))?;
561
562 let conflicts = self.parse_conflict_markers(&content)?;
564
565 if conflicts.is_empty() {
566 return Ok(ConflictResolution::Resolved);
568 }
569
570 info!(
571 "Found {} conflict regions in {}",
572 conflicts.len(),
573 file_path
574 );
575
576 let mut resolved_content = content;
578 let mut any_resolved = false;
579
580 for conflict in conflicts.iter().rev() {
582 match self.resolve_single_conflict(conflict, file_path) {
583 Ok(Some(resolution)) => {
584 let before = &resolved_content[..conflict.start];
586 let after = &resolved_content[conflict.end..];
587 resolved_content = format!("{before}{resolution}{after}");
588 any_resolved = true;
589 debug!(
590 "✅ Resolved conflict at lines {}-{} in {}",
591 conflict.start_line, conflict.end_line, file_path
592 );
593 }
594 Ok(None) => {
595 debug!(
596 "⚠️ Conflict at lines {}-{} in {} too complex for auto-resolution",
597 conflict.start_line, conflict.end_line, file_path
598 );
599 }
600 Err(e) => {
601 debug!("❌ Failed to resolve conflict in {}: {}", file_path, e);
602 }
603 }
604 }
605
606 if any_resolved {
607 let remaining_conflicts = self.parse_conflict_markers(&resolved_content)?;
609
610 if remaining_conflicts.is_empty() {
611 std::fs::write(&full_path, resolved_content).map_err(|e| {
613 CascadeError::config(format!("Failed to write resolved file {file_path}: {e}"))
614 })?;
615
616 return Ok(ConflictResolution::Resolved);
617 } else {
618 info!(
619 "⚠️ Partially resolved conflicts in {} ({} remaining)",
620 file_path,
621 remaining_conflicts.len()
622 );
623 }
624 }
625
626 Ok(ConflictResolution::TooComplex)
627 }
628
629 fn parse_conflict_markers(&self, content: &str) -> Result<Vec<ConflictRegion>> {
631 let lines: Vec<&str> = content.lines().collect();
632 let mut conflicts = Vec::new();
633 let mut i = 0;
634
635 while i < lines.len() {
636 if lines[i].starts_with("<<<<<<<") {
637 let start_line = i + 1;
639 let mut separator_line = None;
640 let mut end_line = None;
641
642 for (j, line) in lines.iter().enumerate().skip(i + 1) {
644 if line.starts_with("=======") {
645 separator_line = Some(j + 1);
646 } else if line.starts_with(">>>>>>>") {
647 end_line = Some(j + 1);
648 break;
649 }
650 }
651
652 if let (Some(sep), Some(end)) = (separator_line, end_line) {
653 let start_pos = lines[..i].iter().map(|l| l.len() + 1).sum::<usize>();
655 let end_pos = lines[..end].iter().map(|l| l.len() + 1).sum::<usize>();
656
657 let our_content = lines[(i + 1)..(sep - 1)].join("\n");
658 let their_content = lines[sep..(end - 1)].join("\n");
659
660 conflicts.push(ConflictRegion {
661 start: start_pos,
662 end: end_pos,
663 start_line,
664 end_line: end,
665 our_content,
666 their_content,
667 });
668
669 i = end;
670 } else {
671 i += 1;
672 }
673 } else {
674 i += 1;
675 }
676 }
677
678 Ok(conflicts)
679 }
680
681 fn resolve_single_conflict(
683 &self,
684 conflict: &ConflictRegion,
685 file_path: &str,
686 ) -> Result<Option<String>> {
687 debug!(
688 "Analyzing conflict in {} (lines {}-{})",
689 file_path, conflict.start_line, conflict.end_line
690 );
691
692 if let Some(resolved) = self.resolve_whitespace_conflict(conflict)? {
694 debug!("Resolved as whitespace-only conflict");
695 return Ok(Some(resolved));
696 }
697
698 if let Some(resolved) = self.resolve_line_ending_conflict(conflict)? {
700 debug!("Resolved as line ending conflict");
701 return Ok(Some(resolved));
702 }
703
704 if let Some(resolved) = self.resolve_addition_conflict(conflict)? {
706 debug!("Resolved as pure addition conflict");
707 return Ok(Some(resolved));
708 }
709
710 if let Some(resolved) = self.resolve_import_conflict(conflict, file_path)? {
712 debug!("Resolved as import reordering conflict");
713 return Ok(Some(resolved));
714 }
715
716 Ok(None)
718 }
719
720 fn resolve_whitespace_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
722 let our_normalized = self.normalize_whitespace(&conflict.our_content);
723 let their_normalized = self.normalize_whitespace(&conflict.their_content);
724
725 if our_normalized == their_normalized {
726 let resolved =
728 if conflict.our_content.trim().len() >= conflict.their_content.trim().len() {
729 conflict.our_content.clone()
730 } else {
731 conflict.their_content.clone()
732 };
733
734 return Ok(Some(resolved));
735 }
736
737 Ok(None)
738 }
739
740 fn resolve_line_ending_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
742 let our_normalized = conflict
743 .our_content
744 .replace("\r\n", "\n")
745 .replace('\r', "\n");
746 let their_normalized = conflict
747 .their_content
748 .replace("\r\n", "\n")
749 .replace('\r', "\n");
750
751 if our_normalized == their_normalized {
752 return Ok(Some(our_normalized));
754 }
755
756 Ok(None)
757 }
758
759 fn resolve_addition_conflict(&self, conflict: &ConflictRegion) -> Result<Option<String>> {
761 let our_lines: Vec<&str> = conflict.our_content.lines().collect();
762 let their_lines: Vec<&str> = conflict.their_content.lines().collect();
763
764 if our_lines.is_empty() {
766 return Ok(Some(conflict.their_content.clone()));
767 }
768 if their_lines.is_empty() {
769 return Ok(Some(conflict.our_content.clone()));
770 }
771
772 let mut merged_lines = Vec::new();
774 let mut our_idx = 0;
775 let mut their_idx = 0;
776
777 while our_idx < our_lines.len() || their_idx < their_lines.len() {
778 if our_idx >= our_lines.len() {
779 merged_lines.extend_from_slice(&their_lines[their_idx..]);
781 break;
782 } else if their_idx >= their_lines.len() {
783 merged_lines.extend_from_slice(&our_lines[our_idx..]);
785 break;
786 } else if our_lines[our_idx] == their_lines[their_idx] {
787 merged_lines.push(our_lines[our_idx]);
789 our_idx += 1;
790 their_idx += 1;
791 } else {
792 return Ok(None);
794 }
795 }
796
797 Ok(Some(merged_lines.join("\n")))
798 }
799
800 fn resolve_import_conflict(
802 &self,
803 conflict: &ConflictRegion,
804 file_path: &str,
805 ) -> Result<Option<String>> {
806 let is_import_file = file_path.ends_with(".rs")
808 || file_path.ends_with(".py")
809 || file_path.ends_with(".js")
810 || file_path.ends_with(".ts")
811 || file_path.ends_with(".go")
812 || file_path.ends_with(".java")
813 || file_path.ends_with(".swift")
814 || file_path.ends_with(".kt")
815 || file_path.ends_with(".cs");
816
817 if !is_import_file {
818 return Ok(None);
819 }
820
821 let our_lines: Vec<&str> = conflict.our_content.lines().collect();
822 let their_lines: Vec<&str> = conflict.their_content.lines().collect();
823
824 let our_imports = our_lines
826 .iter()
827 .all(|line| self.is_import_line(line, file_path));
828 let their_imports = their_lines
829 .iter()
830 .all(|line| self.is_import_line(line, file_path));
831
832 if our_imports && their_imports {
833 let mut all_imports: Vec<&str> = our_lines.into_iter().chain(their_lines).collect();
835 all_imports.sort();
836 all_imports.dedup();
837
838 return Ok(Some(all_imports.join("\n")));
839 }
840
841 Ok(None)
842 }
843
844 fn is_import_line(&self, line: &str, file_path: &str) -> bool {
846 let trimmed = line.trim();
847
848 if trimmed.is_empty() {
849 return true; }
851
852 if file_path.ends_with(".rs") {
853 return trimmed.starts_with("use ") || trimmed.starts_with("extern crate");
854 } else if file_path.ends_with(".py") {
855 return trimmed.starts_with("import ") || trimmed.starts_with("from ");
856 } else if file_path.ends_with(".js") || file_path.ends_with(".ts") {
857 return trimmed.starts_with("import ")
858 || trimmed.starts_with("const ")
859 || trimmed.starts_with("require(");
860 } else if file_path.ends_with(".go") {
861 return trimmed.starts_with("import ") || trimmed == "import (" || trimmed == ")";
862 } else if file_path.ends_with(".java") {
863 return trimmed.starts_with("import ");
864 } else if file_path.ends_with(".swift") {
865 return trimmed.starts_with("import ") || trimmed.starts_with("@testable import ");
866 } else if file_path.ends_with(".kt") {
867 return trimmed.starts_with("import ") || trimmed.starts_with("@file:");
868 } else if file_path.ends_with(".cs") {
869 return trimmed.starts_with("using ") || trimmed.starts_with("extern alias ");
870 }
871
872 false
873 }
874
875 fn normalize_whitespace(&self, content: &str) -> String {
877 content
878 .lines()
879 .map(|line| line.trim())
880 .filter(|line| !line.is_empty())
881 .collect::<Vec<_>>()
882 .join("\n")
883 }
884
885 fn update_stack_entry(
888 &mut self,
889 stack_id: Uuid,
890 entry_id: &Uuid,
891 _new_branch: &str,
892 new_commit_hash: &str,
893 ) -> Result<()> {
894 debug!(
895 "Updating entry {} in stack {} with new commit {}",
896 entry_id, stack_id, new_commit_hash
897 );
898
899 let stack = self
901 .stack_manager
902 .get_stack_mut(&stack_id)
903 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
904
905 if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == *entry_id) {
907 debug!(
908 "Found entry {} - updating commit from '{}' to '{}' (keeping original branch '{}')",
909 entry_id, entry.commit_hash, new_commit_hash, entry.branch
910 );
911
912 entry.commit_hash = new_commit_hash.to_string();
915
916 debug!(
919 "Successfully updated entry {} in stack {}",
920 entry_id, stack_id
921 );
922 Ok(())
923 } else {
924 Err(CascadeError::config(format!(
925 "Entry {entry_id} not found in stack {stack_id}"
926 )))
927 }
928 }
929
930 fn pull_latest_changes(&self, branch: &str) -> Result<()> {
932 info!("Pulling latest changes for branch {}", branch);
933
934 match self.git_repo.fetch() {
936 Ok(_) => {
937 debug!("Fetch successful");
938 match self.git_repo.pull(branch) {
940 Ok(_) => {
941 info!("Pull completed successfully for {}", branch);
942 Ok(())
943 }
944 Err(e) => {
945 warn!("Pull failed for {}: {}", branch, e);
946 Ok(())
948 }
949 }
950 }
951 Err(e) => {
952 warn!("Fetch failed: {}", e);
953 Ok(())
955 }
956 }
957 }
958
959 pub fn is_rebase_in_progress(&self) -> bool {
961 let git_dir = self.git_repo.path().join(".git");
963 git_dir.join("REBASE_HEAD").exists()
964 || git_dir.join("rebase-merge").exists()
965 || git_dir.join("rebase-apply").exists()
966 }
967
968 pub fn abort_rebase(&self) -> Result<()> {
970 info!("Aborting rebase operation");
971
972 let git_dir = self.git_repo.path().join(".git");
973
974 if git_dir.join("REBASE_HEAD").exists() {
976 std::fs::remove_file(git_dir.join("REBASE_HEAD")).map_err(|e| {
977 CascadeError::Git(git2::Error::from_str(&format!(
978 "Failed to clean rebase state: {e}"
979 )))
980 })?;
981 }
982
983 if git_dir.join("rebase-merge").exists() {
984 std::fs::remove_dir_all(git_dir.join("rebase-merge")).map_err(|e| {
985 CascadeError::Git(git2::Error::from_str(&format!(
986 "Failed to clean rebase-merge: {e}"
987 )))
988 })?;
989 }
990
991 if git_dir.join("rebase-apply").exists() {
992 std::fs::remove_dir_all(git_dir.join("rebase-apply")).map_err(|e| {
993 CascadeError::Git(git2::Error::from_str(&format!(
994 "Failed to clean rebase-apply: {e}"
995 )))
996 })?;
997 }
998
999 info!("Rebase aborted successfully");
1000 Ok(())
1001 }
1002
1003 pub fn continue_rebase(&self) -> Result<()> {
1005 info!("Continuing rebase operation");
1006
1007 if self.git_repo.has_conflicts()? {
1009 return Err(CascadeError::branch(
1010 "Cannot continue rebase: there are unresolved conflicts. Resolve conflicts and stage files first.".to_string()
1011 ));
1012 }
1013
1014 self.git_repo.stage_conflict_resolved_files()?;
1016
1017 info!("Rebase continued successfully");
1018 Ok(())
1019 }
1020}
1021
1022impl RebaseResult {
1023 pub fn get_summary(&self) -> String {
1025 if self.success {
1026 format!("✅ {}", self.summary)
1027 } else {
1028 format!(
1029 "❌ Rebase failed: {}",
1030 self.error.as_deref().unwrap_or("Unknown error")
1031 )
1032 }
1033 }
1034
1035 pub fn has_conflicts(&self) -> bool {
1037 !self.conflicts.is_empty()
1038 }
1039
1040 pub fn success_count(&self) -> usize {
1042 self.new_commits.len()
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049 use std::path::PathBuf;
1050 use std::process::Command;
1051 use tempfile::TempDir;
1052
1053 #[allow(dead_code)]
1054 fn create_test_repo() -> (TempDir, PathBuf) {
1055 let temp_dir = TempDir::new().unwrap();
1056 let repo_path = temp_dir.path().to_path_buf();
1057
1058 Command::new("git")
1060 .args(["init"])
1061 .current_dir(&repo_path)
1062 .output()
1063 .unwrap();
1064 Command::new("git")
1065 .args(["config", "user.name", "Test"])
1066 .current_dir(&repo_path)
1067 .output()
1068 .unwrap();
1069 Command::new("git")
1070 .args(["config", "user.email", "test@test.com"])
1071 .current_dir(&repo_path)
1072 .output()
1073 .unwrap();
1074
1075 std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
1077 Command::new("git")
1078 .args(["add", "."])
1079 .current_dir(&repo_path)
1080 .output()
1081 .unwrap();
1082 Command::new("git")
1083 .args(["commit", "-m", "Initial"])
1084 .current_dir(&repo_path)
1085 .output()
1086 .unwrap();
1087
1088 (temp_dir, repo_path)
1089 }
1090
1091 #[test]
1092 fn test_conflict_region_creation() {
1093 let region = ConflictRegion {
1094 start: 0,
1095 end: 50,
1096 start_line: 1,
1097 end_line: 3,
1098 our_content: "function test() {\n return true;\n}".to_string(),
1099 their_content: "function test() {\n return true;\n}".to_string(),
1100 };
1101
1102 assert_eq!(region.start_line, 1);
1103 assert_eq!(region.end_line, 3);
1104 assert!(region.our_content.contains("return true"));
1105 assert!(region.their_content.contains("return true"));
1106 }
1107
1108 #[test]
1109 fn test_rebase_strategies() {
1110 assert_eq!(
1111 RebaseStrategy::BranchVersioning,
1112 RebaseStrategy::BranchVersioning
1113 );
1114 assert_eq!(RebaseStrategy::CherryPick, RebaseStrategy::CherryPick);
1115 assert_eq!(RebaseStrategy::ThreeWayMerge, RebaseStrategy::ThreeWayMerge);
1116 assert_eq!(RebaseStrategy::Interactive, RebaseStrategy::Interactive);
1117 }
1118
1119 #[test]
1120 fn test_rebase_options() {
1121 let options = RebaseOptions::default();
1122 assert_eq!(options.strategy, RebaseStrategy::BranchVersioning);
1123 assert!(!options.interactive);
1124 assert!(options.auto_resolve);
1125 assert_eq!(options.max_retries, 3);
1126 }
1127
1128 #[test]
1129 fn test_rebase_result() {
1130 let result = RebaseResult {
1131 success: true,
1132 branch_mapping: std::collections::HashMap::new(),
1133 conflicts: vec!["abc123".to_string()],
1134 new_commits: vec!["def456".to_string()],
1135 error: None,
1136 summary: "Test summary".to_string(),
1137 };
1138
1139 assert!(result.success);
1140 assert!(result.has_conflicts());
1141 assert_eq!(result.success_count(), 1);
1142 }
1143
1144 #[test]
1145 fn test_import_line_detection() {
1146 let (_temp_dir, repo_path) = create_test_repo();
1147 let git_repo = crate::git::GitRepository::open(&repo_path).unwrap();
1148 let stack_manager = crate::stack::StackManager::new(&repo_path).unwrap();
1149 let options = RebaseOptions::default();
1150 let rebase_manager = RebaseManager::new(stack_manager, git_repo, options);
1151
1152 assert!(rebase_manager.is_import_line("import Foundation", "test.swift"));
1154 assert!(rebase_manager.is_import_line("@testable import MyModule", "test.swift"));
1155 assert!(!rebase_manager.is_import_line("class MyClass {", "test.swift"));
1156
1157 assert!(rebase_manager.is_import_line("import kotlin.collections.*", "test.kt"));
1159 assert!(rebase_manager.is_import_line("@file:JvmName(\"Utils\")", "test.kt"));
1160 assert!(!rebase_manager.is_import_line("fun myFunction() {", "test.kt"));
1161
1162 assert!(rebase_manager.is_import_line("using System;", "test.cs"));
1164 assert!(rebase_manager.is_import_line("using System.Collections.Generic;", "test.cs"));
1165 assert!(rebase_manager.is_import_line("extern alias GridV1;", "test.cs"));
1166 assert!(!rebase_manager.is_import_line("namespace MyNamespace {", "test.cs"));
1167
1168 assert!(rebase_manager.is_import_line("", "test.swift"));
1170 assert!(rebase_manager.is_import_line(" ", "test.kt"));
1171 assert!(rebase_manager.is_import_line("", "test.cs"));
1172 }
1173}