use crate::error::{RailResult, ResultExt};
use clap::ValueEnum;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum ConflictStrategy {
Ours,
Theirs,
#[default]
Manual,
Union,
}
#[derive(Debug, Clone)]
pub struct ConflictInfo {
pub file_path: PathBuf,
}
#[derive(Debug)]
pub enum MergeResult {
Success,
Conflicts(Vec<PathBuf>),
Failed(String),
}
pub struct ConflictResolver {
strategy: ConflictStrategy,
work_dir: PathBuf,
}
impl ConflictResolver {
pub fn new(strategy: ConflictStrategy, work_dir: PathBuf) -> Self {
Self { strategy, work_dir }
}
pub fn strategy(&self) -> ConflictStrategy {
self.strategy
}
pub fn resolve_file(
&self,
current_path: &Path,
base_content: &[u8],
incoming_content: &[u8],
) -> RailResult<MergeResult> {
let temp_base = self.work_dir.join("merge-base");
let temp_current = self.work_dir.join("merge-current");
let temp_incoming = self.work_dir.join("merge-incoming");
std::fs::write(&temp_base, base_content).context("Failed to write base file for merge")?;
std::fs::write(&temp_current, std::fs::read(current_path)?).context("Failed to write current file for merge")?;
std::fs::write(&temp_incoming, incoming_content).context("Failed to write incoming file for merge")?;
let mut cmd = Command::new("git");
cmd.arg("merge-file");
match self.strategy {
ConflictStrategy::Ours => {
cmd.arg("--ours");
}
ConflictStrategy::Theirs => {
cmd.arg("--theirs");
}
ConflictStrategy::Manual => {
}
ConflictStrategy::Union => {
cmd.arg("--union");
}
}
cmd.arg(&temp_current);
cmd.arg(&temp_base);
cmd.arg(&temp_incoming);
let output = cmd.output().context("Failed to run git merge-file")?;
match output.status.code() {
Some(0) => {
let merged_content = std::fs::read(&temp_current)?;
std::fs::write(current_path, merged_content)?;
let _ = std::fs::remove_file(&temp_base);
let _ = std::fs::remove_file(&temp_current);
let _ = std::fs::remove_file(&temp_incoming);
Ok(MergeResult::Success)
}
Some(1) => {
let merged_content = std::fs::read(&temp_current)?;
std::fs::write(current_path, merged_content)?;
let _ = std::fs::remove_file(&temp_base);
let _ = std::fs::remove_file(&temp_current);
let _ = std::fs::remove_file(&temp_incoming);
Ok(MergeResult::Conflicts(vec![current_path.to_path_buf()]))
}
Some(code) => {
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(MergeResult::Failed(format!(
"git merge-file failed with code {}: {}",
code, stderr
)))
}
None => Ok(MergeResult::Failed(
"git merge-file was terminated by signal".to_string(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_strategy_value_enum() {
use clap::ValueEnum;
assert_eq!(
ConflictStrategy::from_str("ours", false).unwrap(),
ConflictStrategy::Ours
);
assert_eq!(
ConflictStrategy::from_str("theirs", false).unwrap(),
ConflictStrategy::Theirs
);
assert_eq!(
ConflictStrategy::from_str("manual", false).unwrap(),
ConflictStrategy::Manual
);
assert_eq!(
ConflictStrategy::from_str("union", false).unwrap(),
ConflictStrategy::Union
);
assert!(ConflictStrategy::from_str("invalid", false).is_err());
}
#[test]
fn test_clean_merge() {
let temp = TempDir::new().unwrap();
let resolver = ConflictResolver::new(ConflictStrategy::Manual, temp.path().to_path_buf());
let current_file = temp.path().join("test.txt");
std::fs::write(¤t_file, "line 1\nline 2\nline 3\n").unwrap();
let base = b"line 1\nline 2\nline 3\n";
let incoming = b"line 1\nline 2 modified\nline 3\n";
let result = resolver.resolve_file(¤t_file, base, incoming).unwrap();
match result {
MergeResult::Success => {
let content = std::fs::read_to_string(¤t_file).unwrap();
assert!(content.contains("line 2 modified"));
}
_ => panic!("Expected clean merge"),
}
}
#[test]
fn test_conflict_detection() {
let temp = TempDir::new().unwrap();
let resolver = ConflictResolver::new(ConflictStrategy::Manual, temp.path().to_path_buf());
let current_file = temp.path().join("test.txt");
std::fs::write(¤t_file, "line 1\nline 2 current\nline 3\n").unwrap();
let base = b"line 1\nline 2\nline 3\n";
let incoming = b"line 1\nline 2 incoming\nline 3\n";
let result = resolver.resolve_file(¤t_file, base, incoming).unwrap();
match result {
MergeResult::Conflicts(paths) => {
assert_eq!(paths.len(), 1);
let content = std::fs::read_to_string(¤t_file).unwrap();
assert!(content.contains("<<<<<<<"));
}
_ => panic!("Expected conflicts"),
}
}
#[test]
fn test_ours_strategy() {
let temp = TempDir::new().unwrap();
let resolver = ConflictResolver::new(ConflictStrategy::Ours, temp.path().to_path_buf());
let current_file = temp.path().join("test.txt");
std::fs::write(¤t_file, "line 1\nline 2 current\nline 3\n").unwrap();
let base = b"line 1\nline 2\nline 3\n";
let incoming = b"line 1\nline 2 incoming\nline 3\n";
let result = resolver.resolve_file(¤t_file, base, incoming).unwrap();
match result {
MergeResult::Success => {
let content = std::fs::read_to_string(¤t_file).unwrap();
assert!(content.contains("line 2 current"));
assert!(!content.contains("line 2 incoming"));
}
_ => panic!("Expected clean merge with --ours"),
}
}
#[test]
fn test_theirs_strategy() {
let temp = TempDir::new().unwrap();
let resolver = ConflictResolver::new(ConflictStrategy::Theirs, temp.path().to_path_buf());
let current_file = temp.path().join("test.txt");
std::fs::write(¤t_file, "line 1\nline 2 current\nline 3\n").unwrap();
let base = b"line 1\nline 2\nline 3\n";
let incoming = b"line 1\nline 2 incoming\nline 3\n";
let result = resolver.resolve_file(¤t_file, base, incoming).unwrap();
match result {
MergeResult::Success => {
let content = std::fs::read_to_string(¤t_file).unwrap();
assert!(!content.contains("line 2 current"));
assert!(content.contains("line 2 incoming"));
}
_ => panic!("Expected clean merge with --theirs"),
}
}
#[test]
fn test_union_strategy() {
let temp = TempDir::new().unwrap();
let resolver = ConflictResolver::new(ConflictStrategy::Union, temp.path().to_path_buf());
let current_file = temp.path().join("test.txt");
std::fs::write(¤t_file, "line 1\nline 2 current\nline 3\n").unwrap();
let base = b"line 1\nline 2\nline 3\n";
let incoming = b"line 1\nline 2 incoming\nline 3\n";
let result = resolver.resolve_file(¤t_file, base, incoming).unwrap();
match result {
MergeResult::Success => {
let content = std::fs::read_to_string(¤t_file).unwrap();
assert!(content.contains("line 2 current"));
assert!(content.contains("line 2 incoming"));
}
_ => panic!("Expected clean merge with --union"),
}
}
}