//! Merge conflict resolution helpers. use std::path::Path; use std::process::Command; pub(super) fn try_resolve_conflicts(worktree: &Path) -> Result<(bool, String), String> { let mut log = String::new(); // List conflicted files. let ls = Command::new("git") .args(["diff", "--name-only", "--diff-filter=U"]) .current_dir(worktree) .output() .map_err(|e| format!("Failed to list conflicted files: {e}"))?; let file_list = String::from_utf8_lossy(&ls.stdout); let conflicted_files: Vec<&str> = file_list.lines().filter(|l| !l.is_empty()).collect(); if conflicted_files.is_empty() { log.push_str("No conflicted files found (conflict may be index-only).\n"); return Ok((false, log)); } log.push_str(&format!("Conflicted files ({}):\n", conflicted_files.len())); for f in &conflicted_files { log.push_str(&format!(" - {f}\n")); } // First pass: check that all files can be resolved before touching any. let mut resolutions: Vec<(&str, String)> = Vec::new(); for file in &conflicted_files { let file_path = worktree.join(file); let content = std::fs::read_to_string(&file_path) .map_err(|e| format!("Failed to read conflicted file '{file}': {e}"))?; match resolve_simple_conflicts(&content) { Some(resolved) => { log.push_str(&format!(" [auto-resolve] {file}\n")); resolutions.push((file, resolved)); } None => { log.push_str(&format!(" [COMPLEX — cannot auto-resolve] {file}\n")); return Ok((false, log)); } } } // Second pass: write resolved content and stage. for (file, resolved) in &resolutions { let file_path = worktree.join(file); std::fs::write(&file_path, resolved) .map_err(|e| format!("Failed to write resolved file '{file}': {e}"))?; let add = Command::new("git") .args(["add", file]) .current_dir(worktree) .output() .map_err(|e| format!("Failed to stage resolved file '{file}': {e}"))?; if !add.status.success() { return Err(format!( "git add failed for '{file}': {}", String::from_utf8_lossy(&add.stderr) )); } } Ok((true, log)) } fn resolve_simple_conflicts(content: &str) -> Option { // Quick check: if there are no conflict markers at all, nothing to do. if !content.contains("<<<<<<<") { return Some(content.to_string()); } let mut result = String::new(); let mut lines = content.lines().peekable(); while let Some(line) = lines.next() { if line.starts_with("<<<<<<<") { // Collect the "ours" side (between <<<<<<< and =======). let mut ours = Vec::new(); let mut found_separator = false; for next_line in lines.by_ref() { if next_line.starts_with("=======") { found_separator = true; break; } ours.push(next_line); } if !found_separator { return None; // Malformed conflict block. } // Collect the "theirs" side (between ======= and >>>>>>>). let mut theirs = Vec::new(); let mut found_end = false; for next_line in lines.by_ref() { if next_line.starts_with(">>>>>>>") { found_end = true; break; } theirs.push(next_line); } if !found_end { return None; // Malformed conflict block. } // Both sides must be non-empty additions to be considered simple. // If either side is empty, it means one side deleted something — complex. if ours.is_empty() && theirs.is_empty() { // Both empty — nothing to add, skip. continue; } // Accept both: ours first, then theirs. for l in &ours { result.push_str(l); result.push('\n'); } for l in &theirs { result.push_str(l); result.push('\n'); } } else { result.push_str(line); result.push('\n'); } } // Preserve trailing newline consistency: if original ended without // newline, strip the trailing one we added. if !content.ends_with('\n') && result.ends_with('\n') { result.pop(); } Some(result) } #[cfg(test)] mod tests { use super::*; #[test] fn resolve_simple_conflicts_no_markers() { let input = "line 1\nline 2\nline 3\n"; let result = resolve_simple_conflicts(input); assert_eq!(result, Some(input.to_string())); } #[test] fn resolve_simple_conflicts_additive() { let input = "\ before <<<<<<< HEAD ours line 1 ours line 2 ======= theirs line 1 theirs line 2 >>>>>>> feature after "; let result = resolve_simple_conflicts(input).unwrap(); assert!( !result.contains("<<<<<<<"), "should not contain conflict markers" ); assert!( !result.contains(">>>>>>>"), "should not contain conflict markers" ); assert!(result.contains("ours line 1")); assert!(result.contains("ours line 2")); assert!(result.contains("theirs line 1")); assert!(result.contains("theirs line 2")); assert!(result.contains("before")); assert!(result.contains("after")); // Ours comes before theirs let ours_pos = result.find("ours line 1").unwrap(); let theirs_pos = result.find("theirs line 1").unwrap(); assert!(ours_pos < theirs_pos, "ours should come before theirs"); } #[test] fn resolve_simple_conflicts_multiple_blocks() { let input = "\ header <<<<<<< HEAD ours block 1 ======= theirs block 1 >>>>>>> feature middle <<<<<<< HEAD ours block 2 ======= theirs block 2 >>>>>>> feature footer "; let result = resolve_simple_conflicts(input).unwrap(); assert!(!result.contains("<<<<<<<")); assert!(result.contains("ours block 1")); assert!(result.contains("theirs block 1")); assert!(result.contains("ours block 2")); assert!(result.contains("theirs block 2")); assert!(result.contains("header")); assert!(result.contains("middle")); assert!(result.contains("footer")); } #[test] fn resolve_simple_conflicts_malformed_no_separator() { let input = "\ <<<<<<< HEAD ours >>>>>>> feature "; let result = resolve_simple_conflicts(input); assert!( result.is_none(), "malformed conflict (no separator) should return None" ); } #[test] fn resolve_simple_conflicts_malformed_no_end() { let input = "\ <<<<<<< HEAD ours ======= theirs "; let result = resolve_simple_conflicts(input); assert!( result.is_none(), "malformed conflict (no end marker) should return None" ); } #[test] fn resolve_simple_conflicts_preserves_no_trailing_newline() { let input = "before\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> branch\nafter"; let result = resolve_simple_conflicts(input).unwrap(); assert!( !result.ends_with('\n'), "should not add trailing newline if original lacks one" ); assert!(result.ends_with("after")); } #[test] fn resolve_simple_conflicts_real_markers_additive_both_sides() { // The most common real-world case: both branches add different content // (e.g. different functions) to the same region of a file. let input = "// shared code\n\ <<<<<<< HEAD\n\ fn master_fn() { println!(\"from master\"); }\n\ =======\n\ fn feature_fn() { println!(\"from feature\"); }\n\ >>>>>>> feature/story-42\n\ // end\n"; let result = resolve_simple_conflicts(input).unwrap(); assert!(!result.contains("<<<<<<<"), "no conflict markers in output"); assert!(!result.contains(">>>>>>>"), "no conflict markers in output"); assert!(!result.contains("======="), "no separator in output"); assert!( result.contains("fn master_fn()"), "master (ours) side must be preserved" ); assert!( result.contains("fn feature_fn()"), "feature (theirs) side must be preserved" ); assert!( result.contains("// shared code"), "context before conflict preserved" ); assert!( result.contains("// end"), "context after conflict preserved" ); // ours (master) must appear before theirs (feature) assert!( result.find("master_fn").unwrap() < result.find("feature_fn").unwrap(), "master side must appear before feature side" ); } #[test] fn resolve_simple_conflicts_real_markers_multiple_conflict_blocks() { // Two separate conflict blocks in the same file — as happens when two // feature branches both add imports AND test suites to the same file. let input = "// imports\n\ <<<<<<< HEAD\n\ import { A } from './a';\n\ =======\n\ import { B } from './b';\n\ >>>>>>> feature/story-43\n\ // implementation\n\ <<<<<<< HEAD\n\ export function masterImpl() {}\n\ =======\n\ export function featureImpl() {}\n\ >>>>>>> feature/story-43\n"; let result = resolve_simple_conflicts(input).unwrap(); assert!(!result.contains("<<<<<<<"), "no conflict markers in output"); assert!( result.contains("import { A }"), "first block ours preserved" ); assert!( result.contains("import { B }"), "first block theirs preserved" ); assert!(result.contains("masterImpl"), "second block ours preserved"); assert!( result.contains("featureImpl"), "second block theirs preserved" ); assert!( result.contains("// imports"), "surrounding context preserved" ); assert!( result.contains("// implementation"), "surrounding context preserved" ); } #[test] fn resolve_simple_conflicts_real_markers_one_side_empty() { // Ours (master) has no content in the conflicted region; theirs (feature) // adds new content. Resolution: keep theirs. let input = "before\n\ <<<<<<< HEAD\n\ =======\n\ feature_addition\n\ >>>>>>> feature/story-44\n\ after\n"; let result = resolve_simple_conflicts(input).unwrap(); assert!(!result.contains("<<<<<<<"), "no conflict markers"); assert!( result.contains("feature_addition"), "non-empty side preserved" ); assert!(result.contains("before"), "context preserved"); assert!(result.contains("after"), "context preserved"); } }