pub mod three_way_merge;
pub use three_way_merge::{
apply_generated, three_way_merge as sync_three_way_merge, MergeConflict as SyncMergeConflict,
MergeError, MergeResult as SyncMergeResult,
};
use crate::utils::error::Result;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::{Path, PathBuf};
use crate::snapshot::{FileSnapshot, Region, RegionType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergeConflict {
pub file_path: PathBuf,
pub conflict_type: ConflictType,
pub description: String,
pub generated: String,
pub manual: String,
pub baseline: String,
}
impl MergeConflict {
pub fn new(
file_path: PathBuf, conflict_type: ConflictType, description: String, generated: String,
manual: String, baseline: String,
) -> Self {
Self {
file_path,
conflict_type,
description,
generated,
manual,
baseline,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConflictType {
OverlappingEdit,
RegionConflict,
StructuralConflict,
}
impl fmt::Display for ConflictType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConflictType::OverlappingEdit => write!(f, "overlapping edit"),
ConflictType::RegionConflict => write!(f, "region conflict"),
ConflictType::StructuralConflict => write!(f, "structural conflict"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergeResult {
pub content: String,
pub has_conflicts: bool,
pub conflicts: Vec<MergeConflict>,
pub strategy: MergeStrategy,
}
impl MergeResult {
pub fn success(content: String, strategy: MergeStrategy) -> Self {
Self {
content,
has_conflicts: false,
conflicts: Vec::new(),
strategy,
}
}
pub fn with_conflicts(
content: String, conflicts: Vec<MergeConflict>, strategy: MergeStrategy,
) -> Self {
Self {
content,
has_conflicts: true,
conflicts,
strategy,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MergeStrategy {
GeneratedWins,
ManualWins,
Interactive,
FailOnConflict,
}
impl fmt::Display for MergeStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MergeStrategy::GeneratedWins => write!(f, "generated wins"),
MergeStrategy::ManualWins => write!(f, "manual wins"),
MergeStrategy::Interactive => write!(f, "interactive"),
MergeStrategy::FailOnConflict => write!(f, "fail on conflict"),
}
}
}
pub struct ThreeWayMerger {
strategy: MergeStrategy,
}
impl ThreeWayMerger {
pub fn new(strategy: MergeStrategy) -> Self {
Self { strategy }
}
pub fn merge(
&self, baseline: &str, generated: &str, manual: &str, file_path: &Path,
) -> Result<MergeResult> {
match self.strategy {
MergeStrategy::GeneratedWins => {
Ok(MergeResult::success(generated.to_string(), self.strategy))
}
MergeStrategy::ManualWins => {
Ok(MergeResult::success(manual.to_string(), self.strategy))
}
MergeStrategy::FailOnConflict => {
if self.has_conflicts(baseline, generated, manual)? {
let conflicts =
self.detect_conflicts(baseline, generated, manual, file_path)?;
Ok(MergeResult::with_conflicts(
String::new(),
conflicts,
self.strategy,
))
} else {
Ok(MergeResult::success(generated.to_string(), self.strategy))
}
}
MergeStrategy::Interactive => {
Ok(MergeResult::success(generated.to_string(), self.strategy))
}
}
}
fn has_conflicts(&self, _baseline: &str, _generated: &str, _manual: &str) -> Result<bool> {
Ok(false)
}
fn detect_conflicts(
&self, _baseline: &str, _generated: &str, _manual: &str, _file_path: &Path,
) -> Result<Vec<MergeConflict>> {
Ok(Vec::new())
}
}
pub struct RegionAwareMerger {
strategy: MergeStrategy,
}
impl RegionAwareMerger {
pub fn new(strategy: MergeStrategy) -> Self {
Self { strategy }
}
pub fn merge_with_regions(
&self, baseline: &str, generated: &str, manual: &str, _baseline_snapshot: &FileSnapshot,
file_path: &Path,
) -> Result<MergeResult> {
let mut result = String::new();
let mut conflicts = Vec::new();
let mut has_conflicts = false;
let baseline_lines: Vec<&str> = baseline.lines().collect();
let generated_lines: Vec<&str> = generated.lines().collect();
let manual_lines: Vec<&str> = manual.lines().collect();
let mut i = 0;
while i < baseline_lines
.len()
.max(generated_lines.len())
.max(manual_lines.len())
{
let baseline_line = baseline_lines.get(i);
let generated_line = generated_lines.get(i);
let manual_line = manual_lines.get(i);
match (baseline_line, generated_line, manual_line) {
(Some(bl), Some(gl), Some(ml)) => {
if bl == gl && gl == ml {
result.push_str(gl);
result.push('\n');
} else if bl == gl {
result.push_str(ml);
result.push('\n');
} else if bl == ml {
result.push_str(gl);
result.push('\n');
} else {
has_conflicts = true;
conflicts.push(MergeConflict::new(
file_path.to_path_buf(),
ConflictType::OverlappingEdit,
format!("Conflict at line {}", i + 1),
gl.to_string(),
ml.to_string(),
bl.to_string(),
));
match self.strategy {
MergeStrategy::GeneratedWins => {
result.push_str(gl);
result.push('\n');
}
MergeStrategy::ManualWins => {
result.push_str(ml);
result.push('\n');
}
_ => {
result.push_str(gl);
result.push('\n');
}
}
}
}
(Some(bl), Some(gl), None) => {
if bl == gl {
result.push_str(gl);
result.push('\n');
} else {
has_conflicts = true;
conflicts.push(MergeConflict::new(
file_path.to_path_buf(),
ConflictType::StructuralConflict,
"Length mismatch".to_string(),
gl.to_string(),
String::new(),
bl.to_string(),
));
}
}
(Some(bl), None, Some(ml)) => {
if bl == ml {
result.push_str(ml);
result.push('\n');
} else {
has_conflicts = true;
conflicts.push(MergeConflict::new(
file_path.to_path_buf(),
ConflictType::StructuralConflict,
"Length mismatch".to_string(),
String::new(),
ml.to_string(),
bl.to_string(),
));
}
}
(None, Some(gl), Some(ml)) => {
if gl == ml {
result.push_str(gl);
result.push('\n');
} else {
has_conflicts = true;
conflicts.push(MergeConflict::new(
file_path.to_path_buf(),
ConflictType::OverlappingEdit,
"New content conflict".to_string(),
gl.to_string(),
ml.to_string(),
String::new(),
));
}
}
_ => break, }
i += 1;
}
if has_conflicts {
Ok(MergeResult::with_conflicts(
result,
conflicts,
self.strategy,
))
} else {
Ok(MergeResult::success(result, self.strategy))
}
}
}
pub struct RegionUtils;
impl RegionUtils {
pub fn parse_regions(content: &str) -> (Vec<Region>, Vec<Region>) {
let mut generated_regions = Vec::new();
let mut manual_regions = Vec::new();
let mut in_generated = false;
let mut in_manual = false;
let mut current_start = 0;
let mut line_number = 1;
for line in content.lines() {
if line.contains("// GENERATED: DO NOT EDIT")
|| line.contains("<!-- GENERATED: DO NOT EDIT")
{
if !in_generated && !in_manual {
current_start = line_number;
in_generated = true;
}
} else if line.contains("// END GENERATED") || line.contains("<!-- END GENERATED") {
if in_generated {
generated_regions.push(Region {
start: current_start,
end: line_number,
region_type: RegionType::Generated,
});
in_generated = false;
}
} else if line.contains("// MANUAL:") || line.contains("<!-- MANUAL:") {
if !in_generated && !in_manual {
current_start = line_number;
in_manual = true;
}
} else if (line.contains("// END MANUAL") || line.contains("<!-- END MANUAL"))
&& in_manual
{
manual_regions.push(Region {
start: current_start,
end: line_number,
region_type: RegionType::Manual,
});
in_manual = false;
}
line_number += 1;
}
(generated_regions, manual_regions)
}
pub fn is_in_region(line_number: usize, regions: &[Region], region_type: &RegionType) -> bool {
regions.iter().any(|r| {
r.start <= line_number && r.end >= line_number && &r.region_type == region_type
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::FileSnapshot;
#[test]
fn test_merge_strategies() {
let merger = ThreeWayMerger::new(MergeStrategy::GeneratedWins);
let baseline = "line 1\nline 2\n";
let generated = "line 1\nline 2\nline 3\n";
let manual = "line 1\nmanual line\nline 3\n";
let result = merger
.merge(baseline, generated, manual, Path::new("test.txt"))
.unwrap();
assert!(!result.has_conflicts);
assert_eq!(result.strategy, MergeStrategy::GeneratedWins);
assert_eq!(result.content, generated);
}
#[test]
fn test_region_aware_merge() {
let merger = RegionAwareMerger::new(MergeStrategy::ManualWins);
let baseline = "line 1\n// GENERATED: DO NOT EDIT\nline 2\n// END GENERATED\nline 3\n";
let generated =
"line 1\n// GENERATED: DO NOT EDIT\nline 2 modified\n// END GENERATED\nline 3\n";
let manual = "line 1\n// GENERATED: DO NOT EDIT\nline 2\n// END GENERATED\nmanual line\n";
let snapshot = FileSnapshot {
path: PathBuf::from("test.txt"),
hash: String::new(),
size: 0,
modified_at: chrono::Utc::now(),
generated_regions: vec![Region {
start: 2,
end: 4,
region_type: RegionType::Generated,
}],
manual_regions: vec![Region {
start: 5,
end: 5,
region_type: RegionType::Manual,
}],
};
let result = merger
.merge_with_regions(
baseline,
generated,
manual,
&snapshot,
Path::new("test.txt"),
)
.unwrap();
assert!(!result.has_conflicts);
assert_eq!(result.strategy, MergeStrategy::ManualWins);
}
#[test]
fn test_region_parsing() {
let content = r#"
line 1
// GENERATED: DO NOT EDIT
generated line 1
generated line 2
// END GENERATED
// MANUAL: Safe to edit
manual line 1
manual line 2
// END MANUAL
line 4
"#;
let (generated, manual) = RegionUtils::parse_regions(content);
log::debug!("Generated regions: {:?}", generated);
log::debug!("Manual regions: {:?}", manual);
assert_eq!(generated.len(), 1);
assert_eq!(manual.len(), 1);
assert_eq!(generated[0].start, 3);
assert_eq!(generated[0].end, 6);
assert_eq!(manual[0].start, 7);
assert_eq!(manual[0].end, 10);
}
#[test]
fn test_region_check() {
let regions = vec![
Region {
start: 2,
end: 4,
region_type: RegionType::Generated,
},
Region {
start: 6,
end: 8,
region_type: RegionType::Manual,
},
];
assert!(RegionUtils::is_in_region(
3,
®ions,
&RegionType::Generated
));
assert!(!RegionUtils::is_in_region(3, ®ions, &RegionType::Manual));
assert!(RegionUtils::is_in_region(7, ®ions, &RegionType::Manual));
assert!(!RegionUtils::is_in_region(
1,
®ions,
&RegionType::Generated
));
}
}