use crate::conflict::ast_merge;
use crate::parser::ParserRegistry;
#[derive(Debug, Clone)]
pub struct SemanticConflict {
pub file_path: String,
pub symbol_name: String,
pub our_change: SymbolChangeKind,
pub their_change: SymbolChangeKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymbolChangeKind {
Added,
Modified,
Removed,
}
#[derive(Debug)]
pub enum MergeAnalysis {
AutoMerge {
merged_content: Vec<u8>,
},
Conflict {
conflicts: Vec<SemanticConflict>,
},
}
pub fn analyze_file_conflict(
file_path: &str,
base_content: &[u8],
head_content: &[u8],
overlay_content: &[u8],
parser: &ParserRegistry,
) -> MergeAnalysis {
let base_str = std::str::from_utf8(base_content).ok();
let head_str = std::str::from_utf8(head_content).ok();
let overlay_str = std::str::from_utf8(overlay_content).ok();
if let (Some(base), Some(head), Some(overlay)) = (base_str, head_str, overlay_str) {
match ast_merge::ast_merge(parser, file_path, base, head, overlay) {
Ok(result) => {
return match result.status {
ast_merge::MergeStatus::Clean => {
let merged_len = result.merged_content.len();
let min_agent_len = head.len().min(overlay.len());
if min_agent_len > 0 && merged_len * 5 < min_agent_len * 4 {
byte_level_analysis(
file_path,
base_content,
head_content,
overlay_content,
)
} else {
MergeAnalysis::AutoMerge {
merged_content: result.merged_content.into_bytes(),
}
}
}
ast_merge::MergeStatus::Conflict => MergeAnalysis::Conflict {
conflicts: result
.conflicts
.into_iter()
.map(|c| {
let their_change = infer_change_kind(&c.base, &c.version_a);
let our_change = infer_change_kind(&c.base, &c.version_b);
SemanticConflict {
file_path: file_path.to_string(),
symbol_name: c.qualified_name,
our_change,
their_change,
}
})
.collect(),
},
};
}
Err(e) => {
tracing::debug!(
file_path,
error = %e,
"ast_merge failed, falling back to byte-level analysis"
);
}
}
}
byte_level_analysis(file_path, base_content, head_content, overlay_content)
}
fn byte_level_analysis(
file_path: &str,
base_content: &[u8],
head_content: &[u8],
overlay_content: &[u8],
) -> MergeAnalysis {
let head_changed = base_content != head_content;
let overlay_changed = base_content != overlay_content;
if head_changed && overlay_changed && head_content != overlay_content {
MergeAnalysis::Conflict {
conflicts: vec![SemanticConflict {
file_path: file_path.to_string(),
symbol_name: "<entire file>".to_string(),
our_change: SymbolChangeKind::Modified,
their_change: SymbolChangeKind::Modified,
}],
}
} else {
MergeAnalysis::AutoMerge {
merged_content: if overlay_changed {
overlay_content.to_vec()
} else {
head_content.to_vec()
},
}
}
}
fn infer_change_kind(base: &str, current: &str) -> SymbolChangeKind {
match (base.is_empty(), current.is_empty()) {
(true, false) => SymbolChangeKind::Added,
(false, true) => SymbolChangeKind::Removed,
(false, false) => SymbolChangeKind::Modified,
(true, true) => SymbolChangeKind::Modified,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn byte_level_no_conflict_when_only_overlay_changed() {
let base = b"base content";
let head = b"base content"; let overlay = b"overlay content";
match byte_level_analysis("test.txt", base, head, overlay) {
MergeAnalysis::AutoMerge { merged_content } => {
assert_eq!(merged_content, overlay.to_vec());
}
MergeAnalysis::Conflict { .. } => panic!("expected auto-merge"),
}
}
#[test]
fn byte_level_no_conflict_when_only_head_changed() {
let base = b"base content";
let head = b"head content";
let overlay = b"base content";
match byte_level_analysis("test.txt", base, head, overlay) {
MergeAnalysis::AutoMerge { merged_content } => {
assert_eq!(merged_content, head.to_vec());
}
MergeAnalysis::Conflict { .. } => panic!("expected auto-merge"),
}
}
#[test]
fn byte_level_conflict_when_both_changed_differently() {
let base = b"base content";
let head = b"head content";
let overlay = b"overlay content";
match byte_level_analysis("test.txt", base, head, overlay) {
MergeAnalysis::Conflict { conflicts } => {
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].symbol_name, "<entire file>");
}
MergeAnalysis::AutoMerge { .. } => panic!("expected conflict"),
}
}
#[test]
fn byte_level_no_conflict_when_both_changed_identically() {
let base = b"base content";
let same = b"same content";
match byte_level_analysis("test.txt", base, same, same) {
MergeAnalysis::AutoMerge { .. } => {} MergeAnalysis::Conflict { .. } => panic!("expected auto-merge"),
}
}
#[test]
fn infer_change_kind_added() {
assert_eq!(infer_change_kind("", "fn new() {}"), SymbolChangeKind::Added);
}
#[test]
fn infer_change_kind_removed() {
assert_eq!(infer_change_kind("fn old() {}", ""), SymbolChangeKind::Removed);
}
#[test]
fn infer_change_kind_modified() {
assert_eq!(
infer_change_kind("fn foo() { 1 }", "fn foo() { 2 }"),
SymbolChangeKind::Modified
);
}
#[test]
fn infer_change_kind_both_empty() {
assert_eq!(infer_change_kind("", ""), SymbolChangeKind::Modified);
}
}