use crate::amalgamator::{AmalgamResult, amalgam_to_merge_result, amalgamate};
use crate::diff3;
use crate::parser::{self, ParseError};
use crate::patterns::PatternRegistry;
use crate::search::{self, SearchConfig};
use crate::types::*;
use crate::vsa;
pub struct ResolverConfig {
pub auto_accept_threshold: Confidence,
pub max_vsa_candidates: usize,
pub search_config: SearchConfig,
pub language: Option<Language>,
}
impl Default for ResolverConfig {
fn default() -> Self {
Self {
auto_accept_threshold: Confidence::Medium,
max_vsa_candidates: 100,
search_config: SearchConfig::default(),
language: None,
}
}
}
pub struct Resolver {
config: ResolverConfig,
patterns: PatternRegistry,
}
#[derive(Debug)]
pub struct ResolverOutput {
pub resolution: Option<ResolutionCandidate>,
pub candidates: Vec<ResolutionCandidate>,
pub strategies_tried: Vec<ResolutionStrategy>,
pub diff3_result: MergeResult,
}
impl Resolver {
pub fn new(config: ResolverConfig) -> Self {
Self {
config,
patterns: PatternRegistry::new(),
}
}
pub fn resolve_file(&self, base: &str, left: &str, right: &str) -> FileResolverOutput {
let scenario = MergeScenario::new(base, left, right);
let diff3_result = diff3::diff3_merge(&scenario);
match &diff3_result {
MergeResult::Resolved(content) => {
FileResolverOutput {
merged_content: content.clone(),
conflicts: vec![],
all_resolved: true,
}
}
MergeResult::Conflict { .. } => {
let _conflicts = diff3::extract_conflicts(&scenario);
let mut merged_parts = Vec::new();
let mut unresolved = Vec::new();
let mut all_resolved = true;
let hunks = diff3::diff3_hunks(&scenario);
for hunk in &hunks {
match hunk {
Diff3Hunk::Stable(lines)
| Diff3Hunk::LeftChanged(lines)
| Diff3Hunk::RightChanged(lines) => {
for line in lines {
merged_parts.push(line.clone());
}
}
Diff3Hunk::Conflict { base, left, right } => {
let conflict_scenario = MergeScenario::new(
base.join("\n").as_str().to_string(),
left.join("\n").as_str().to_string(),
right.join("\n").as_str().to_string(),
);
let output = self.resolve_conflict(
&conflict_scenario.base,
&conflict_scenario.left,
&conflict_scenario.right,
);
if let Some(ref resolution) = output.resolution {
for line in resolution.content.lines() {
merged_parts.push(line.to_string());
}
} else {
all_resolved = false;
merged_parts.push("<<<<<<< LEFT".to_string());
merged_parts.extend(left.iter().cloned());
merged_parts.push("||||||| BASE".to_string());
merged_parts.extend(base.iter().cloned());
merged_parts.push("=======".to_string());
merged_parts.extend(right.iter().cloned());
merged_parts.push(">>>>>>> RIGHT".to_string());
}
unresolved.push(output);
}
}
}
FileResolverOutput {
merged_content: merged_parts.join("\n"),
conflicts: unresolved,
all_resolved,
}
}
}
}
pub fn resolve_conflict(&self, base: &str, left: &str, right: &str) -> ResolverOutput {
let mut candidates: Vec<ResolutionCandidate> = Vec::new();
let mut strategies_tried = Vec::new();
let text_scenario = MergeScenario::new(base, left, right);
let diff3_result = diff3::diff3_merge(&text_scenario);
strategies_tried.push(ResolutionStrategy::PatternRule);
if let Some(resolution) = self.patterns.try_resolve(&text_scenario) {
if resolution.confidence >= self.config.auto_accept_threshold {
return ResolverOutput {
resolution: Some(resolution.clone()),
candidates: vec![resolution],
strategies_tried,
diff3_result,
};
}
candidates.push(resolution);
}
if let Some(lang) = self.config.language {
strategies_tried.push(ResolutionStrategy::StructuredMerge);
match self.try_structured_merge(base, left, right, lang) {
Ok(Some(result)) => {
if let MergeResult::Resolved(content) = result {
let resolution = ResolutionCandidate {
content,
confidence: Confidence::High,
strategy: ResolutionStrategy::StructuredMerge,
};
if resolution.confidence >= self.config.auto_accept_threshold {
return ResolverOutput {
resolution: Some(resolution.clone()),
candidates: vec![resolution],
strategies_tried,
diff3_result,
};
}
candidates.push(resolution);
}
}
Ok(None) => {} Err(_) => {} }
}
if let Some(lang) = self.config.language {
strategies_tried.push(ResolutionStrategy::VersionSpaceAlgebra);
if let Ok(vsa_candidates) = self.try_vsa_resolve(base, left, right, lang) {
candidates.extend(vsa_candidates);
}
}
strategies_tried.push(ResolutionStrategy::SearchBased);
let search_candidates = search::search_resolve(&text_scenario, &self.config.search_config);
candidates.extend(search_candidates);
candidates.sort_by(|a, b| b.confidence.cmp(&a.confidence));
let mut seen = std::collections::HashSet::new();
candidates.retain(|c| seen.insert(c.content.clone()));
let resolution = candidates
.first()
.filter(|c| c.confidence >= self.config.auto_accept_threshold)
.cloned();
ResolverOutput {
resolution,
candidates,
strategies_tried,
diff3_result,
}
}
fn try_structured_merge(
&self,
base: &str,
left: &str,
right: &str,
lang: Language,
) -> Result<Option<MergeResult>, ParseError> {
let base_tree = parser::parse_to_cst(base, lang)?;
let left_tree = parser::parse_to_cst(left, lang)?;
let right_tree = parser::parse_to_cst(right, lang)?;
let scenario = MergeScenario::new(&base_tree, &left_tree, &right_tree);
let result = amalgamate(&scenario);
match result {
AmalgamResult::Merged(_) => Ok(Some(amalgam_to_merge_result(&result))),
AmalgamResult::Conflict { .. } => Ok(None),
}
}
fn try_vsa_resolve(
&self,
base: &str,
left: &str,
right: &str,
lang: Language,
) -> Result<Vec<ResolutionCandidate>, ParseError> {
let base_tree = parser::parse_to_cst(base, lang)?;
let left_tree = parser::parse_to_cst(left, lang)?;
let right_tree = parser::parse_to_cst(right, lang)?;
let scenario = MergeScenario::new(&base_tree, &left_tree, &right_tree);
Ok(vsa::resolve_via_vsa(
&scenario,
self.config.max_vsa_candidates,
))
}
}
#[derive(Debug)]
pub struct FileResolverOutput {
pub merged_content: String,
pub conflicts: Vec<ResolverOutput>,
pub all_resolved: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clean_merge() {
let resolver = Resolver::new(ResolverConfig::default());
let result = resolver.resolve_file(
"line1\nline2\nline3\n",
"lineA\nline2\nline3\n",
"line1\nline2\nlineC\n",
);
assert!(result.all_resolved);
}
#[test]
fn test_pattern_resolves_whitespace() {
let resolver = Resolver::new(ResolverConfig::default());
let output = resolver.resolve_conflict("int x = 1;", "int x = 1;", "int x = 1;");
assert!(output.resolution.is_some());
assert_eq!(
output.resolution.unwrap().strategy,
ResolutionStrategy::PatternRule
);
}
#[test]
fn test_search_fallback() {
let resolver = Resolver::new(ResolverConfig::default());
let output = resolver.resolve_conflict(
"fn foo() { return 1; }",
"fn foo() { return 2; }",
"fn bar() { return 1; }",
);
assert!(!output.candidates.is_empty());
}
#[test]
fn test_structured_merge_rust() {
let config = ResolverConfig {
language: Some(Language::Rust),
..Default::default()
};
let resolver = Resolver::new(config);
let output = resolver.resolve_conflict(
"fn main() { let x = 1; }",
"fn main() { let x = 2; }",
"fn main() { let x = 1; let y = 3; }",
);
assert!(
output
.strategies_tried
.contains(&ResolutionStrategy::StructuredMerge)
);
}
}