use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::process::Command;
use std::sync::{mpsc, LazyLock};
use std::time::Duration;
use serde::Serialize;
use sem_core::model::change::ChangeType;
use sem_core::model::entity::SemanticEntity;
use sem_core::model::identity::match_entities;
use sem_core::parser::plugins::create_default_registry;
use sem_core::parser::registry::ParserRegistry;
static PARSER_REGISTRY: LazyLock<ParserRegistry> = LazyLock::new(create_default_registry);
use crate::conflict::{classify_conflict, ConflictComplexity, ConflictKind, EntityConflict, MarkerFormat, MergeStats};
use crate::region::{extract_regions, EntityRegion, FileRegion};
use crate::validate::SemanticWarning;
use crate::reconstruct::reconstruct;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResolutionStrategy {
Unchanged,
OursOnly,
TheirsOnly,
ContentEqual,
DiffyMerged,
DecoratorMerged,
InnerMerged,
ConflictBothModified,
ConflictModifyDelete,
ConflictBothAdded,
ConflictRenameRename,
ConflictRenameModify,
AddedOurs,
AddedTheirs,
Deleted,
Renamed { from: String, to: String },
Fallback,
}
#[derive(Debug, Clone, Serialize)]
pub struct EntityAudit {
pub name: String,
#[serde(rename = "type")]
pub entity_type: String,
pub resolution: ResolutionStrategy,
}
#[derive(Debug)]
pub struct MergeResult {
pub content: String,
pub conflicts: Vec<EntityConflict>,
pub warnings: Vec<SemanticWarning>,
pub stats: MergeStats,
pub audit: Vec<EntityAudit>,
}
impl MergeResult {
pub fn is_clean(&self) -> bool {
self.conflicts.is_empty()
&& !self.content.lines().any(|l| l.starts_with("<<<<<<< ours"))
}
}
#[derive(Debug, Clone)]
pub enum ResolvedEntity {
Clean(EntityRegion),
Conflict(EntityConflict),
ScopedConflict {
content: String,
conflict: EntityConflict,
},
Deleted,
}
pub fn entity_merge(
base: &str,
ours: &str,
theirs: &str,
file_path: &str,
) -> MergeResult {
entity_merge_fmt(base, ours, theirs, file_path, &MarkerFormat::default())
}
pub fn entity_merge_fmt(
base: &str,
ours: &str,
theirs: &str,
file_path: &str,
marker_format: &MarkerFormat,
) -> MergeResult {
let timeout_secs = std::env::var("WEAVE_TIMEOUT")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(5);
let base_owned = base.to_string();
let ours_owned = ours.to_string();
let theirs_owned = theirs.to_string();
let path_owned = file_path.to_string();
let fmt_owned = marker_format.clone();
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let result = entity_merge_with_registry(&base_owned, &ours_owned, &theirs_owned, &path_owned, &PARSER_REGISTRY, &fmt_owned);
let _ = tx.send(result);
});
match rx.recv_timeout(Duration::from_secs(timeout_secs)) {
Ok(result) => result,
Err(_) => {
eprintln!("weave: merge timed out after {}s for {}, falling back to git merge-file", timeout_secs, file_path);
let mut stats = MergeStats::default();
stats.used_fallback = true;
git_merge_file(base, ours, theirs, &mut stats)
}
}
}
pub fn entity_merge_with_registry(
base: &str,
ours: &str,
theirs: &str,
file_path: &str,
registry: &ParserRegistry,
marker_format: &MarkerFormat,
) -> MergeResult {
if has_conflict_markers(base) || has_conflict_markers(ours) || has_conflict_markers(theirs) {
let mut stats = MergeStats::default();
stats.entities_conflicted = 1;
stats.used_fallback = true;
let content = if has_conflict_markers(ours) {
ours
} else if has_conflict_markers(theirs) {
theirs
} else {
base
};
let complexity = classify_conflict(Some(base), Some(ours), Some(theirs));
return MergeResult {
content: content.to_string(),
conflicts: vec![EntityConflict {
entity_name: "(file)".to_string(),
entity_type: "file".to_string(),
kind: ConflictKind::BothModified,
complexity,
ours_content: Some(ours.to_string()),
theirs_content: Some(theirs.to_string()),
base_content: Some(base.to_string()),
}],
warnings: vec![],
stats,
audit: vec![],
};
}
if ours == theirs {
return MergeResult {
content: ours.to_string(),
conflicts: vec![],
warnings: vec![],
stats: MergeStats::default(),
audit: vec![],
};
}
if base == ours {
return MergeResult {
content: theirs.to_string(),
conflicts: vec![],
warnings: vec![],
stats: MergeStats {
entities_theirs_only: 1,
..Default::default()
},
audit: vec![],
};
}
if base == theirs {
return MergeResult {
content: ours.to_string(),
conflicts: vec![],
warnings: vec![],
stats: MergeStats {
entities_ours_only: 1,
..Default::default()
},
audit: vec![],
};
}
if is_binary(base) || is_binary(ours) || is_binary(theirs) {
let mut stats = MergeStats::default();
stats.used_fallback = true;
return git_merge_file(base, ours, theirs, &mut stats);
}
if base.len() > 1_000_000 || ours.len() > 1_000_000 || theirs.len() > 1_000_000 {
return line_level_fallback(base, ours, theirs, file_path);
}
let plugin = match registry.get_plugin(file_path) {
Some(p) if p.id() != "fallback" => p,
_ => return line_level_fallback(base, ours, theirs, file_path),
};
let base_all = plugin.extract_entities(base, file_path);
let ours_all = plugin.extract_entities(ours, file_path);
let theirs_all = plugin.extract_entities(theirs, file_path);
let base_entities = filter_nested_entities(base_all.clone());
let ours_entities = filter_nested_entities(ours_all.clone());
let theirs_entities = filter_nested_entities(theirs_all.clone());
if base_entities.is_empty() && !base.trim().is_empty() {
return line_level_fallback(base, ours, theirs, file_path);
}
if base.trim().is_empty() && !ours.trim().is_empty() && !theirs.trim().is_empty() {
return line_level_fallback(base, ours, theirs, file_path);
}
if ours_entities.is_empty() && !ours.trim().is_empty() && theirs_entities.is_empty() && !theirs.trim().is_empty() {
return line_level_fallback(base, ours, theirs, file_path);
}
if has_excessive_duplicates(&base_entities) || has_excessive_duplicates(&ours_entities) || has_excessive_duplicates(&theirs_entities) {
return line_level_fallback(base, ours, theirs, file_path);
}
let base_regions = extract_regions(base, &base_entities);
let ours_regions = extract_regions(ours, &ours_entities);
let theirs_regions = extract_regions(theirs, &theirs_entities);
let base_region_content = build_region_content_map(&base_regions);
let ours_region_content = build_region_content_map(&ours_regions);
let theirs_region_content = build_region_content_map(&theirs_regions);
let ours_changes = match_entities(&base_entities, &ours_entities, file_path, None, None, None);
let theirs_changes = match_entities(&base_entities, &theirs_entities, file_path, None, None, None);
let base_entity_map: HashMap<&str, &SemanticEntity> =
base_entities.iter().map(|e| (e.id.as_str(), e)).collect();
let ours_entity_map: HashMap<&str, &SemanticEntity> =
ours_entities.iter().map(|e| (e.id.as_str(), e)).collect();
let theirs_entity_map: HashMap<&str, &SemanticEntity> =
theirs_entities.iter().map(|e| (e.id.as_str(), e)).collect();
let mut ours_change_map: HashMap<String, ChangeType> = HashMap::new();
for change in &ours_changes.changes {
ours_change_map.insert(change.entity_id.clone(), change.change_type);
}
let mut theirs_change_map: HashMap<String, ChangeType> = HashMap::new();
for change in &theirs_changes.changes {
theirs_change_map.insert(change.entity_id.clone(), change.change_type);
}
let ours_rename_to_base = build_rename_map(&base_entities, &ours_entities);
let theirs_rename_to_base = build_rename_map(&base_entities, &theirs_entities);
let base_to_ours_rename: HashMap<String, String> = ours_rename_to_base
.iter()
.map(|(new, old)| (old.clone(), new.clone()))
.collect();
let base_to_theirs_rename: HashMap<String, String> = theirs_rename_to_base
.iter()
.map(|(new, old)| (old.clone(), new.clone()))
.collect();
let mut all_entity_ids: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut skip_ids: HashSet<String> = HashSet::new();
for new_id in ours_rename_to_base.keys() {
skip_ids.insert(new_id.clone());
}
for new_id in theirs_rename_to_base.keys() {
skip_ids.insert(new_id.clone());
}
for entity in &ours_entities {
if skip_ids.contains(&entity.id) {
continue;
}
if seen.insert(entity.id.clone()) {
all_entity_ids.push(entity.id.clone());
}
}
for entity in &theirs_entities {
if skip_ids.contains(&entity.id) {
continue;
}
if seen.insert(entity.id.clone()) {
all_entity_ids.push(entity.id.clone());
}
}
for entity in &base_entities {
if seen.insert(entity.id.clone()) {
all_entity_ids.push(entity.id.clone());
}
}
let mut stats = MergeStats::default();
let mut conflicts: Vec<EntityConflict> = Vec::new();
let mut audit: Vec<EntityAudit> = Vec::new();
let mut resolved_entities: HashMap<String, ResolvedEntity> = HashMap::new();
let mut rename_conflict_ids: HashSet<String> = HashSet::new();
for (base_id, ours_new_id) in &base_to_ours_rename {
if let Some(theirs_new_id) = base_to_theirs_rename.get(base_id) {
if ours_new_id != theirs_new_id {
rename_conflict_ids.insert(base_id.clone());
}
}
}
for entity_id in &all_entity_ids {
if rename_conflict_ids.contains(entity_id) {
let ours_new_id = &base_to_ours_rename[entity_id];
let theirs_new_id = &base_to_theirs_rename[entity_id];
let base_entity = base_entity_map.get(entity_id.as_str());
let ours_entity = ours_entity_map.get(ours_new_id.as_str());
let theirs_entity = theirs_entity_map.get(theirs_new_id.as_str());
let base_name = base_entity.map(|e| e.name.as_str()).unwrap_or(entity_id);
let ours_name = ours_entity.map(|e| e.name.as_str()).unwrap_or(ours_new_id);
let theirs_name = theirs_entity.map(|e| e.name.as_str()).unwrap_or(theirs_new_id);
let base_rc = base_entity.map(|e| base_region_content.get(e.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| e.content.clone()));
let ours_rc = ours_entity.map(|e| ours_region_content.get(e.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| e.content.clone()));
let theirs_rc = theirs_entity.map(|e| theirs_region_content.get(e.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| e.content.clone()));
stats.entities_conflicted += 1;
let conflict = EntityConflict {
entity_name: base_name.to_string(),
entity_type: base_entity.map(|e| e.entity_type.clone()).unwrap_or_default(),
kind: ConflictKind::RenameRename {
base_name: base_name.to_string(),
ours_name: ours_name.to_string(),
theirs_name: theirs_name.to_string(),
},
complexity: crate::conflict::ConflictComplexity::Syntax,
ours_content: ours_rc,
theirs_content: theirs_rc,
base_content: base_rc,
};
conflicts.push(conflict.clone());
audit.push(EntityAudit {
name: base_name.to_string(),
entity_type: base_entity.map(|e| e.entity_type.clone()).unwrap_or_default(),
resolution: ResolutionStrategy::ConflictRenameRename,
});
let resolution = ResolvedEntity::Conflict(conflict);
resolved_entities.insert(entity_id.clone(), resolution.clone());
resolved_entities.insert(ours_new_id.clone(), resolution);
resolved_entities.insert(theirs_new_id.clone(), ResolvedEntity::Deleted);
continue;
}
let in_base = base_entity_map.get(entity_id.as_str());
let ours_id = base_to_ours_rename.get(entity_id.as_str()).map(|s| s.as_str()).unwrap_or(entity_id.as_str());
let theirs_id = base_to_theirs_rename.get(entity_id.as_str()).map(|s| s.as_str()).unwrap_or(entity_id.as_str());
let in_ours = ours_entity_map.get(ours_id).or_else(|| ours_entity_map.get(entity_id.as_str()));
let in_theirs = theirs_entity_map.get(theirs_id).or_else(|| theirs_entity_map.get(entity_id.as_str()));
let ours_change = ours_change_map.get(entity_id);
let theirs_change = theirs_change_map.get(entity_id);
let (resolution, strategy) = resolve_entity(
entity_id,
in_base,
in_ours,
in_theirs,
ours_change,
theirs_change,
&base_region_content,
&ours_region_content,
&theirs_region_content,
&base_all,
&ours_all,
&theirs_all,
&mut stats,
marker_format,
);
let entity_name = in_ours.map(|e| e.name.as_str())
.or_else(|| in_theirs.map(|e| e.name.as_str()))
.or_else(|| in_base.map(|e| e.name.as_str()))
.unwrap_or(entity_id)
.to_string();
let entity_type = in_ours.map(|e| e.entity_type.as_str())
.or_else(|| in_theirs.map(|e| e.entity_type.as_str()))
.or_else(|| in_base.map(|e| e.entity_type.as_str()))
.unwrap_or("")
.to_string();
audit.push(EntityAudit {
name: entity_name,
entity_type,
resolution: strategy,
});
match &resolution {
ResolvedEntity::Conflict(ref c) => conflicts.push(c.clone()),
ResolvedEntity::ScopedConflict { conflict, .. } => conflicts.push(conflict.clone()),
_ => {}
}
resolved_entities.insert(entity_id.clone(), resolution.clone());
if let Some(ours_renamed_id) = base_to_ours_rename.get(entity_id.as_str()) {
resolved_entities.insert(ours_renamed_id.clone(), resolution.clone());
}
if let Some(theirs_renamed_id) = base_to_theirs_rename.get(entity_id.as_str()) {
resolved_entities.insert(theirs_renamed_id.clone(), resolution);
}
}
let (merged_interstitials, interstitial_conflicts) =
merge_interstitials(&base_regions, &ours_regions, &theirs_regions, marker_format);
stats.entities_conflicted += interstitial_conflicts.len();
conflicts.extend(interstitial_conflicts);
let theirs_rename_base_ids: HashSet<String> = base_to_ours_rename.keys().cloned().collect();
let content = reconstruct(
&ours_regions,
&theirs_regions,
&theirs_entities,
&ours_entity_map,
&resolved_entities,
&merged_interstitials,
marker_format,
&theirs_rename_base_ids,
);
let content = post_merge_cleanup(&content);
let mut warnings = vec![];
if conflicts.is_empty() && stats.entities_both_changed_merged > 0 {
let merged_entities = plugin.extract_entities(&content, file_path);
if merged_entities.is_empty() && !content.trim().is_empty() {
warnings.push(crate::validate::SemanticWarning {
entity_name: "(file)".to_string(),
entity_type: "file".to_string(),
file_path: file_path.to_string(),
kind: crate::validate::WarningKind::ParseFailedAfterMerge,
related: vec![],
});
}
if conflicts.is_empty() {
for (_, resolved) in &resolved_entities {
if let ResolvedEntity::Clean(region) = resolved {
let trimmed = region.content.trim();
if !trimmed.is_empty() && trimmed.len() > 20 && !content.contains(trimmed) {
return git_merge_file(base, ours, theirs, &mut stats);
}
}
}
}
if conflicts.is_empty() && !merged_entities.is_empty() {
let merged_top = filter_nested_entities(merged_entities);
let deleted_count = resolved_entities.values()
.filter(|r| matches!(r, ResolvedEntity::Deleted))
.count();
let expected_min = ours_entities.len().min(theirs_entities.len()).saturating_sub(deleted_count);
if expected_min > 3 && merged_top.len() < expected_min * 80 / 100 {
return git_merge_file(base, ours, theirs, &mut stats);
}
}
}
if conflicts.is_empty() {
let base_lines: HashSet<&str> = base.lines()
.map(|l| l.trim())
.filter(|l| l.len() > 15 && !is_import_line_trimmed(l))
.collect();
let ours_lines: HashSet<&str> = ours.lines()
.map(|l| l.trim())
.filter(|l| l.len() > 15 && !is_import_line_trimmed(l))
.collect();
let theirs_lines: HashSet<&str> = theirs.lines()
.map(|l| l.trim())
.filter(|l| l.len() > 15 && !is_import_line_trimmed(l))
.collect();
let output_lines: HashSet<&str> = content.lines()
.map(|l| l.trim())
.collect();
let missing_unchanged = base_lines.iter()
.filter(|l| ours_lines.contains(*l) && theirs_lines.contains(*l))
.filter(|l| !output_lines.contains(*l))
.count();
if missing_unchanged > 0 {
return git_merge_file(base, ours, theirs, &mut stats);
}
let mut missing_added_significant = 0;
let mut missing_added_short = 0;
for l in ours_lines.iter().chain(theirs_lines.iter()) {
if base_lines.contains(l) || output_lines.contains(l) {
continue;
}
if l.len() > 25 && content.contains(*l) {
continue;
}
if l.len() > 40 {
missing_added_significant += 1;
} else {
missing_added_short += 1;
}
}
if missing_added_significant > 0 || missing_added_short > 3 {
return git_merge_file(base, ours, theirs, &mut stats);
}
}
let entity_result = MergeResult {
content,
conflicts,
warnings,
stats: stats.clone(),
audit,
};
let entity_markers = entity_result.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
if entity_markers > 0 {
let git_result = git_merge_file(base, ours, theirs, &mut stats);
let git_markers = git_result.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
if entity_markers > git_markers {
return git_result;
}
}
if entity_markers == 0 {
let merged_len = entity_result.content.len();
let max_input_len = ours.len().max(theirs.len());
let min_input_len = ours.len().min(theirs.len());
if min_input_len > 200 && merged_len < min_input_len * 90 / 100 {
return git_merge_file(base, ours, theirs, &mut stats);
}
if max_input_len > 500 && merged_len < max_input_len * 70 / 100 {
let base_len = base.len();
let ours_deleted = base_len > ours.len() && (base_len - ours.len()) > max_input_len * 20 / 100;
let theirs_deleted = base_len > theirs.len() && (base_len - theirs.len()) > max_input_len * 20 / 100;
if !ours_deleted && !theirs_deleted {
return git_merge_file(base, ours, theirs, &mut stats);
}
}
}
entity_result
}
fn resolve_entity(
_entity_id: &str,
in_base: Option<&&SemanticEntity>,
in_ours: Option<&&SemanticEntity>,
in_theirs: Option<&&SemanticEntity>,
_ours_change: Option<&ChangeType>,
_theirs_change: Option<&ChangeType>,
base_region_content: &HashMap<&str, &str>,
ours_region_content: &HashMap<&str, &str>,
theirs_region_content: &HashMap<&str, &str>,
base_all: &[SemanticEntity],
ours_all: &[SemanticEntity],
theirs_all: &[SemanticEntity],
stats: &mut MergeStats,
marker_format: &MarkerFormat,
) -> (ResolvedEntity, ResolutionStrategy) {
let region_content = |entity: &SemanticEntity, map: &HashMap<&str, &str>| -> String {
map.get(entity.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| entity.content.clone())
};
match (in_base, in_ours, in_theirs) {
(Some(base), Some(ours), Some(theirs)) => {
let base_rc_lazy = || region_content(base, base_region_content);
let ours_rc_lazy = || region_content(ours, ours_region_content);
let theirs_rc_lazy = || region_content(theirs, theirs_region_content);
let ours_modified = ours.content_hash != base.content_hash
|| ours_rc_lazy() != base_rc_lazy();
let theirs_modified = theirs.content_hash != base.content_hash
|| theirs_rc_lazy() != base_rc_lazy();
match (ours_modified, theirs_modified) {
(false, false) => {
stats.entities_unchanged += 1;
(ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::Unchanged)
}
(true, false) => {
stats.entities_ours_only += 1;
(ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::OursOnly)
}
(false, true) => {
stats.entities_theirs_only += 1;
(ResolvedEntity::Clean(entity_to_region_with_content(theirs, ®ion_content(theirs, theirs_region_content))), ResolutionStrategy::TheirsOnly)
}
(true, true) => {
if ours.content_hash == theirs.content_hash {
stats.entities_both_changed_merged += 1;
(ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::ContentEqual)
} else {
let base_rc = region_content(base, base_region_content);
let ours_rc = region_content(ours, ours_region_content);
let theirs_rc = region_content(theirs, theirs_region_content);
let ours_renamed = base.name != ours.name;
let theirs_renamed = base.name != theirs.name;
let (merge_base_rc, merge_ours_rc, merge_theirs_rc, _final_name) =
if ours_renamed && !theirs_renamed {
let nb = replace_at_word_boundaries(&base_rc, &base.name, &ours.name);
let nt = replace_at_word_boundaries(&theirs_rc, &theirs.name, &ours.name);
(nb, ours_rc.clone(), nt, Some(&ours.name))
} else if theirs_renamed && !ours_renamed {
let nb = replace_at_word_boundaries(&base_rc, &base.name, &theirs.name);
let no = replace_at_word_boundaries(&ours_rc, &ours.name, &theirs.name);
(nb, no, theirs_rc.clone(), Some(&theirs.name))
} else {
(base_rc.clone(), ours_rc.clone(), theirs_rc.clone(), None)
};
if (ours_renamed && !theirs_renamed) || (!ours_renamed && theirs_renamed) {
let renamed_in_ours = ours_renamed;
let (old_name, new_name) = if renamed_in_ours {
(base.name.clone(), ours.name.clone())
} else {
(base.name.clone(), theirs.name.clone())
};
stats.entities_conflicted += 1;
let conflict = EntityConflict {
entity_name: base.name.clone(),
entity_type: base.entity_type.clone(),
kind: ConflictKind::RenameModify {
old_name,
new_name,
renamed_in_ours,
},
complexity: ConflictComplexity::SyntaxFunctional,
ours_content: Some(ours_rc),
theirs_content: Some(theirs_rc),
base_content: Some(base_rc),
};
return (ResolvedEntity::Conflict(conflict), ResolutionStrategy::ConflictRenameModify);
}
if is_whitespace_only_diff(&base_rc, &ours_rc) {
stats.entities_theirs_only += 1;
return (ResolvedEntity::Clean(entity_to_region_with_content(theirs, &theirs_rc)), ResolutionStrategy::TheirsOnly);
}
if is_whitespace_only_diff(&base_rc, &theirs_rc) {
stats.entities_ours_only += 1;
return (ResolvedEntity::Clean(entity_to_region_with_content(ours, &ours_rc)), ResolutionStrategy::OursOnly);
}
let output_entity = if ours_renamed { ours } else if theirs_renamed { theirs } else { ours };
match diffy_merge(&merge_base_rc, &merge_ours_rc, &merge_theirs_rc) {
Some(merged) => {
stats.entities_both_changed_merged += 1;
stats.resolved_via_diffy += 1;
(ResolvedEntity::Clean(EntityRegion {
entity_id: output_entity.id.clone(),
entity_name: output_entity.name.clone(),
entity_type: output_entity.entity_type.clone(),
content: merged,
start_line: output_entity.start_line,
end_line: output_entity.end_line,
}), ResolutionStrategy::DiffyMerged)
}
None => {
if let Some(merged) = try_decorator_aware_merge(&base_rc, &ours_rc, &theirs_rc) {
stats.entities_both_changed_merged += 1;
stats.resolved_via_diffy += 1;
return (ResolvedEntity::Clean(EntityRegion {
entity_id: ours.id.clone(),
entity_name: ours.name.clone(),
entity_type: ours.entity_type.clone(),
content: merged,
start_line: ours.start_line,
end_line: ours.end_line,
}), ResolutionStrategy::DecoratorMerged);
}
if is_container_entity_type(&ours.entity_type) {
let base_children = in_base
.map(|b| get_child_entities(b, base_all))
.unwrap_or_default();
let ours_children = get_child_entities(ours, ours_all);
let theirs_children = in_theirs
.map(|t| get_child_entities(t, theirs_all))
.unwrap_or_default();
let base_start = in_base.map(|b| b.start_line).unwrap_or(1);
let ours_start = ours.start_line;
let theirs_start = in_theirs.map(|t| t.start_line).unwrap_or(1);
if let Some(inner) = try_inner_entity_merge(
&base_rc, &ours_rc, &theirs_rc,
&base_children, &ours_children, &theirs_children,
base_start, ours_start, theirs_start,
marker_format,
) {
if inner.has_conflicts {
stats.entities_conflicted += 1;
stats.resolved_via_inner_merge += 1;
let complexity = classify_conflict(Some(&base_rc), Some(&ours_rc), Some(&theirs_rc));
return (ResolvedEntity::ScopedConflict {
content: inner.content,
conflict: EntityConflict {
entity_name: ours.name.clone(),
entity_type: ours.entity_type.clone(),
kind: ConflictKind::BothModified,
complexity,
ours_content: Some(ours_rc),
theirs_content: Some(theirs_rc),
base_content: Some(base_rc),
},
}, ResolutionStrategy::InnerMerged);
} else {
stats.entities_both_changed_merged += 1;
stats.resolved_via_inner_merge += 1;
return (ResolvedEntity::Clean(EntityRegion {
entity_id: ours.id.clone(),
entity_name: ours.name.clone(),
entity_type: ours.entity_type.clone(),
content: inner.content,
start_line: ours.start_line,
end_line: ours.end_line,
}), ResolutionStrategy::InnerMerged);
}
}
}
stats.entities_conflicted += 1;
let complexity = classify_conflict(Some(&base_rc), Some(&ours_rc), Some(&theirs_rc));
(ResolvedEntity::Conflict(EntityConflict {
entity_name: ours.name.clone(),
entity_type: ours.entity_type.clone(),
kind: ConflictKind::BothModified,
complexity,
ours_content: Some(ours_rc),
theirs_content: Some(theirs_rc),
base_content: Some(base_rc),
}), ResolutionStrategy::ConflictBothModified)
}
}
}
}
}
}
(Some(_base), Some(ours), None) => {
let ours_modified = ours.content_hash != _base.content_hash;
if ours_modified {
stats.entities_conflicted += 1;
let ours_rc = region_content(ours, ours_region_content);
let base_rc = region_content(_base, base_region_content);
let complexity = classify_conflict(Some(&base_rc), Some(&ours_rc), None);
(ResolvedEntity::Conflict(EntityConflict {
entity_name: ours.name.clone(),
entity_type: ours.entity_type.clone(),
kind: ConflictKind::ModifyDelete {
modified_in_ours: true,
},
complexity,
ours_content: Some(ours_rc),
theirs_content: None,
base_content: Some(base_rc),
}), ResolutionStrategy::ConflictModifyDelete)
} else {
stats.entities_deleted += 1;
(ResolvedEntity::Deleted, ResolutionStrategy::Deleted)
}
}
(Some(_base), None, Some(theirs)) => {
let theirs_modified = theirs.content_hash != _base.content_hash;
if theirs_modified {
stats.entities_conflicted += 1;
let theirs_rc = region_content(theirs, theirs_region_content);
let base_rc = region_content(_base, base_region_content);
let complexity = classify_conflict(Some(&base_rc), None, Some(&theirs_rc));
(ResolvedEntity::Conflict(EntityConflict {
entity_name: theirs.name.clone(),
entity_type: theirs.entity_type.clone(),
kind: ConflictKind::ModifyDelete {
modified_in_ours: false,
},
complexity,
ours_content: None,
theirs_content: Some(theirs_rc),
base_content: Some(base_rc),
}), ResolutionStrategy::ConflictModifyDelete)
} else {
stats.entities_deleted += 1;
(ResolvedEntity::Deleted, ResolutionStrategy::Deleted)
}
}
(None, Some(ours), None) => {
stats.entities_added_ours += 1;
(ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::AddedOurs)
}
(None, None, Some(theirs)) => {
stats.entities_added_theirs += 1;
(ResolvedEntity::Clean(entity_to_region_with_content(theirs, ®ion_content(theirs, theirs_region_content))), ResolutionStrategy::AddedTheirs)
}
(None, Some(ours), Some(theirs)) => {
if ours.content_hash == theirs.content_hash {
stats.entities_added_ours += 1;
(ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::ContentEqual)
} else {
stats.entities_conflicted += 1;
let ours_rc = region_content(ours, ours_region_content);
let theirs_rc = region_content(theirs, theirs_region_content);
let complexity = classify_conflict(None, Some(&ours_rc), Some(&theirs_rc));
(ResolvedEntity::Conflict(EntityConflict {
entity_name: ours.name.clone(),
entity_type: ours.entity_type.clone(),
kind: ConflictKind::BothAdded,
complexity,
ours_content: Some(ours_rc),
theirs_content: Some(theirs_rc),
base_content: None,
}), ResolutionStrategy::ConflictBothAdded)
}
}
(Some(_), None, None) => {
stats.entities_deleted += 1;
(ResolvedEntity::Deleted, ResolutionStrategy::Deleted)
}
(None, None, None) => (ResolvedEntity::Deleted, ResolutionStrategy::Deleted),
}
}
fn entity_to_region_with_content(entity: &SemanticEntity, content: &str) -> EntityRegion {
EntityRegion {
entity_id: entity.id.clone(),
entity_name: entity.name.clone(),
entity_type: entity.entity_type.clone(),
content: content.to_string(),
start_line: entity.start_line,
end_line: entity.end_line,
}
}
fn build_region_content_map(regions: &[FileRegion]) -> HashMap<&str, &str> {
regions
.iter()
.filter_map(|r| match r {
FileRegion::Entity(e) => Some((e.entity_id.as_str(), e.content.as_str())),
_ => None,
})
.collect()
}
fn is_whitespace_only_diff(a: &str, b: &str) -> bool {
if a == b {
return true; }
let a_normalized: Vec<&str> = a.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
let b_normalized: Vec<&str> = b.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
a_normalized == b_normalized
}
fn is_decorator_line(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with('@')
&& !trimmed.starts_with("@param")
&& !trimmed.starts_with("@return")
&& !trimmed.starts_with("@type")
&& !trimmed.starts_with("@see")
}
fn split_decorators(content: &str) -> (Vec<&str>, &str) {
let mut decorator_end = 0;
let mut byte_offset = 0;
for line in content.lines() {
if is_decorator_line(line) || line.trim().is_empty() {
decorator_end += 1;
byte_offset += line.len() + 1; } else {
break;
}
}
let lines: Vec<&str> = content.lines().collect();
while decorator_end > 0 && lines.get(decorator_end - 1).map_or(false, |l| l.trim().is_empty()) {
byte_offset -= lines[decorator_end - 1].len() + 1;
decorator_end -= 1;
}
let decorators: Vec<&str> = lines[..decorator_end]
.iter()
.filter(|l| is_decorator_line(l))
.copied()
.collect();
let body = &content[byte_offset.min(content.len())..];
(decorators, body)
}
fn try_decorator_aware_merge(base: &str, ours: &str, theirs: &str) -> Option<String> {
let (base_decorators, base_body) = split_decorators(base);
let (ours_decorators, ours_body) = split_decorators(ours);
let (theirs_decorators, theirs_body) = split_decorators(theirs);
if ours_decorators.is_empty() && theirs_decorators.is_empty() {
return None;
}
let merged_body = if base_body == ours_body && base_body == theirs_body {
base_body.to_string()
} else if base_body == ours_body {
theirs_body.to_string()
} else if base_body == theirs_body {
ours_body.to_string()
} else {
diffy_merge(base_body, ours_body, theirs_body)?
};
let base_set: HashSet<&str> = base_decorators.iter().copied().collect();
let ours_set: HashSet<&str> = ours_decorators.iter().copied().collect();
let theirs_set: HashSet<&str> = theirs_decorators.iter().copied().collect();
let ours_deleted: HashSet<&str> = base_set.difference(&ours_set).copied().collect();
let theirs_deleted: HashSet<&str> = base_set.difference(&theirs_set).copied().collect();
let mut merged_decorators: Vec<&str> = base_decorators
.iter()
.filter(|d| !ours_deleted.contains(**d) && !theirs_deleted.contains(**d))
.copied()
.collect();
for d in &ours_decorators {
if !base_set.contains(d) && !merged_decorators.contains(d) {
merged_decorators.push(d);
}
}
for d in &theirs_decorators {
if !base_set.contains(d) && !merged_decorators.contains(d) {
merged_decorators.push(d);
}
}
let mut result = String::new();
for d in &merged_decorators {
result.push_str(d);
result.push('\n');
}
result.push_str(&merged_body);
Some(result)
}
fn diffy_merge(base: &str, ours: &str, theirs: &str) -> Option<String> {
let result = diffy::merge(base, ours, theirs);
match result {
Ok(merged) => Some(merged),
Err(_conflicted) => None,
}
}
fn git_merge_string(base: &str, ours: &str, theirs: &str) -> Option<String> {
let dir = tempfile::tempdir().ok()?;
let base_path = dir.path().join("base");
let ours_path = dir.path().join("ours");
let theirs_path = dir.path().join("theirs");
std::fs::write(&base_path, base).ok()?;
std::fs::write(&ours_path, ours).ok()?;
std::fs::write(&theirs_path, theirs).ok()?;
let output = Command::new("git")
.arg("merge-file")
.arg("-p")
.arg(&ours_path)
.arg(&base_path)
.arg(&theirs_path)
.output()
.ok()?;
if output.status.success() {
String::from_utf8(output.stdout).ok()
} else {
None
}
}
fn merge_interstitials(
base_regions: &[FileRegion],
ours_regions: &[FileRegion],
theirs_regions: &[FileRegion],
marker_format: &MarkerFormat,
) -> (HashMap<String, String>, Vec<EntityConflict>) {
let base_map: HashMap<&str, &str> = base_regions
.iter()
.filter_map(|r| match r {
FileRegion::Interstitial(i) => Some((i.position_key.as_str(), i.content.as_str())),
_ => None,
})
.collect();
let ours_map: HashMap<&str, &str> = ours_regions
.iter()
.filter_map(|r| match r {
FileRegion::Interstitial(i) => Some((i.position_key.as_str(), i.content.as_str())),
_ => None,
})
.collect();
let theirs_map: HashMap<&str, &str> = theirs_regions
.iter()
.filter_map(|r| match r {
FileRegion::Interstitial(i) => Some((i.position_key.as_str(), i.content.as_str())),
_ => None,
})
.collect();
let mut all_keys: HashSet<&str> = HashSet::new();
all_keys.extend(base_map.keys());
all_keys.extend(ours_map.keys());
all_keys.extend(theirs_map.keys());
let mut merged: HashMap<String, String> = HashMap::new();
let mut interstitial_conflicts: Vec<EntityConflict> = Vec::new();
for key in all_keys {
let base_content = base_map.get(key).copied().unwrap_or("");
let ours_content = ours_map.get(key).copied().unwrap_or("");
let theirs_content = theirs_map.get(key).copied().unwrap_or("");
if ours_content == theirs_content {
merged.insert(key.to_string(), ours_content.to_string());
} else if base_content == ours_content {
merged.insert(key.to_string(), theirs_content.to_string());
} else if base_content == theirs_content {
merged.insert(key.to_string(), ours_content.to_string());
} else {
if is_whitespace_only_diff(base_content, ours_content)
&& is_whitespace_only_diff(base_content, theirs_content)
{
merged.insert(key.to_string(), theirs_content.to_string());
} else if is_whitespace_only_diff(base_content, ours_content) {
merged.insert(key.to_string(), theirs_content.to_string());
} else if is_whitespace_only_diff(base_content, theirs_content) {
merged.insert(key.to_string(), ours_content.to_string());
} else if is_import_region(base_content)
|| is_import_region(ours_content)
|| is_import_region(theirs_content)
{
let result = merge_imports_commutatively(base_content, ours_content, theirs_content);
merged.insert(key.to_string(), result);
} else {
match diffy::merge(base_content, ours_content, theirs_content) {
Ok(m) => {
merged.insert(key.to_string(), m);
}
Err(_conflicted) => {
let complexity = classify_conflict(
Some(base_content),
Some(ours_content),
Some(theirs_content),
);
let conflict = EntityConflict {
entity_name: key.to_string(),
entity_type: "interstitial".to_string(),
kind: ConflictKind::BothModified,
complexity,
ours_content: Some(ours_content.to_string()),
theirs_content: Some(theirs_content.to_string()),
base_content: Some(base_content.to_string()),
};
merged.insert(key.to_string(), conflict.to_conflict_markers(marker_format));
interstitial_conflicts.push(conflict);
}
}
}
}
}
(merged, interstitial_conflicts)
}
fn is_import_region(content: &str) -> bool {
let lines: Vec<&str> = content
.lines()
.filter(|l| !l.trim().is_empty())
.collect();
if lines.is_empty() {
return false;
}
let mut import_count = 0;
let mut in_multiline_import = false;
for line in &lines {
if in_multiline_import {
import_count += 1;
let trimmed = line.trim();
if trimmed.starts_with('}') || trimmed.ends_with(')') {
in_multiline_import = false;
}
} else if is_import_line(line) {
import_count += 1;
let trimmed = line.trim();
if (trimmed.contains('{') && !trimmed.contains('}'))
|| (trimmed.starts_with("import (") && !trimmed.contains(')'))
|| (trimmed.starts_with("from ") && trimmed.contains("import (") && !trimmed.contains(')'))
{
in_multiline_import = true;
}
}
}
import_count * 2 > lines.len()
}
fn post_merge_cleanup(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
let mut result: Vec<&str> = Vec::with_capacity(lines.len());
for line in &lines {
if line.trim().is_empty() {
result.push(line);
continue;
}
if let Some(prev) = result.last() {
if !prev.trim().is_empty() && *prev == *line && looks_like_declaration(line) {
continue; }
}
result.push(line);
}
let mut final_lines: Vec<&str> = Vec::with_capacity(result.len());
let mut consecutive_blanks = 0;
for line in &result {
if line.trim().is_empty() {
consecutive_blanks += 1;
if consecutive_blanks <= 2 {
final_lines.push(line);
}
} else {
consecutive_blanks = 0;
final_lines.push(line);
}
}
let mut out = final_lines.join("\n");
if content.ends_with('\n') && !out.ends_with('\n') {
out.push('\n');
}
out
}
fn looks_like_declaration(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with("import ")
|| trimmed.starts_with("from ")
|| trimmed.starts_with("use ")
|| trimmed.starts_with("export ")
|| trimmed.starts_with("require(")
|| trimmed.starts_with("#include")
|| trimmed.starts_with("typedef ")
|| trimmed.starts_with("using ")
|| (trimmed.starts_with("pub ") && trimmed.contains("mod "))
}
fn is_import_line(line: &str) -> bool {
if line.starts_with(' ') || line.starts_with('\t') {
return false;
}
let trimmed = line.trim();
trimmed.starts_with("import ")
|| trimmed.starts_with("from ")
|| trimmed.starts_with("use ")
|| trimmed.starts_with("require(")
|| trimmed.starts_with("const ") && trimmed.contains("require(")
|| trimmed.starts_with("package ")
|| trimmed.starts_with("#include ")
|| trimmed.starts_with("using ")
}
fn is_import_line_trimmed(trimmed: &str) -> bool {
trimmed.starts_with("import ")
|| trimmed.starts_with("from ")
|| trimmed.starts_with("use ")
|| trimmed.starts_with("require(")
|| trimmed.starts_with("const ") && trimmed.contains("require(")
|| trimmed.starts_with("package ")
|| trimmed.starts_with("#include ")
|| trimmed.starts_with("using ")
}
#[derive(Debug, Clone)]
struct ImportStatement {
lines: Vec<String>,
source: String,
specifiers: Vec<String>,
is_multiline: bool,
}
fn parse_single_line_specifiers(trimmed: &str) -> Vec<String> {
if let Some(import_pos) = trimmed.find(" import ") {
let after_import = &trimmed[import_pos + 8..];
if after_import.starts_with('*') || after_import.starts_with('(') {
return Vec::new();
}
return after_import
.split(',')
.map(|s| s.trim().trim_end_matches(';').to_string())
.filter(|s| !s.is_empty())
.collect();
}
if trimmed.starts_with("import ") {
if let Some(brace_start) = trimmed.find('{') {
if let Some(brace_end) = trimmed.find('}') {
let inner = &trimmed[brace_start + 1..brace_end];
return inner
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
}
}
Vec::new()
}
fn parse_import_statements(content: &str) -> (Vec<ImportStatement>, Vec<String>) {
let mut imports: Vec<ImportStatement> = Vec::new();
let mut non_import_lines: Vec<String> = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if line.trim().is_empty() {
non_import_lines.push(line.to_string());
i += 1;
continue;
}
if is_import_line(line) {
let trimmed = line.trim();
let starts_multiline = (trimmed.contains('{') && !trimmed.contains('}'))
|| (trimmed.starts_with("import (") && !trimmed.contains(')'))
|| (trimmed.starts_with("from ") && trimmed.contains("import (") && !trimmed.contains(')'));
if starts_multiline {
let mut block_lines = vec![line.to_string()];
let mut specifiers = Vec::new();
let close_char = if trimmed.contains('{') { '}' } else { ')' };
i += 1;
while i < lines.len() {
let inner = lines[i];
block_lines.push(inner.to_string());
let inner_trimmed = inner.trim();
if inner_trimmed.starts_with(close_char) {
break;
} else if !inner_trimmed.is_empty() {
let spec = inner_trimmed.trim_end_matches(',').trim().to_string();
if !spec.is_empty() {
specifiers.push(spec);
}
}
i += 1;
}
let full_text = block_lines.join("\n");
let source = import_source_prefix(&full_text).to_string();
imports.push(ImportStatement {
lines: block_lines,
source,
specifiers,
is_multiline: true,
});
} else {
let source = import_source_prefix(line).to_string();
let specifiers = parse_single_line_specifiers(trimmed);
imports.push(ImportStatement {
lines: vec![line.to_string()],
source,
specifiers,
is_multiline: false,
});
}
} else {
non_import_lines.push(line.to_string());
}
i += 1;
}
(imports, non_import_lines)
}
fn merge_imports_commutatively(base: &str, ours: &str, theirs: &str) -> String {
let (base_imports, _) = parse_import_statements(base);
let (ours_imports, _) = parse_import_statements(ours);
let (theirs_imports, _) = parse_import_statements(theirs);
let has_multiline = base_imports.iter().any(|i| i.is_multiline)
|| ours_imports.iter().any(|i| i.is_multiline)
|| theirs_imports.iter().any(|i| i.is_multiline);
if has_multiline {
return merge_imports_with_multiline(base, ours, theirs,
&base_imports, &ours_imports, &theirs_imports);
}
let base_lines: HashSet<&str> = base.lines().filter(|l| is_import_line(l)).collect();
let ours_lines: HashSet<&str> = ours.lines().filter(|l| is_import_line(l)).collect();
let theirs_deleted: HashSet<&str> = base_lines.difference(
&theirs.lines().filter(|l| is_import_line(l)).collect::<HashSet<&str>>()
).copied().collect();
let theirs_added: Vec<&str> = theirs
.lines()
.filter(|l| is_import_line(l) && !base_lines.contains(l) && !ours_lines.contains(l))
.collect();
let mut groups: Vec<Vec<&str>> = Vec::new();
let mut current_group: Vec<&str> = Vec::new();
for line in ours.lines() {
if line.trim().is_empty() {
if !current_group.is_empty() {
groups.push(current_group);
current_group = Vec::new();
}
} else if is_import_line(line) {
if theirs_deleted.contains(line) {
continue;
}
current_group.push(line);
}
}
if !current_group.is_empty() {
groups.push(current_group);
}
for add in &theirs_added {
let prefix = import_source_prefix(add);
let mut best_group = if groups.is_empty() { 0 } else { groups.len() - 1 };
for (i, group) in groups.iter().enumerate() {
if group.iter().any(|l| {
is_import_line(l) && import_source_prefix(l) == prefix
}) {
best_group = i;
break;
}
}
if best_group < groups.len() {
groups[best_group].push(add);
} else {
groups.push(vec![add]);
}
}
for group in &mut groups {
group.sort_unstable();
}
let mut import_lines: Vec<&str> = Vec::new();
for (i, group) in groups.iter().enumerate() {
if i > 0 {
import_lines.push("");
}
import_lines.extend(group);
}
let mut result = import_lines.join("\n");
let extract_non_imports = |content: &str| -> String {
content
.lines()
.filter(|l| !l.trim().is_empty() && !is_import_line(l))
.collect::<Vec<_>>()
.join("\n")
};
let base_ni = extract_non_imports(base);
let ours_ni = extract_non_imports(ours);
let theirs_ni = extract_non_imports(theirs);
if !base_ni.is_empty() || !ours_ni.is_empty() || !theirs_ni.is_empty() {
let merged_ni = match diffy::merge(&base_ni, &ours_ni, &theirs_ni) {
Ok(m) => m,
Err(conflicted) => conflicted,
};
if !merged_ni.trim().is_empty() {
result.push('\n');
result.push('\n');
result.push_str(&merged_ni);
}
}
let ours_trailing = ours.len() - ours.trim_end_matches('\n').len();
let result_trailing = result.len() - result.trim_end_matches('\n').len();
for _ in result_trailing..ours_trailing {
result.push('\n');
}
result
}
fn merge_imports_with_multiline(
_base_raw: &str,
ours_raw: &str,
_theirs_raw: &str,
base_imports: &[ImportStatement],
ours_imports: &[ImportStatement],
theirs_imports: &[ImportStatement],
) -> String {
let base_specs: HashMap<&str, HashSet<&str>> = base_imports.iter().map(|imp| {
let specs: HashSet<&str> = imp.specifiers.iter().map(|s| s.as_str()).collect();
(imp.source.as_str(), specs)
}).collect();
let theirs_specs: HashMap<&str, HashSet<&str>> = theirs_imports.iter().map(|imp| {
let specs: HashSet<&str> = imp.specifiers.iter().map(|s| s.as_str()).collect();
(imp.source.as_str(), specs)
}).collect();
let base_single: HashSet<String> = base_imports.iter()
.filter(|i| !i.is_multiline)
.map(|i| i.lines[0].clone())
.collect();
let theirs_single: HashSet<String> = theirs_imports.iter()
.filter(|i| !i.is_multiline)
.map(|i| i.lines[0].clone())
.collect();
let theirs_deleted_single: HashSet<&str> = base_single.iter()
.filter(|l| !theirs_single.contains(l.as_str()))
.map(|l| l.as_str())
.collect();
let mut result_parts: Vec<String> = Vec::new();
let mut handled_theirs_sources: HashSet<&str> = HashSet::new();
let lines: Vec<&str> = ours_raw.lines().collect();
let mut i = 0;
let mut ours_imp_idx = 0;
while i < lines.len() {
let line = lines[i];
if line.trim().is_empty() {
result_parts.push(line.to_string());
i += 1;
continue;
}
if is_import_line(line) {
let trimmed = line.trim();
let starts_multiline = (trimmed.contains('{') && !trimmed.contains('}'))
|| (trimmed.starts_with("import (") && !trimmed.contains(')'))
|| (trimmed.starts_with("from ") && trimmed.contains("import (") && !trimmed.contains(')'));
if starts_multiline && ours_imp_idx < ours_imports.len() {
let imp = &ours_imports[ours_imp_idx];
let source = imp.source.as_str();
handled_theirs_sources.insert(source);
let base_spec_set = base_specs.get(source).cloned().unwrap_or_default();
let theirs_spec_set = theirs_specs.get(source).cloned().unwrap_or_default();
let theirs_added: HashSet<&str> = theirs_spec_set.difference(&base_spec_set).copied().collect();
let theirs_removed: HashSet<&str> = base_spec_set.difference(&theirs_spec_set).copied().collect();
let mut final_specs: Vec<&str> = imp.specifiers.iter()
.map(|s| s.as_str())
.filter(|s| !theirs_removed.contains(s))
.collect();
for added in &theirs_added {
if !final_specs.contains(added) {
final_specs.push(added);
}
}
let indent = if imp.lines.len() > 1 {
let second = &imp.lines[1];
&second[..second.len() - second.trim_start().len()]
} else {
" "
};
result_parts.push(imp.lines[0].clone()); for spec in &final_specs {
result_parts.push(format!("{}{},", indent, spec));
}
if let Some(last) = imp.lines.last() {
result_parts.push(last.clone());
}
let close_char = if trimmed.contains('{') { '}' } else { ')' };
i += 1;
while i < lines.len() {
if lines[i].trim().starts_with(close_char) {
i += 1;
break;
}
i += 1;
}
ours_imp_idx += 1;
continue;
} else {
if ours_imp_idx < ours_imports.len() {
let imp = &ours_imports[ours_imp_idx];
let source = imp.source.as_str();
handled_theirs_sources.insert(source);
ours_imp_idx += 1;
if let Some(theirs_spec_set) = theirs_specs.get(source) {
let base_spec_set = base_specs.get(source).cloned().unwrap_or_default();
let theirs_added: HashSet<&str> = theirs_spec_set.difference(&base_spec_set).copied().collect();
if !theirs_added.is_empty() {
let mut ours_specifiers: Vec<&str> = Vec::new();
let trimmed_line = line.trim();
if let Some(import_pos) = trimmed_line.find(" import ") {
let after_import = &trimmed_line[import_pos + 8..];
for spec in after_import.split(',') {
let s = spec.trim().trim_end_matches(';');
if !s.is_empty() {
ours_specifiers.push(s);
}
}
}
if !ours_specifiers.is_empty() {
let mut final_specs: Vec<&str> = ours_specifiers.clone();
for added in &theirs_added {
if !final_specs.contains(added) {
final_specs.push(added);
}
}
if let Some(theirs_imp) = theirs_imports.iter().find(|ti| ti.source == source) {
if theirs_imp.is_multiline {
let indent = if theirs_imp.lines.len() > 1 {
let second = &theirs_imp.lines[1];
&second[..second.len() - second.trim_start().len()]
} else {
" "
};
result_parts.push(theirs_imp.lines[0].clone());
for spec in &final_specs {
result_parts.push(format!("{}{},", indent, spec));
}
if let Some(last) = theirs_imp.lines.last() {
result_parts.push(last.clone());
}
i += 1;
continue;
}
}
let prefix = &trimmed_line[..trimmed_line.find(" import ").unwrap() + 8];
result_parts.push(format!("{}{}", prefix, final_specs.join(", ")));
i += 1;
continue;
}
}
}
} else {
}
if !theirs_deleted_single.contains(line) {
result_parts.push(line.to_string());
}
}
} else {
result_parts.push(line.to_string());
}
i += 1;
}
for imp in theirs_imports {
if handled_theirs_sources.contains(imp.source.as_str()) {
continue;
}
for line in &imp.lines {
result_parts.push(line.clone());
}
}
let mut result = result_parts.join("\n");
let extract_non_imports = |content: &str| -> String {
content
.lines()
.filter(|l| !l.trim().is_empty() && !is_import_line(l))
.filter(|l| {
let t = l.trim();
!(t.ends_with(',') && !t.contains('='))
&& t != ")"
&& t != "}"
})
.collect::<Vec<_>>()
.join("\n")
};
let base_ni = extract_non_imports(_base_raw);
let ours_ni = extract_non_imports(ours_raw);
let theirs_ni = extract_non_imports(_theirs_raw);
if !base_ni.is_empty() || !ours_ni.is_empty() || !theirs_ni.is_empty() {
let merged_ni = match diffy::merge(&base_ni, &ours_ni, &theirs_ni) {
Ok(m) => m,
Err(conflicted) => conflicted,
};
if !merged_ni.trim().is_empty() {
result.push('\n');
result.push('\n');
result.push_str(&merged_ni);
}
}
let ours_trailing = ours_raw.len() - ours_raw.trim_end_matches('\n').len();
let result_trailing = result.len() - result.trim_end_matches('\n').len();
for _ in result_trailing..ours_trailing {
result.push('\n');
}
result
}
fn import_source_prefix(line: &str) -> &str {
for l in line.lines() {
let trimmed = l.trim();
if let Some(rest) = trimmed.strip_prefix("from ") {
return rest.split_whitespace().next().unwrap_or("");
}
if trimmed.starts_with('}') && trimmed.contains("from ") {
if let Some(quote_start) = trimmed.find(|c: char| c == '\'' || c == '"') {
let after = &trimmed[quote_start + 1..];
if let Some(quote_end) = after.find(|c: char| c == '\'' || c == '"') {
return &after[..quote_end];
}
}
}
if trimmed.starts_with("import ") {
if let Some(quote_start) = trimmed.find(|c: char| c == '\'' || c == '"') {
let after = &trimmed[quote_start + 1..];
if let Some(quote_end) = after.find(|c: char| c == '\'' || c == '"') {
return &after[..quote_end];
}
}
}
if let Some(rest) = trimmed.strip_prefix("use ") {
return rest.split("::").next().unwrap_or("").trim_end_matches(';');
}
}
line.trim()
}
fn line_level_fallback(base: &str, ours: &str, theirs: &str, file_path: &str) -> MergeResult {
let mut stats = MergeStats::default();
stats.used_fallback = true;
let skip = skip_sesame(file_path);
if skip {
return git_merge_file(base, ours, theirs, &mut stats);
}
let base_expanded = expand_separators(base);
let ours_expanded = expand_separators(ours);
let theirs_expanded = expand_separators(theirs);
let sesame_result = match diffy::merge(&base_expanded, &ours_expanded, &theirs_expanded) {
Ok(merged) => {
let content = collapse_separators(&merged, base);
Some(MergeResult {
content: post_merge_cleanup(&content),
conflicts: vec![],
warnings: vec![],
stats: stats.clone(),
audit: vec![],
})
}
Err(_) => {
match diffy::merge(base, ours, theirs) {
Ok(merged) => Some(MergeResult {
content: merged,
conflicts: vec![],
warnings: vec![],
stats: stats.clone(),
audit: vec![],
}),
Err(conflicted) => {
let _markers = conflicted.lines().filter(|l| l.starts_with("<<<<<<<")).count();
let mut s = stats.clone();
s.entities_conflicted = 1;
Some(MergeResult {
content: conflicted,
conflicts: vec![EntityConflict {
entity_name: "(file)".to_string(),
entity_type: "file".to_string(),
kind: ConflictKind::BothModified,
complexity: classify_conflict(Some(base), Some(ours), Some(theirs)),
ours_content: Some(ours.to_string()),
theirs_content: Some(theirs.to_string()),
base_content: Some(base.to_string()),
}],
warnings: vec![],
stats: s,
audit: vec![],
})
}
}
}
};
let git_result = git_merge_file(base, ours, theirs, &mut stats);
match sesame_result {
Some(sesame) if sesame.conflicts.is_empty() && !git_result.conflicts.is_empty() => {
sesame
}
Some(sesame) if !sesame.conflicts.is_empty() && !git_result.conflicts.is_empty() => {
let sesame_markers = sesame.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
let git_markers = git_result.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
if sesame_markers <= git_markers { sesame } else { git_result }
}
_ => git_result,
}
}
fn git_merge_file(base: &str, ours: &str, theirs: &str, stats: &mut MergeStats) -> MergeResult {
let dir = match tempfile::tempdir() {
Ok(d) => d,
Err(_) => return diffy_fallback(base, ours, theirs, stats),
};
let base_path = dir.path().join("base");
let ours_path = dir.path().join("ours");
let theirs_path = dir.path().join("theirs");
let write_ok = (|| -> std::io::Result<()> {
std::fs::File::create(&base_path)?.write_all(base.as_bytes())?;
std::fs::File::create(&ours_path)?.write_all(ours.as_bytes())?;
std::fs::File::create(&theirs_path)?.write_all(theirs.as_bytes())?;
Ok(())
})();
if write_ok.is_err() {
return diffy_fallback(base, ours, theirs, stats);
}
let output = Command::new("git")
.arg("merge-file")
.arg("-p") .arg("--diff3") .arg("-L").arg("ours")
.arg("-L").arg("base")
.arg("-L").arg("theirs")
.arg(&ours_path)
.arg(&base_path)
.arg(&theirs_path)
.output();
match output {
Ok(result) => {
let content = String::from_utf8_lossy(&result.stdout).into_owned();
if result.status.success() {
MergeResult {
content: post_merge_cleanup(&content),
conflicts: vec![],
warnings: vec![],
stats: stats.clone(),
audit: vec![],
}
} else {
stats.entities_conflicted = 1;
MergeResult {
content,
conflicts: vec![EntityConflict {
entity_name: "(file)".to_string(),
entity_type: "file".to_string(),
kind: ConflictKind::BothModified,
complexity: classify_conflict(Some(base), Some(ours), Some(theirs)),
ours_content: Some(ours.to_string()),
theirs_content: Some(theirs.to_string()),
base_content: Some(base.to_string()),
}],
warnings: vec![],
stats: stats.clone(),
audit: vec![],
}
}
}
Err(_) => diffy_fallback(base, ours, theirs, stats),
}
}
fn diffy_fallback(base: &str, ours: &str, theirs: &str, stats: &mut MergeStats) -> MergeResult {
match diffy::merge(base, ours, theirs) {
Ok(merged) => {
let content = post_merge_cleanup(&merged);
MergeResult {
content,
conflicts: vec![],
warnings: vec![],
stats: stats.clone(),
audit: vec![],
}
}
Err(conflicted) => {
stats.entities_conflicted = 1;
MergeResult {
content: conflicted,
conflicts: vec![EntityConflict {
entity_name: "(file)".to_string(),
entity_type: "file".to_string(),
kind: ConflictKind::BothModified,
complexity: classify_conflict(Some(base), Some(ours), Some(theirs)),
ours_content: Some(ours.to_string()),
theirs_content: Some(theirs.to_string()),
base_content: Some(base.to_string()),
}],
warnings: vec![],
stats: stats.clone(),
audit: vec![],
}
}
}
}
fn has_excessive_duplicates(entities: &[SemanticEntity]) -> bool {
let threshold = std::env::var("WEAVE_MAX_DUPLICATES")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(10);
let mut counts: HashMap<&str, usize> = HashMap::new();
for e in entities {
*counts.entry(&e.name).or_default() += 1;
}
counts.values().any(|&c| c >= threshold)
}
fn filter_nested_entities(mut entities: Vec<SemanticEntity>) -> Vec<SemanticEntity> {
if entities.len() <= 1 {
return entities;
}
entities.sort_by(|a, b| {
a.start_line.cmp(&b.start_line).then(b.end_line.cmp(&a.end_line))
});
let mut result: Vec<SemanticEntity> = Vec::with_capacity(entities.len());
let mut max_end: usize = 0;
for entity in entities {
if entity.start_line > max_end || max_end == 0 {
max_end = entity.end_line;
result.push(entity);
} else if entity.start_line == result.last().map_or(0, |e| e.start_line)
&& entity.end_line == result.last().map_or(0, |e| e.end_line)
{
result.push(entity);
}
}
result
}
fn get_child_entities<'a>(
parent: &SemanticEntity,
all_entities: &'a [SemanticEntity],
) -> Vec<&'a SemanticEntity> {
let mut children: Vec<&SemanticEntity> = all_entities
.iter()
.filter(|e| e.parent_id.as_deref() == Some(&parent.id))
.collect();
children.sort_by_key(|e| e.start_line);
children
}
fn token_jaccard(a: &str, b: &str) -> f64 {
let tokens_a: HashSet<&str> = a.split_whitespace().collect();
let tokens_b: HashSet<&str> = b.split_whitespace().collect();
if tokens_a.is_empty() && tokens_b.is_empty() {
return 1.0;
}
let intersection = tokens_a.intersection(&tokens_b).count();
let union = tokens_a.union(&tokens_b).count();
if union == 0 { 1.0 } else { intersection as f64 / union as f64 }
}
fn body_hash(entity: &SemanticEntity) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let normalized = replace_at_word_boundaries(&entity.content, &entity.name, "__ENTITY__");
let mut hasher = DefaultHasher::new();
normalized.hash(&mut hasher);
hasher.finish()
}
fn replace_at_word_boundaries(content: &str, needle: &str, replacement: &str) -> String {
if needle.is_empty() {
return content.to_string();
}
let bytes = content.as_bytes();
let mut result = String::with_capacity(content.len());
let mut i = 0;
while i < content.len() {
if content.is_char_boundary(i) && content[i..].starts_with(needle) {
let before_ok = i == 0 || {
let prev_idx = content[..i]
.char_indices()
.next_back()
.map(|(idx, _)| idx)
.unwrap_or(0);
!is_ident_char(bytes[prev_idx])
};
let after_idx = i + needle.len();
let after_ok = after_idx >= content.len()
|| (content.is_char_boundary(after_idx)
&& !is_ident_char(bytes[after_idx]));
if before_ok && after_ok {
result.push_str(replacement);
i += needle.len();
continue;
}
}
if content.is_char_boundary(i) {
let ch = content[i..].chars().next().unwrap();
result.push(ch);
i += ch.len_utf8();
} else {
i += 1;
}
}
result
}
fn is_ident_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn build_rename_map(
base_entities: &[SemanticEntity],
branch_entities: &[SemanticEntity],
) -> HashMap<String, String> {
let mut rename_map: HashMap<String, String> = HashMap::new();
let base_ids: HashSet<&str> = base_entities.iter().map(|e| e.id.as_str()).collect();
let mut base_by_body: HashMap<u64, Vec<&SemanticEntity>> = HashMap::new();
for entity in base_entities {
base_by_body.entry(body_hash(entity)).or_default().push(entity);
}
let mut base_by_structural: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
for entity in base_entities {
if let Some(ref sh) = entity.structural_hash {
base_by_structural.entry(sh.as_str()).or_default().push(entity);
}
}
struct RenameCandidate<'a> {
branch: &'a SemanticEntity,
base: &'a SemanticEntity,
confidence: f64,
}
let mut candidates: Vec<RenameCandidate> = Vec::new();
for branch_entity in branch_entities {
if base_ids.contains(branch_entity.id.as_str()) {
continue;
}
let bh = body_hash(branch_entity);
if let Some(base_entities_for_hash) = base_by_body.get(&bh) {
for &base_entity in base_entities_for_hash {
let same_type = base_entity.entity_type == branch_entity.entity_type;
let same_parent = base_entity.parent_id == branch_entity.parent_id;
let confidence = match (same_type, same_parent) {
(true, true) => 0.95,
(true, false) => 0.8,
(false, _) => 0.6,
};
candidates.push(RenameCandidate { branch: branch_entity, base: base_entity, confidence });
}
}
if let Some(ref sh) = branch_entity.structural_hash {
if let Some(base_entities_for_sh) = base_by_structural.get(sh.as_str()) {
for &base_entity in base_entities_for_sh {
if candidates.iter().any(|c| c.branch.id == branch_entity.id && c.base.id == base_entity.id) {
continue;
}
candidates.push(RenameCandidate { branch: branch_entity, base: base_entity, confidence: 0.6 });
}
}
}
let has_candidate = candidates.iter().any(|c| c.branch.id == branch_entity.id);
if !has_candidate {
for base_entity in base_entities {
if base_entity.entity_type != branch_entity.entity_type {
continue;
}
if base_entity.file_path != branch_entity.file_path {
continue;
}
if branch_entities.iter().any(|e| e.id == base_entity.id) {
continue;
}
let similarity = token_jaccard(&base_entity.content, &branch_entity.content);
if similarity >= 0.7 {
let same_parent = base_entity.parent_id == branch_entity.parent_id;
let confidence = if same_parent { 0.75 } else { 0.65 };
candidates.push(RenameCandidate { branch: branch_entity, base: base_entity, confidence });
}
}
}
}
candidates.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
let mut used_base_ids: HashSet<String> = HashSet::new();
let mut used_branch_ids: HashSet<String> = HashSet::new();
for candidate in &candidates {
if candidate.confidence < 0.6 {
break;
}
if used_base_ids.contains(&candidate.base.id) || used_branch_ids.contains(&candidate.branch.id) {
continue;
}
let base_id_in_branch = branch_entities.iter().any(|e| e.id == candidate.base.id);
if base_id_in_branch {
continue;
}
rename_map.insert(candidate.branch.id.clone(), candidate.base.id.clone());
used_base_ids.insert(candidate.base.id.clone());
used_branch_ids.insert(candidate.branch.id.clone());
}
rename_map
}
fn is_container_entity_type(entity_type: &str) -> bool {
matches!(
entity_type,
"class" | "interface" | "enum" | "impl" | "trait" | "module" | "impl_item" | "trait_item"
| "struct" | "union" | "namespace" | "struct_item" | "struct_specifier"
| "variable" | "export"
)
}
#[derive(Debug, Clone)]
struct MemberChunk {
name: String,
content: String,
}
struct InnerMergeResult {
content: String,
has_conflicts: bool,
}
fn children_to_chunks(
children: &[&SemanticEntity],
container_content: &str,
container_start_line: usize,
) -> Vec<MemberChunk> {
if children.is_empty() {
return Vec::new();
}
let lines: Vec<&str> = container_content.lines().collect();
let mut chunks = Vec::new();
for (i, child) in children.iter().enumerate() {
let child_start_idx = child.start_line.saturating_sub(container_start_line);
let child_end_idx = child.end_line.saturating_sub(container_start_line) + 1;
if child_end_idx > lines.len() + 1 || child_start_idx >= lines.len() {
chunks.push(MemberChunk {
name: child.name.clone(),
content: child.content.clone(),
});
continue;
}
let child_end_idx = child_end_idx.min(lines.len());
let floor = if i > 0 {
children[i - 1].end_line.saturating_sub(container_start_line) + 1
} else {
let header_end = if is_python_style_container(&lines) {
lines
.iter()
.position(|l| {
let t = l.trim();
(t.starts_with("class ") || t.starts_with("def ") || t.starts_with("async def "))
&& t.ends_with(':')
})
.map(|p| p + 1)
.unwrap_or(0)
} else {
lines
.iter()
.position(|l| l.contains('{'))
.map(|p| p + 1)
.unwrap_or(0)
};
header_end
};
let mut content_start = child_start_idx;
while content_start > floor {
let prev = content_start - 1;
let trimmed = lines[prev].trim();
if trimmed.starts_with('@')
|| trimmed.starts_with("#[")
|| trimmed.starts_with("//")
|| trimmed.starts_with("///")
|| trimmed.starts_with("/**")
|| trimmed.starts_with("* ")
|| trimmed == "*/"
{
content_start = prev;
} else if trimmed.is_empty() && content_start > floor + 1 {
content_start = prev;
} else {
break;
}
}
while content_start < child_start_idx && lines[content_start].trim().is_empty() {
content_start += 1;
}
let chunk_content: String = lines[content_start..child_end_idx].join("\n");
chunks.push(MemberChunk {
name: child.name.clone(),
content: chunk_content,
});
}
chunks
}
fn scoped_conflict_marker(
name: &str,
base: Option<&str>,
ours: Option<&str>,
theirs: Option<&str>,
ours_deleted: bool,
theirs_deleted: bool,
fmt: &MarkerFormat,
) -> String {
let open = "<".repeat(fmt.marker_length);
let sep = "=".repeat(fmt.marker_length);
let close = ">".repeat(fmt.marker_length);
let o = ours.unwrap_or("");
let t = theirs.unwrap_or("");
let ours_lines: Vec<&str> = o.lines().collect();
let theirs_lines: Vec<&str> = t.lines().collect();
let (prefix_len, suffix_len) = if ours.is_some() && theirs.is_some() {
crate::conflict::narrow_conflict_lines(&ours_lines, &theirs_lines)
} else {
(0, 0)
};
let has_narrowing = prefix_len > 0 || suffix_len > 0;
let ours_mid = &ours_lines[prefix_len..ours_lines.len() - suffix_len];
let theirs_mid = &theirs_lines[prefix_len..theirs_lines.len() - suffix_len];
let mut out = String::new();
if has_narrowing {
for line in &ours_lines[..prefix_len] {
out.push_str(line);
out.push('\n');
}
}
if fmt.enhanced {
if ours_deleted {
out.push_str(&format!("{} ours ({} deleted)\n", open, name));
} else {
out.push_str(&format!("{} ours ({})\n", open, name));
}
} else {
out.push_str(&format!("{} ours\n", open));
}
if ours.is_some() {
if has_narrowing {
for line in ours_mid {
out.push_str(line);
out.push('\n');
}
} else {
out.push_str(o);
if !o.ends_with('\n') {
out.push('\n');
}
}
}
if !fmt.enhanced {
let base_marker = "|".repeat(fmt.marker_length);
out.push_str(&format!("{} base\n", base_marker));
let b = base.unwrap_or("");
if has_narrowing {
let base_lines: Vec<&str> = b.lines().collect();
let base_prefix = prefix_len.min(base_lines.len());
let base_suffix = suffix_len.min(base_lines.len().saturating_sub(base_prefix));
for line in &base_lines[base_prefix..base_lines.len() - base_suffix] {
out.push_str(line);
out.push('\n');
}
} else {
out.push_str(b);
if !b.is_empty() && !b.ends_with('\n') {
out.push('\n');
}
}
}
out.push_str(&format!("{}\n", sep));
if theirs.is_some() {
if has_narrowing {
for line in theirs_mid {
out.push_str(line);
out.push('\n');
}
} else {
out.push_str(t);
if !t.ends_with('\n') {
out.push('\n');
}
}
}
if fmt.enhanced {
if theirs_deleted {
out.push_str(&format!("{} theirs ({} deleted)\n", close, name));
} else {
out.push_str(&format!("{} theirs ({})\n", close, name));
}
} else {
out.push_str(&format!("{} theirs\n", close));
}
if has_narrowing {
for line in &ours_lines[ours_lines.len() - suffix_len..] {
out.push_str(line);
out.push('\n');
}
}
out
}
fn try_inner_entity_merge(
base: &str,
ours: &str,
theirs: &str,
base_children: &[&SemanticEntity],
ours_children: &[&SemanticEntity],
theirs_children: &[&SemanticEntity],
base_start_line: usize,
ours_start_line: usize,
theirs_start_line: usize,
marker_format: &MarkerFormat,
) -> Option<InnerMergeResult> {
let use_children = !ours_children.is_empty() || !theirs_children.is_empty();
let (base_chunks, ours_chunks, theirs_chunks) = if use_children {
(
children_to_chunks(base_children, base, base_start_line),
children_to_chunks(ours_children, ours, ours_start_line),
children_to_chunks(theirs_children, theirs, theirs_start_line),
)
} else {
(
extract_member_chunks(base)?,
extract_member_chunks(ours)?,
extract_member_chunks(theirs)?,
)
};
if base_chunks.is_empty() && ours_chunks.is_empty() && theirs_chunks.is_empty() {
return None;
}
let base_map: HashMap<&str, &str> = base_chunks
.iter()
.map(|c| (c.name.as_str(), c.content.as_str()))
.collect();
let ours_map: HashMap<&str, &str> = ours_chunks
.iter()
.map(|c| (c.name.as_str(), c.content.as_str()))
.collect();
let theirs_map: HashMap<&str, &str> = theirs_chunks
.iter()
.map(|c| (c.name.as_str(), c.content.as_str()))
.collect();
let mut all_names: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for chunk in &ours_chunks {
if seen.insert(chunk.name.clone()) {
all_names.push(chunk.name.clone());
}
}
for chunk in &theirs_chunks {
if seen.insert(chunk.name.clone()) {
all_names.push(chunk.name.clone());
}
}
let (ours_header, ours_footer) = extract_container_wrapper(ours)?;
let mut merged_members: Vec<String> = Vec::new();
let mut has_conflict = false;
for name in &all_names {
let in_base = base_map.get(name.as_str());
let in_ours = ours_map.get(name.as_str());
let in_theirs = theirs_map.get(name.as_str());
match (in_base, in_ours, in_theirs) {
(Some(b), Some(o), Some(t)) => {
if o == t {
merged_members.push(o.to_string());
} else if b == o {
merged_members.push(t.to_string());
} else if b == t {
merged_members.push(o.to_string());
} else {
if let Some(merged) = diffy_merge(b, o, t) {
merged_members.push(merged);
} else if let Some(merged) = git_merge_string(b, o, t) {
merged_members.push(merged);
} else if let Some(merged) = try_decorator_aware_merge(b, o, t) {
merged_members.push(merged);
} else {
has_conflict = true;
merged_members.push(scoped_conflict_marker(name, Some(b), Some(o), Some(t), false, false, marker_format));
}
}
}
(Some(b), Some(o), None) => {
if *b == *o {
} else {
has_conflict = true;
merged_members.push(scoped_conflict_marker(name, Some(b), Some(o), None, false, true, marker_format));
}
}
(Some(b), None, Some(t)) => {
if *b == *t {
} else {
has_conflict = true;
merged_members.push(scoped_conflict_marker(name, Some(b), None, Some(t), true, false, marker_format));
}
}
(None, Some(o), None) => {
merged_members.push(o.to_string());
}
(None, None, Some(t)) => {
merged_members.push(t.to_string());
}
(None, Some(o), Some(t)) => {
if o == t {
merged_members.push(o.to_string());
} else {
has_conflict = true;
merged_members.push(scoped_conflict_marker(name, None, Some(o), Some(t), false, false, marker_format));
}
}
(Some(_), None, None) => {}
(None, None, None) => {}
}
}
let mut result = String::new();
result.push_str(ours_header);
if !ours_header.ends_with('\n') {
result.push('\n');
}
let has_multiline_members = merged_members.iter().any(|m| m.contains('\n'));
let original_has_blank_separators = {
let body = ours_header.len()..ours.rfind(ours_footer).unwrap_or(ours.len());
let body_content = &ours[body];
body_content.contains("\n\n")
};
for (i, member) in merged_members.iter().enumerate() {
result.push_str(member);
if !member.ends_with('\n') {
result.push('\n');
}
if i < merged_members.len() - 1 && has_multiline_members && original_has_blank_separators && !member.ends_with("\n\n") {
result.push('\n');
}
}
result.push_str(ours_footer);
if !ours_footer.ends_with('\n') && ours.ends_with('\n') {
result.push('\n');
}
if has_conflict && use_children {
if let (Some(bc), Some(oc), Some(tc)) = (
extract_member_chunks(base),
extract_member_chunks(ours),
extract_member_chunks(theirs),
) {
if !bc.is_empty() || !oc.is_empty() || !tc.is_empty() {
let fallback = try_inner_merge_with_chunks(
&bc, &oc, &tc, ours, ours_header, ours_footer,
has_multiline_members, marker_format,
);
if let Some(fb) = fallback {
if !fb.has_conflicts {
return Some(fb);
}
}
}
}
}
Some(InnerMergeResult {
content: result,
has_conflicts: has_conflict,
})
}
fn try_inner_merge_with_chunks(
base_chunks: &[MemberChunk],
ours_chunks: &[MemberChunk],
theirs_chunks: &[MemberChunk],
ours: &str,
ours_header: &str,
ours_footer: &str,
has_multiline_hint: bool,
marker_format: &MarkerFormat,
) -> Option<InnerMergeResult> {
let base_map: HashMap<&str, &str> = base_chunks.iter().map(|c| (c.name.as_str(), c.content.as_str())).collect();
let ours_map: HashMap<&str, &str> = ours_chunks.iter().map(|c| (c.name.as_str(), c.content.as_str())).collect();
let theirs_map: HashMap<&str, &str> = theirs_chunks.iter().map(|c| (c.name.as_str(), c.content.as_str())).collect();
let mut all_names: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for chunk in ours_chunks {
if seen.insert(chunk.name.clone()) {
all_names.push(chunk.name.clone());
}
}
for chunk in theirs_chunks {
if seen.insert(chunk.name.clone()) {
all_names.push(chunk.name.clone());
}
}
let mut merged_members: Vec<String> = Vec::new();
let mut has_conflict = false;
for name in &all_names {
let in_base = base_map.get(name.as_str());
let in_ours = ours_map.get(name.as_str());
let in_theirs = theirs_map.get(name.as_str());
match (in_base, in_ours, in_theirs) {
(Some(b), Some(o), Some(t)) => {
if o == t {
merged_members.push(o.to_string());
} else if b == o {
merged_members.push(t.to_string());
} else if b == t {
merged_members.push(o.to_string());
} else if let Some(merged) = diffy_merge(b, o, t) {
merged_members.push(merged);
} else if let Some(merged) = git_merge_string(b, o, t) {
merged_members.push(merged);
} else {
has_conflict = true;
merged_members.push(scoped_conflict_marker(name, Some(b), Some(o), Some(t), false, false, marker_format));
}
}
(Some(b), Some(o), None) => {
if *b != *o { merged_members.push(o.to_string()); }
}
(Some(b), None, Some(t)) => {
if *b != *t { merged_members.push(t.to_string()); }
}
(None, Some(o), None) => merged_members.push(o.to_string()),
(None, None, Some(t)) => merged_members.push(t.to_string()),
(None, Some(o), Some(t)) => {
if o == t {
merged_members.push(o.to_string());
} else {
has_conflict = true;
merged_members.push(scoped_conflict_marker(name, None, Some(o), Some(t), false, false, marker_format));
}
}
(Some(_), None, None) | (None, None, None) => {}
}
}
let has_multiline_members = has_multiline_hint || merged_members.iter().any(|m| m.contains('\n'));
let mut result = String::new();
result.push_str(ours_header);
if !ours_header.ends_with('\n') { result.push('\n'); }
for (i, member) in merged_members.iter().enumerate() {
result.push_str(member);
if !member.ends_with('\n') { result.push('\n'); }
if i < merged_members.len() - 1 && has_multiline_members && !member.ends_with("\n\n") {
result.push('\n');
}
}
result.push_str(ours_footer);
if !ours_footer.ends_with('\n') && ours.ends_with('\n') { result.push('\n'); }
Some(InnerMergeResult {
content: result,
has_conflicts: has_conflict,
})
}
fn is_python_style_container(lines: &[&str]) -> bool {
let has_colon_decl = lines.iter().any(|l| {
let trimmed = l.trim();
(trimmed.starts_with("class ")
|| trimmed.starts_with("def ")
|| trimmed.starts_with("async def "))
&& trimmed.ends_with(':')
});
if !has_colon_decl {
return false;
}
if let Some(decl) = lines.iter().find(|l| {
let t = l.trim();
(t.starts_with("class ") || t.starts_with("def ") || t.starts_with("async def "))
&& t.ends_with(':')
}) {
!decl.contains('{')
} else {
false
}
}
fn extract_container_wrapper(content: &str) -> Option<(&str, &str)> {
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 2 {
return None;
}
let is_python_style = is_python_style_container(&lines);
if is_python_style {
let header_end = lines.iter().position(|l| l.trim().ends_with(':'))?;
let header_byte_end: usize = lines[..=header_end]
.iter()
.map(|l| l.len() + 1)
.sum();
let header = &content[..header_byte_end.min(content.len())];
let footer = &content[content.len()..];
Some((header, footer))
} else {
let header_end = lines.iter().position(|l| l.contains('{'))?;
let header_byte_end = lines[..=header_end]
.iter()
.map(|l| l.len() + 1)
.sum::<usize>();
let header = &content[..header_byte_end.min(content.len())];
let footer_start = lines.iter().rposition(|l| {
let trimmed = l.trim();
trimmed == "}" || trimmed == "};"
})?;
let footer_byte_start: usize = lines[..footer_start]
.iter()
.map(|l| l.len() + 1)
.sum();
let footer = &content[footer_byte_start.min(content.len())..];
Some((header, footer))
}
}
fn extract_member_chunks(content: &str) -> Option<Vec<MemberChunk>> {
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 2 {
return None;
}
let is_python_style = is_python_style_container(&lines);
let body_start = if is_python_style {
lines.iter().position(|l| l.trim().ends_with(':'))? + 1
} else {
lines.iter().position(|l| l.contains('{'))? + 1
};
let body_end = if is_python_style {
lines.len()
} else {
lines.iter().rposition(|l| {
let trimmed = l.trim();
trimmed == "}" || trimmed == "};"
})?
};
if body_start >= body_end {
return None;
}
let member_indent = lines[body_start..body_end]
.iter()
.find(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())?;
let mut chunks: Vec<MemberChunk> = Vec::new();
let mut current_chunk_lines: Vec<&str> = Vec::new();
let mut current_name: Option<String> = None;
for line in &lines[body_start..body_end] {
let trimmed = line.trim();
if trimmed.is_empty() {
if current_name.is_some() {
current_chunk_lines.push(line);
}
continue;
}
let indent = line.len() - line.trim_start().len();
if indent == member_indent
&& !trimmed.starts_with("//")
&& !trimmed.starts_with("/*")
&& !trimmed.starts_with("*")
&& !trimmed.starts_with("#")
&& !trimmed.starts_with("@")
&& !trimmed.starts_with("}")
&& trimmed != ","
{
if let Some(name) = current_name.take() {
while current_chunk_lines.last().map_or(false, |l| l.trim().is_empty()) {
current_chunk_lines.pop();
}
if !current_chunk_lines.is_empty() {
chunks.push(MemberChunk {
name,
content: current_chunk_lines.join("\n"),
});
}
current_chunk_lines.clear();
}
let name = extract_member_name(trimmed);
current_name = Some(name);
current_chunk_lines.push(line);
} else if current_name.is_some() {
current_chunk_lines.push(line);
} else {
current_chunk_lines.push(line);
}
}
if let Some(name) = current_name {
while current_chunk_lines.last().map_or(false, |l| l.trim().is_empty()) {
current_chunk_lines.pop();
}
if !current_chunk_lines.is_empty() {
chunks.push(MemberChunk {
name,
content: current_chunk_lines.join("\n"),
});
}
}
for chunk in &mut chunks {
if chunk.name == "{" || chunk.name == "{}" {
if let Some(better) = derive_name_from_struct_literal(&chunk.content) {
chunk.name = better;
}
}
}
if chunks.is_empty() {
None
} else {
Some(chunks)
}
}
fn extract_member_name(line: &str) -> String {
let trimmed = line.trim();
if trimmed.starts_with("func ") && trimmed.get(5..6) == Some("(") {
if let Some(recv_close) = trimmed.find(')') {
let after_recv = &trimmed[recv_close + 1..];
if let Some(paren_pos) = after_recv.find('(') {
let before = after_recv[..paren_pos].trim();
let name: String = before
.chars()
.rev()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
if !name.is_empty() {
return name;
}
}
}
}
if let Some(paren_pos) = trimmed.find('(') {
let before = trimmed[..paren_pos].trim_end();
let name: String = before
.chars()
.rev()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
if !name.is_empty() {
return name;
}
}
let mut s = trimmed;
for keyword in &[
"export ", "public ", "private ", "protected ", "static ",
"abstract ", "async ", "override ", "readonly ",
"pub ", "pub(crate) ", "fn ", "def ", "get ", "set ",
] {
if s.starts_with(keyword) {
s = &s[keyword.len()..];
}
}
if s.starts_with("fn ") {
s = &s[3..];
}
let name: String = s
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if name.is_empty() {
trimmed.chars().take(20).collect()
} else {
name
}
}
fn derive_name_from_struct_literal(content: &str) -> Option<String> {
for line in content.lines().skip(1) {
let trimmed = line.trim().trim_end_matches(',');
if let Some(colon_pos) = trimmed.find(':') {
let value = trimmed[colon_pos + 1..].trim();
let value = value.trim_matches('"').trim_matches('\'');
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
fn is_binary(content: &str) -> bool {
content.as_bytes().iter().take(8192).any(|&b| b == 0)
}
fn has_conflict_markers(content: &str) -> bool {
content.contains("<<<<<<<") && content.contains(">>>>>>>")
}
fn skip_sesame(file_path: &str) -> bool {
let path_lower = file_path.to_lowercase();
let extensions = [
".json", ".yaml", ".yml", ".toml", ".lock", ".xml", ".csv", ".tsv",
".ini", ".cfg", ".conf", ".properties", ".env",
".md", ".markdown", ".txt", ".rst", ".svg", ".html", ".htm",
];
extensions.iter().any(|ext| path_lower.ends_with(ext))
}
fn expand_separators(content: &str) -> String {
let bytes = content.as_bytes();
let mut result = Vec::with_capacity(content.len() * 2);
let mut in_string = false;
let mut escape_next = false;
let mut string_char = b'"';
for &b in bytes {
if escape_next {
result.push(b);
escape_next = false;
continue;
}
if b == b'\\' && in_string {
result.push(b);
escape_next = true;
continue;
}
if !in_string && (b == b'"' || b == b'\'' || b == b'`') {
in_string = true;
string_char = b;
result.push(b);
continue;
}
if in_string && b == string_char {
in_string = false;
result.push(b);
continue;
}
if !in_string && (b == b'{' || b == b'}' || b == b';') {
if result.last() != Some(&b'\n') && !result.is_empty() {
result.push(b'\n');
}
result.push(b);
result.push(b'\n');
} else {
result.push(b);
}
}
unsafe { String::from_utf8_unchecked(result) }
}
fn collapse_separators(merged: &str, _base: &str) -> String {
let lines: Vec<&str> = merged.lines().collect();
let mut result = String::new();
let mut i = 0;
while i < lines.len() {
let trimmed = lines[i].trim();
if (trimmed == "{" || trimmed == "}" || trimmed == ";") && trimmed.len() == 1 {
if !result.is_empty() && !result.ends_with('\n') {
if trimmed == "{" {
result.push(' ');
result.push_str(trimmed);
result.push('\n');
} else if trimmed == "}" {
result.push('\n');
result.push_str(trimmed);
result.push('\n');
} else {
result.push_str(trimmed);
result.push('\n');
}
} else {
result.push_str(lines[i]);
result.push('\n');
}
} else {
result.push_str(lines[i]);
result.push('\n');
}
i += 1;
}
while result.ends_with("\n\n") {
result.pop();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replace_at_word_boundaries() {
assert_eq!(replace_at_word_boundaries("fn get() {}", "get", "__E__"), "fn __E__() {}");
assert_eq!(replace_at_word_boundaries("fn getAll() {}", "get", "__E__"), "fn getAll() {}");
assert_eq!(replace_at_word_boundaries("fn _get() {}", "get", "__E__"), "fn _get() {}");
assert_eq!(
replace_at_word_boundaries("pub enum Source { Source }", "Source", "__E__"),
"pub enum __E__ { __E__ }"
);
assert_eq!(
replace_at_word_boundaries("SourceManager isSource", "Source", "__E__"),
"SourceManager isSource"
);
assert_eq!(
replace_at_word_boundaries("❌ get ✅", "get", "__E__"),
"❌ __E__ ✅"
);
assert_eq!(
replace_at_word_boundaries("fn 名前() { get }", "get", "__E__"),
"fn 名前() { __E__ }"
);
assert_eq!(
replace_at_word_boundaries("🎉🚀✨", "get", "__E__"),
"🎉🚀✨"
);
}
#[test]
fn test_fast_path_identical() {
let content = "hello world";
let result = entity_merge(content, content, content, "test.ts");
assert!(result.is_clean());
assert_eq!(result.content, content);
}
#[test]
fn test_fast_path_only_ours_changed() {
let base = "hello";
let ours = "hello world";
let result = entity_merge(base, ours, base, "test.ts");
assert!(result.is_clean());
assert_eq!(result.content, ours);
}
#[test]
fn test_fast_path_only_theirs_changed() {
let base = "hello";
let theirs = "hello world";
let result = entity_merge(base, base, theirs, "test.ts");
assert!(result.is_clean());
assert_eq!(result.content, theirs);
}
#[test]
fn test_different_functions_no_conflict() {
let base = r#"export function existing() {
return 1;
}
"#;
let ours = r#"export function existing() {
return 1;
}
export function agentA() {
return "added by agent A";
}
"#;
let theirs = r#"export function existing() {
return 1;
}
export function agentB() {
return "added by agent B";
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
assert!(
result.is_clean(),
"Should auto-resolve: different functions added. Conflicts: {:?}",
result.conflicts
);
assert!(
result.content.contains("agentA"),
"Should contain agentA function"
);
assert!(
result.content.contains("agentB"),
"Should contain agentB function"
);
}
#[test]
fn test_same_function_modified_by_both_conflict() {
let base = r#"export function shared() {
return "original";
}
"#;
let ours = r#"export function shared() {
return "modified by ours";
}
"#;
let theirs = r#"export function shared() {
return "modified by theirs";
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
assert!(
!result.is_clean(),
"Should conflict when both modify same function differently"
);
assert_eq!(result.conflicts.len(), 1);
assert_eq!(result.conflicts[0].entity_name, "shared");
}
#[test]
fn test_fallback_for_unknown_filetype() {
let base = "line 1\nline 2\nline 3\nline 4\nline 5\n";
let ours = "line 1 modified\nline 2\nline 3\nline 4\nline 5\n";
let theirs = "line 1\nline 2\nline 3\nline 4\nline 5 modified\n";
let result = entity_merge(base, ours, theirs, "test.xyz");
assert!(
result.is_clean(),
"Non-adjacent changes should merge cleanly. Conflicts: {:?}",
result.conflicts,
);
}
#[test]
fn test_line_level_fallback() {
let base = "a\nb\nc\nd\ne\n";
let ours = "A\nb\nc\nd\ne\n";
let theirs = "a\nb\nc\nd\nE\n";
let result = line_level_fallback(base, ours, theirs, "test.rs");
assert!(result.is_clean());
assert!(result.stats.used_fallback);
assert_eq!(result.content, "A\nb\nc\nd\nE\n");
}
#[test]
fn test_line_level_fallback_conflict() {
let base = "a\nb\nc\n";
let ours = "X\nb\nc\n";
let theirs = "Y\nb\nc\n";
let result = line_level_fallback(base, ours, theirs, "test.rs");
assert!(!result.is_clean());
assert!(result.stats.used_fallback);
}
#[test]
fn test_expand_separators() {
let code = "function foo() { return 1; }";
let expanded = expand_separators(code);
assert!(expanded.contains("{\n"), "Opening brace should have newline after");
assert!(expanded.contains(";\n"), "Semicolons should have newline after");
assert!(expanded.contains("\n}"), "Closing brace should have newline before");
}
#[test]
fn test_expand_separators_preserves_strings() {
let code = r#"let x = "hello { world };";"#;
let expanded = expand_separators(code);
assert!(
expanded.contains("\"hello { world };\""),
"Separators in strings should be preserved: {}",
expanded
);
}
#[test]
fn test_is_import_region() {
assert!(is_import_region("import foo from 'foo';\nimport bar from 'bar';\n"));
assert!(is_import_region("use std::io;\nuse std::fs;\n"));
assert!(!is_import_region("let x = 1;\nlet y = 2;\n"));
assert!(!is_import_region("import foo from 'foo';\nlet x = 1;\nlet y = 2;\n"));
assert!(!is_import_region(""));
}
#[test]
fn test_is_import_line() {
assert!(is_import_line("import foo from 'foo';"));
assert!(is_import_line("import { bar } from 'bar';"));
assert!(is_import_line("from typing import List"));
assert!(is_import_line("use std::io::Read;"));
assert!(is_import_line("#include <stdio.h>"));
assert!(is_import_line("const fs = require('fs');"));
assert!(!is_import_line("let x = 1;"));
assert!(!is_import_line("function foo() {}"));
}
#[test]
fn test_commutative_import_merge_both_add_different() {
let base = "import a from 'a';\nimport b from 'b';\n";
let ours = "import a from 'a';\nimport b from 'b';\nimport c from 'c';\n";
let theirs = "import a from 'a';\nimport b from 'b';\nimport d from 'd';\n";
let result = merge_imports_commutatively(base, ours, theirs);
assert!(result.contains("import a from 'a';"));
assert!(result.contains("import b from 'b';"));
assert!(result.contains("import c from 'c';"));
assert!(result.contains("import d from 'd';"));
}
#[test]
fn test_commutative_import_merge_one_removes() {
let base = "import a from 'a';\nimport b from 'b';\nimport c from 'c';\n";
let ours = "import a from 'a';\nimport c from 'c';\n";
let theirs = "import a from 'a';\nimport b from 'b';\nimport c from 'c';\n";
let result = merge_imports_commutatively(base, ours, theirs);
assert!(result.contains("import a from 'a';"));
assert!(!result.contains("import b from 'b';"), "Removed import should stay removed");
assert!(result.contains("import c from 'c';"));
}
#[test]
fn test_commutative_import_merge_both_add_same() {
let base = "import a from 'a';\n";
let ours = "import a from 'a';\nimport b from 'b';\n";
let theirs = "import a from 'a';\nimport b from 'b';\n";
let result = merge_imports_commutatively(base, ours, theirs);
let count = result.matches("import b from 'b';").count();
assert_eq!(count, 1, "Duplicate import should be deduplicated");
}
#[test]
fn test_inner_entity_merge_different_methods() {
let base = r#"export class Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
"#;
let ours = r#"export class Calculator {
add(a: number, b: number): number {
// Added logging
console.log("adding", a, b);
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
"#;
let theirs = r#"export class Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
// Added validation
if (b > a) throw new Error("negative");
return a - b;
}
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
assert!(
result.is_clean(),
"Different methods modified should auto-merge via inner entity merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("console.log"), "Should contain ours changes");
assert!(result.content.contains("negative"), "Should contain theirs changes");
}
#[test]
fn test_inner_entity_merge_both_add_different_methods() {
let base = r#"export class Calculator {
add(a: number, b: number): number {
return a + b;
}
}
"#;
let ours = r#"export class Calculator {
add(a: number, b: number): number {
return a + b;
}
multiply(a: number, b: number): number {
return a * b;
}
}
"#;
let theirs = r#"export class Calculator {
add(a: number, b: number): number {
return a + b;
}
divide(a: number, b: number): number {
return a / b;
}
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
assert!(
result.is_clean(),
"Both adding different methods should auto-merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("multiply"), "Should contain ours's new method");
assert!(result.content.contains("divide"), "Should contain theirs's new method");
}
#[test]
fn test_inner_entity_merge_same_method_modified_still_conflicts() {
let base = r#"export class Calculator {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
"#;
let ours = r#"export class Calculator {
add(a: number, b: number): number {
return a + b + 1;
}
subtract(a: number, b: number): number {
return a - b;
}
}
"#;
let theirs = r#"export class Calculator {
add(a: number, b: number): number {
return a + b + 2;
}
subtract(a: number, b: number): number {
return a - b;
}
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
assert!(
!result.is_clean(),
"Both modifying same method differently should still conflict"
);
}
#[test]
fn test_extract_member_chunks() {
let class_body = r#"export class Foo {
bar() {
return 1;
}
baz() {
return 2;
}
}
"#;
let chunks = extract_member_chunks(class_body).unwrap();
assert_eq!(chunks.len(), 2, "Should find 2 members, found {:?}", chunks.iter().map(|c| &c.name).collect::<Vec<_>>());
assert_eq!(chunks[0].name, "bar");
assert_eq!(chunks[1].name, "baz");
}
#[test]
fn test_extract_member_name() {
assert_eq!(extract_member_name("add(a, b) {"), "add");
assert_eq!(extract_member_name("fn add(&self, a: i32) -> i32 {"), "add");
assert_eq!(extract_member_name("def add(self, a, b):"), "add");
assert_eq!(extract_member_name("public static getValue(): number {"), "getValue");
assert_eq!(extract_member_name("async fetchData() {"), "fetchData");
}
#[test]
fn test_commutative_import_merge_rust_use() {
let base = "use std::io;\nuse std::fs;\n";
let ours = "use std::io;\nuse std::fs;\nuse std::path::Path;\n";
let theirs = "use std::io;\nuse std::fs;\nuse std::collections::HashMap;\n";
let result = merge_imports_commutatively(base, ours, theirs);
assert!(result.contains("use std::path::Path;"));
assert!(result.contains("use std::collections::HashMap;"));
assert!(result.contains("use std::io;"));
assert!(result.contains("use std::fs;"));
}
#[test]
fn test_is_whitespace_only_diff_true() {
assert!(is_whitespace_only_diff(
" return 1;\n return 2;\n",
" return 1;\n return 2;\n"
));
assert!(is_whitespace_only_diff(
"return 1;\nreturn 2;\n",
"return 1;\n\nreturn 2;\n"
));
}
#[test]
fn test_is_whitespace_only_diff_false() {
assert!(!is_whitespace_only_diff(
" return 1;\n",
" return 2;\n"
));
assert!(!is_whitespace_only_diff(
"return 1;\n",
"return 1;\nconsole.log('x');\n"
));
}
#[test]
fn test_ts_interface_both_add_different_fields() {
let base = "interface Config {\n name: string;\n}\n";
let ours = "interface Config {\n name: string;\n age: number;\n}\n";
let theirs = "interface Config {\n name: string;\n email: string;\n}\n";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS interface: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content: {:?}", result.content);
assert!(
result.is_clean(),
"Both adding different fields to TS interface should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("age"));
assert!(result.content.contains("email"));
}
#[test]
fn test_rust_enum_both_add_different_variants() {
let base = "enum Color {\n Red,\n Blue,\n}\n";
let ours = "enum Color {\n Red,\n Blue,\n Green,\n}\n";
let theirs = "enum Color {\n Red,\n Blue,\n Yellow,\n}\n";
let result = entity_merge(base, ours, theirs, "test.rs");
eprintln!("Rust enum: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content: {:?}", result.content);
assert!(
result.is_clean(),
"Both adding different enum variants should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("Green"));
assert!(result.content.contains("Yellow"));
}
#[test]
fn test_python_both_add_different_decorators() {
let base = "def foo():\n return 1\n\ndef bar():\n return 2\n";
let ours = "@cache\ndef foo():\n return 1\n\ndef bar():\n return 2\n";
let theirs = "@deprecated\ndef foo():\n return 1\n\ndef bar():\n return 2\n";
let result = entity_merge(base, ours, theirs, "test.py");
assert!(
result.is_clean(),
"Both adding different decorators should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("@cache"));
assert!(result.content.contains("@deprecated"));
assert!(result.content.contains("def foo()"));
}
#[test]
fn test_decorator_plus_body_change() {
let base = "def foo():\n return 1\n";
let ours = "@cache\ndef foo():\n return 1\n";
let theirs = "def foo():\n return 42\n";
let result = entity_merge(base, ours, theirs, "test.py");
assert!(
result.is_clean(),
"Decorator + body change should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("@cache"));
assert!(result.content.contains("return 42"));
}
#[test]
fn test_ts_class_decorator_merge() {
let base = "class Foo {\n bar() {\n return 1;\n }\n}\n";
let ours = "class Foo {\n @Injectable()\n bar() {\n return 1;\n }\n}\n";
let theirs = "class Foo {\n @Deprecated()\n bar() {\n return 1;\n }\n}\n";
let result = entity_merge(base, ours, theirs, "test.ts");
assert!(
result.is_clean(),
"Both adding different decorators to same method should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("@Injectable()"));
assert!(result.content.contains("@Deprecated()"));
assert!(result.content.contains("bar()"));
}
#[test]
fn test_non_adjacent_intra_function_changes() {
let base = r#"export function process(data: any) {
const validated = validate(data);
const transformed = transform(validated);
const saved = save(transformed);
return saved;
}
"#;
let ours = r#"export function process(data: any) {
const validated = validate(data);
const transformed = transform(validated);
const saved = save(transformed);
console.log("saved", saved);
return saved;
}
"#;
let theirs = r#"export function process(data: any) {
console.log("input", data);
const validated = validate(data);
const transformed = transform(validated);
const saved = save(transformed);
return saved;
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
assert!(
result.is_clean(),
"Non-adjacent changes within same function should merge via diffy. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("console.log(\"saved\""));
assert!(result.content.contains("console.log(\"input\""));
}
#[test]
fn test_method_reordering_with_modification() {
let base = r#"class Service {
getUser(id: string) {
return db.find(id);
}
createUser(data: any) {
return db.create(data);
}
deleteUser(id: string) {
return db.delete(id);
}
}
"#;
let ours = r#"class Service {
getUser(id: string) {
return db.find(id);
}
deleteUser(id: string) {
return db.delete(id);
}
createUser(data: any) {
return db.create(data);
}
}
"#;
let theirs = r#"class Service {
getUser(id: string) {
console.log("fetching", id);
return db.find(id);
}
createUser(data: any) {
return db.create(data);
}
deleteUser(id: string) {
return db.delete(id);
}
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("Method reorder: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(
result.is_clean(),
"Method reordering + modification should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("console.log(\"fetching\""), "Should contain theirs modification");
assert!(result.content.contains("deleteUser"), "Should have deleteUser");
assert!(result.content.contains("createUser"), "Should have createUser");
}
#[test]
fn test_doc_comment_plus_body_change() {
let base = r#"export function calculate(a: number, b: number): number {
return a + b;
}
"#;
let ours = r#"/**
* Calculate the sum of two numbers.
* @param a - First number
* @param b - Second number
*/
export function calculate(a: number, b: number): number {
return a + b;
}
"#;
let theirs = r#"export function calculate(a: number, b: number): number {
const result = a + b;
console.log("result:", result);
return result;
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("Doc comment + body: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
}
#[test]
fn test_both_add_different_guard_clauses() {
let base = r#"export function processOrder(order: Order): Result {
const total = calculateTotal(order);
return { success: true, total };
}
"#;
let ours = r#"export function processOrder(order: Order): Result {
if (!order) throw new Error("Order required");
const total = calculateTotal(order);
return { success: true, total };
}
"#;
let theirs = r#"export function processOrder(order: Order): Result {
if (order.items.length === 0) throw new Error("Empty order");
const total = calculateTotal(order);
return { success: true, total };
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("Guard clauses: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
}
#[test]
fn test_both_modify_different_enum_variants() {
let base = r#"enum Status {
Active = "active",
Inactive = "inactive",
Pending = "pending",
}
"#;
let ours = r#"enum Status {
Active = "active",
Inactive = "disabled",
Pending = "pending",
}
"#;
let theirs = r#"enum Status {
Active = "active",
Inactive = "inactive",
Pending = "pending",
Deleted = "deleted",
}
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("Enum modify+add: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(
result.is_clean(),
"Modify variant + add new variant should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("\"disabled\""), "Should have modified Inactive");
assert!(result.content.contains("Deleted"), "Should have new Deleted variant");
}
#[test]
fn test_config_object_field_additions() {
let base = r#"export const config = {
timeout: 5000,
retries: 3,
};
"#;
let ours = r#"export const config = {
timeout: 5000,
retries: 3,
maxConnections: 10,
};
"#;
let theirs = r#"export const config = {
timeout: 5000,
retries: 3,
logLevel: "info",
};
"#;
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("Config fields: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
}
#[test]
fn test_rust_impl_block_both_add_methods() {
let base = r#"impl Calculator {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
}
"#;
let ours = r#"impl Calculator {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
fn multiply(&self, a: i32, b: i32) -> i32 {
a * b
}
}
"#;
let theirs = r#"impl Calculator {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
fn divide(&self, a: i32, b: i32) -> i32 {
a / b
}
}
"#;
let result = entity_merge(base, ours, theirs, "test.rs");
eprintln!("Rust impl: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(
result.is_clean(),
"Both adding methods to Rust impl should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("multiply"), "Should have multiply");
assert!(result.content.contains("divide"), "Should have divide");
}
#[test]
fn test_rust_impl_same_trait_different_types() {
let base = r#"struct Foo;
struct Bar;
impl Stream for Foo {
type Item = i32;
fn poll_next(&self) -> Option<i32> {
Some(1)
}
}
impl Stream for Bar {
type Item = String;
fn poll_next(&self) -> Option<String> {
Some("hello".into())
}
}
fn other() {}
"#;
let ours = r#"struct Foo;
struct Bar;
impl Stream for Foo {
type Item = i32;
fn poll_next(&self) -> Option<i32> {
let x = compute();
Some(x + 1)
}
}
impl Stream for Bar {
type Item = String;
fn poll_next(&self) -> Option<String> {
Some("hello".into())
}
}
fn other() {}
"#;
let theirs = r#"struct Foo;
struct Bar;
impl Stream for Foo {
type Item = i32;
fn poll_next(&self) -> Option<i32> {
Some(1)
}
}
impl Stream for Bar {
type Item = String;
fn poll_next(&self) -> Option<String> {
let s = format!("hello {}", name);
Some(s)
}
}
fn other() {}
"#;
let result = entity_merge(base, ours, theirs, "test.rs");
assert!(
result.is_clean(),
"Same trait, different types should not conflict. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("impl Stream for Foo"), "Should have Foo impl");
assert!(result.content.contains("impl Stream for Bar"), "Should have Bar impl");
assert!(result.content.contains("compute()"), "Should have ours' Foo change");
assert!(result.content.contains("format!"), "Should have theirs' Bar change");
}
#[test]
fn test_rust_doc_comment_plus_body_change() {
let base = r#"fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
"#;
let ours = r#"/// Adds two numbers together.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
"#;
let theirs = r#"fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b - 1
}
"#;
let result = entity_merge(base, ours, theirs, "test.rs");
assert!(
result.is_clean(),
"Rust doc comment + body change should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("/// Adds two numbers"), "Should have ours doc comment");
assert!(result.content.contains("a - b - 1"), "Should have theirs body change");
}
#[test]
fn test_both_add_different_doc_comments() {
let base = r#"fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
"#;
let ours = r#"/// Adds two numbers.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
"#;
let theirs = r#"fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Subtracts b from a.
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
"#;
let result = entity_merge(base, ours, theirs, "test.rs");
assert!(
result.is_clean(),
"Both adding doc comments to different functions should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("/// Adds two numbers"), "Should have add's doc comment");
assert!(result.content.contains("/// Subtracts b from a"), "Should have subtract's doc comment");
}
#[test]
fn test_go_import_block_both_add_different() {
let base = "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n";
let ours = "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n";
let theirs = "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"io\"\n)\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n";
let result = entity_merge(base, ours, theirs, "main.go");
eprintln!("Go import block: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
}
#[test]
fn test_python_class_both_add_methods() {
let base = "class Calculator:\n def add(self, a, b):\n return a + b\n";
let ours = "class Calculator:\n def add(self, a, b):\n return a + b\n\n def multiply(self, a, b):\n return a * b\n";
let theirs = "class Calculator:\n def add(self, a, b):\n return a + b\n\n def divide(self, a, b):\n return a / b\n";
let result = entity_merge(base, ours, theirs, "test.py");
eprintln!("Python class: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(
result.is_clean(),
"Both adding methods to Python class should merge. Conflicts: {:?}",
result.conflicts,
);
assert!(result.content.contains("multiply"), "Should have multiply");
assert!(result.content.contains("divide"), "Should have divide");
}
#[test]
fn test_interstitial_conflict_not_silently_embedded() {
let base = r#"export { alpha } from "./alpha";
// Section: data utilities
// TODO: add more exports here
export { beta } from "./beta";
"#;
let ours = r#"export { alpha } from "./alpha";
// Section: data utilities (sorting)
// Sorting helpers for list views
export { beta } from "./beta";
"#;
let theirs = r#"export { alpha } from "./alpha";
// Section: data utilities (filtering)
// Filtering helpers for search views
export { beta } from "./beta";
"#;
let result = entity_merge(base, ours, theirs, "index.ts");
let has_markers = result.content.contains("<<<<<<<") || result.content.contains(">>>>>>>");
if has_markers {
assert!(
!result.is_clean(),
"BUG: is_clean()=true but merged content has conflict markers!\n\
stats: {}\nconflicts: {:?}\ncontent:\n{}",
result.stats, result.conflicts, result.content
);
assert!(
result.stats.entities_conflicted > 0,
"entities_conflicted should be > 0 when markers are present"
);
}
if result.is_clean() {
assert!(
!has_markers,
"Clean merge should not contain conflict markers!\ncontent:\n{}",
result.content
);
}
}
#[test]
fn test_pre_conflicted_input_not_treated_as_clean() {
let base = "";
let theirs = "";
let ours = r#"/**
* MIT License
*/
<<<<<<<< HEAD:src/lib/exports/index.ts
export { renderDocToBuffer } from "./doc-exporter";
export type { ExportOptions, ExportMetadata, RenderContext } from "./types";
========
export * from "./editor";
export * from "./types";
>>>>>>>> feature:packages/core/src/editor/index.ts
"#;
let result = entity_merge(base, ours, theirs, "index.ts");
assert!(
!result.is_clean(),
"Pre-conflicted input must not be reported as clean!\n\
stats: {}\nconflicts: {:?}",
result.stats, result.conflicts,
);
assert!(result.stats.entities_conflicted > 0);
assert!(!result.conflicts.is_empty());
}
#[test]
fn test_multi_line_signature_classified_as_syntax() {
let base = "function process(\n a: number,\n b: string\n) {\n return a;\n}\n";
let ours = "function process(\n a: number,\n b: string,\n c: boolean\n) {\n return a;\n}\n";
let theirs = "function process(\n a: number,\n b: number\n) {\n return a;\n}\n";
let complexity = crate::conflict::classify_conflict(Some(base), Some(ours), Some(theirs));
assert_eq!(
complexity,
crate::conflict::ConflictComplexity::Syntax,
"Multi-line signature change should be classified as Syntax, got {:?}",
complexity
);
}
#[test]
fn test_grouped_import_merge_preserves_groups() {
let base = "import os\nimport sys\n\nfrom collections import OrderedDict\nfrom typing import List\n";
let ours = "import os\nimport sys\nimport json\n\nfrom collections import OrderedDict\nfrom typing import List\n";
let theirs = "import os\nimport sys\n\nfrom collections import OrderedDict\nfrom collections import defaultdict\nfrom typing import List\n";
let result = merge_imports_commutatively(base, ours, theirs);
let lines: Vec<&str> = result.lines().collect();
let json_idx = lines.iter().position(|l| l.contains("json"));
let blank_idx = lines.iter().position(|l| l.trim().is_empty());
let defaultdict_idx = lines.iter().position(|l| l.contains("defaultdict"));
assert!(json_idx.is_some(), "json import should be present");
assert!(blank_idx.is_some(), "blank line separator should be present");
assert!(defaultdict_idx.is_some(), "defaultdict import should be present");
assert!(json_idx.unwrap() < blank_idx.unwrap(), "json should be in first group");
assert!(defaultdict_idx.unwrap() > blank_idx.unwrap(), "defaultdict should be in second group");
}
#[test]
fn test_configurable_duplicate_threshold() {
let entities: Vec<SemanticEntity> = (0..15).map(|i| SemanticEntity {
id: format!("test::function::test_{}", i),
file_path: "test.ts".to_string(),
entity_type: "function".to_string(),
name: "test".to_string(),
parent_id: None,
content: format!("function test() {{ return {}; }}", i),
content_hash: format!("hash_{}", i),
structural_hash: None,
start_line: i * 3 + 1,
end_line: i * 3 + 3,
metadata: None,
}).collect();
assert!(has_excessive_duplicates(&entities));
std::env::set_var("WEAVE_MAX_DUPLICATES", "20");
assert!(!has_excessive_duplicates(&entities));
std::env::remove_var("WEAVE_MAX_DUPLICATES");
}
#[test]
fn test_ts_multiline_import_consolidation() {
let base = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let ours = base;
let theirs = "\
import {
type Foo,
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS import consolidation: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(result.content.contains("import {"), "import {{ must not be dropped");
assert!(result.content.contains("type Foo,"), "type Foo must be present");
assert!(result.content.contains("} from \"./foo\""), "closing must be present");
assert!(!result.content.contains("import type { Foo }"), "old separate import should be removed");
}
#[test]
fn test_ts_multiline_import_both_modify() {
let base = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let ours = "\
import {
type Foo,
type a,
type b,
type c,
type d,
} from \"./foo\"
export function bar() {
return 1;
}
";
let theirs = "\
import {
type Foo,
type a,
type b,
type c,
type e,
} from \"./foo\"
export function bar() {
return 1;
}
";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS import both modify: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(result.content.contains("import {"), "import {{ must not be dropped");
assert!(result.content.contains("type Foo,"), "type Foo must be present");
assert!(result.content.contains("type d,"), "ours addition must be present");
assert!(result.content.contains("type e,"), "theirs addition must be present");
assert!(result.content.contains("} from \"./foo\""), "closing must be present");
}
#[test]
fn test_ts_multiline_import_no_entities() {
let base = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
} from \"./foo\"
";
let ours = base;
let theirs = "\
import {
type Foo,
type a,
type b,
type c,
} from \"./foo\"
";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS import no entities: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(result.content.contains("import {"), "import {{ must not be dropped");
assert!(result.content.contains("type Foo,"), "type Foo must be present");
}
#[test]
fn test_ts_multiline_import_export_variable() {
let base = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
} from \"./foo\"
export const X = 1;
export function bar() {
return 1;
}
";
let ours = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
type d,
} from \"./foo\"
export const X = 1;
export function bar() {
return 1;
}
";
let theirs = "\
import {
type Foo,
type a,
type b,
type c,
} from \"./foo\"
export const X = 2;
export function bar() {
return 1;
}
";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS import + export var: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(result.content.contains("import {"), "import {{ must not be dropped");
}
#[test]
fn test_ts_multiline_import_adjacent_to_entity() {
let base = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let ours = base;
let theirs = "\
import {
type Foo,
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS import adjacent: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(result.content.contains("import {"), "import {{ must not be dropped");
assert!(result.content.contains("type Foo,"), "type Foo must be present");
}
#[test]
fn test_ts_multiline_import_both_consolidate_differently() {
let base = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
} from \"./foo\"
export function bar() {
return 1;
}
";
let ours = "\
import {
type Foo,
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let theirs = "\
import {
type Foo,
type a,
type b,
type d,
} from \"./foo\"
export function bar() {
return 1;
}
";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS both consolidate: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(result.content.contains("import {"), "import {{ must not be dropped");
assert!(result.content.contains("type Foo,"), "type Foo must be present");
assert!(result.content.contains("} from \"./foo\""), "closing must be present");
}
#[test]
fn test_ts_multiline_import_ours_adds_theirs_consolidates() {
let base = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let ours = "\
import type { Foo } from \"./foo\"
import {
type a,
type b,
type c,
type d,
} from \"./foo\"
export function bar() {
return 1;
}
";
let theirs = "\
import {
type Foo,
type a,
type b,
type c,
} from \"./foo\"
export function bar() {
return 1;
}
";
let result = entity_merge(base, ours, theirs, "test.ts");
eprintln!("TS import ours-adds theirs-consolidates: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
eprintln!("Content:\n{}", result.content);
assert!(result.content.contains("import {"), "import {{ must not be dropped");
assert!(result.content.contains("type d,"), "ours addition must be present");
assert!(result.content.contains("} from \"./foo\""), "closing must be present");
}
#[test]
fn test_rename_plus_modify_auto_resolves() {
let base = r#"export const cubeQueryExecutorTool = tool({
name: "cubeQueryExecutorTool",
description: "Execute a cube query",
schema: z.object({ query: z.string() }),
execute: async (input) => {
return await runQuery(input.query);
},
});
"#;
let ours = r#"export const cubeQueryTool = tool({
name: "cubeQueryTool",
description: "Execute a cube query",
schema: z.object({ query: z.string() }),
execute: async (input) => {
return await runQuery(input.query);
},
});
"#;
let theirs = r#"export const cubeQueryExecutorTool = tool({
name: "cubeQueryExecutorTool",
description: "Execute a cube query with unit inference",
schema: z.object({ query: z.string(), unit: z.string().optional() }),
execute: async (input) => {
const unit = input.unit || inferUnit(input.query);
return await runQuery(input.query, unit);
},
});
"#;
let result = entity_merge(base, ours, theirs, "cubeQueryTool.ts");
assert_eq!(result.conflicts.len(), 1, "Should have exactly one conflict");
assert!(
matches!(result.conflicts[0].kind, ConflictKind::RenameModify { renamed_in_ours: true, .. }),
"Should be a RenameModify conflict, got: {:?}",
result.conflicts[0].kind
);
assert!(result.content.contains("cubeQueryTool"), "Ours (renamed) should be in conflict markers");
assert!(result.content.contains("unit inference"), "Theirs (modified) should be in conflict markers");
}
}