use std::collections::{BTreeMap, BTreeSet};
use crate::error::Result;
use super::model::{Key, Tombstone, TreeValue};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ConflictReason {
BothChanged,
ConcurrentAdd,
LocalModifiedRemoteRemoved,
RemoteModifiedLocalRemoved,
NoCommonAncestorLocalWins,
}
impl ConflictReason {
pub fn as_str(&self) -> &'static str {
match self {
ConflictReason::BothChanged => "both-changed",
ConflictReason::ConcurrentAdd => "concurrent-add",
ConflictReason::LocalModifiedRemoteRemoved => "local-modified-remote-removed",
ConflictReason::RemoteModifiedLocalRemoved => "remote-modified-local-removed",
ConflictReason::NoCommonAncestorLocalWins => "no-common-ancestor-local-wins",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ConflictResolution {
Local,
Remote,
Union,
}
impl ConflictResolution {
pub fn as_str(&self) -> &'static str {
match self {
ConflictResolution::Local => "local",
ConflictResolution::Remote => "remote",
ConflictResolution::Union => "union",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConflictDecision {
pub key: Key,
pub reason: ConflictReason,
pub resolution: ConflictResolution,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum MergeState {
Absent,
Value(TreeValue),
Tombstone(Tombstone),
}
pub fn three_way_merge(
base: &BTreeMap<Key, TreeValue>,
local: &BTreeMap<Key, TreeValue>,
remote: &BTreeMap<Key, TreeValue>,
local_timestamp: i64,
remote_timestamp: i64,
) -> Result<(BTreeMap<Key, TreeValue>, Vec<ConflictDecision>)> {
let mut merged = BTreeMap::new();
let mut conflicts = Vec::new();
let mut all_keys: BTreeMap<&Key, ()> = BTreeMap::new();
for k in base.keys().chain(local.keys()).chain(remote.keys()) {
all_keys.insert(k, ());
}
for key in all_keys.keys() {
let in_base = base.get(*key);
let in_local = local.get(*key);
let in_remote = remote.get(*key);
match (in_base, in_local, in_remote) {
(Some(b), Some(l), Some(r)) => {
let local_changed = l != b;
let remote_changed = r != b;
match (local_changed, remote_changed) {
(false, false) => {
merged.insert((*key).clone(), b.clone());
}
(true, false) => {
merged.insert((*key).clone(), l.clone());
}
(false, true) => {
merged.insert((*key).clone(), r.clone());
}
(true, true) => {
let (resolved, resolution) =
resolve_conflict(l, r, local_timestamp, remote_timestamp);
merged.insert((*key).clone(), resolved);
conflicts.push(ConflictDecision {
key: (*key).clone(),
reason: ConflictReason::BothChanged,
resolution,
});
}
}
}
(Some(b), Some(l), None) => {
if l != b {
merged.insert((*key).clone(), l.clone());
conflicts.push(ConflictDecision {
key: (*key).clone(),
reason: ConflictReason::LocalModifiedRemoteRemoved,
resolution: ConflictResolution::Local,
});
}
}
(Some(b), None, Some(r)) => {
if r != b {
merged.insert((*key).clone(), r.clone());
conflicts.push(ConflictDecision {
key: (*key).clone(),
reason: ConflictReason::RemoteModifiedLocalRemoved,
resolution: ConflictResolution::Remote,
});
}
}
(Some(_), None, None) => {
}
(None, Some(l), None) => {
merged.insert((*key).clone(), l.clone());
}
(None, None, Some(r)) => {
merged.insert((*key).clone(), r.clone());
}
(None, Some(l), Some(r)) => {
let (resolved, resolution) =
resolve_conflict(l, r, local_timestamp, remote_timestamp);
merged.insert((*key).clone(), resolved);
conflicts.push(ConflictDecision {
key: (*key).clone(),
reason: ConflictReason::ConcurrentAdd,
resolution,
});
}
(None, None, None) => {}
}
}
Ok((merged, conflicts))
}
pub fn two_way_merge_no_common_ancestor(
local_values: &BTreeMap<Key, TreeValue>,
local_tombstones: &BTreeMap<Key, Tombstone>,
remote_values: &BTreeMap<Key, TreeValue>,
remote_tombstones: &BTreeMap<Key, Tombstone>,
) -> (
BTreeMap<Key, TreeValue>,
BTreeMap<Key, Tombstone>,
Vec<ConflictDecision>,
) {
let mut merged_values = BTreeMap::new();
let mut merged_tombstones = BTreeMap::new();
let mut conflicts = Vec::new();
let mut all_keys: BTreeSet<Key> = BTreeSet::new();
for key in local_values
.keys()
.chain(local_tombstones.keys())
.chain(remote_values.keys())
.chain(remote_tombstones.keys())
{
all_keys.insert(key.clone());
}
for key in all_keys {
let local_state = if let Some(v) = local_values.get(&key) {
MergeState::Value(v.clone())
} else if let Some(t) = local_tombstones.get(&key) {
MergeState::Tombstone(t.clone())
} else {
MergeState::Absent
};
let remote_state = if let Some(v) = remote_values.get(&key) {
MergeState::Value(v.clone())
} else if let Some(t) = remote_tombstones.get(&key) {
MergeState::Tombstone(t.clone())
} else {
MergeState::Absent
};
if local_state != MergeState::Absent
&& remote_state != MergeState::Absent
&& local_state != remote_state
{
conflicts.push(ConflictDecision {
key: key.clone(),
reason: ConflictReason::NoCommonAncestorLocalWins,
resolution: ConflictResolution::Local,
});
}
let selected = if local_state != MergeState::Absent {
local_state
} else {
remote_state
};
match selected {
MergeState::Absent => {}
MergeState::Value(v) => {
merged_values.insert(key, v);
}
MergeState::Tombstone(t) => {
merged_tombstones.insert(key, t);
}
}
}
(merged_values, merged_tombstones, conflicts)
}
pub fn merge_tombstones(
base: &BTreeMap<Key, Tombstone>,
local: &BTreeMap<Key, Tombstone>,
remote: &BTreeMap<Key, Tombstone>,
merged_values: &BTreeMap<Key, TreeValue>,
) -> BTreeMap<Key, Tombstone> {
let mut merged = BTreeMap::new();
let mut all_keys: BTreeMap<&Key, ()> = BTreeMap::new();
for k in base.keys().chain(local.keys()).chain(remote.keys()) {
all_keys.insert(k, ());
}
for key in all_keys.keys() {
let in_base = base.get(*key);
let in_local = local.get(*key);
let in_remote = remote.get(*key);
let selected = match (in_base, in_local, in_remote) {
(Some(b), Some(l), Some(r)) => {
let local_changed = l != b;
let remote_changed = r != b;
match (local_changed, remote_changed) {
(false, false) => Some(b.clone()),
(true, false) => Some(l.clone()),
(false, true) => Some(r.clone()),
(true, true) => Some(select_preferred_tombstone(l, r)),
}
}
(Some(b), Some(l), None) => {
if l != b {
Some(l.clone())
} else {
None
}
}
(Some(b), None, Some(r)) => {
if r != b {
Some(r.clone())
} else {
None
}
}
(Some(_), None, None) => None,
(None, Some(l), None) => Some(l.clone()),
(None, None, Some(r)) => Some(r.clone()),
(None, Some(l), Some(r)) => Some(select_preferred_tombstone(l, r)),
(None, None, None) => None,
};
if let Some(tombstone) = selected {
if !merged_values.contains_key(*key) {
merged.insert((*key).clone(), tombstone);
}
}
}
merged
}
fn select_preferred_tombstone(local: &Tombstone, _remote: &Tombstone) -> Tombstone {
local.clone()
}
pub fn merge_set_member_tombstones(
local: &BTreeMap<(Key, String), String>,
remote: &BTreeMap<(Key, String), String>,
merged_values: &BTreeMap<Key, TreeValue>,
) -> BTreeMap<(Key, String), String> {
let mut merged = remote.clone();
for (key, value) in local {
merged.insert(key.clone(), value.clone());
}
merged.retain(|(key, member_id), _| match merged_values.get(key) {
Some(TreeValue::Set(set)) => !set.contains_key(member_id),
_ => true,
});
merged
}
pub fn merge_list_tombstones(
local: &BTreeMap<(Key, String), Tombstone>,
remote: &BTreeMap<(Key, String), Tombstone>,
merged_values: &BTreeMap<Key, TreeValue>,
) -> BTreeMap<(Key, String), Tombstone> {
let mut merged = remote.clone();
for (key, entry) in local {
merged
.entry(key.clone())
.and_modify(|existing| {
if entry.timestamp > existing.timestamp {
*existing = entry.clone();
}
})
.or_insert_with(|| entry.clone());
}
merged.retain(|(key, entry_name), _| match merged_values.get(key) {
Some(TreeValue::List(list)) => !list.iter().any(|(name, _)| name == entry_name),
_ => true,
});
merged
}
fn resolve_conflict(
local: &TreeValue,
remote: &TreeValue,
_local_timestamp: i64,
_remote_timestamp: i64,
) -> (TreeValue, ConflictResolution) {
match (local, remote) {
(TreeValue::List(local_list), TreeValue::List(remote_list)) => {
let mut combined: BTreeMap<String, String> = BTreeMap::new();
for (name, content) in local_list {
combined.insert(name.clone(), content.clone());
}
for (name, content) in remote_list {
combined
.entry(name.clone())
.or_insert_with(|| content.clone());
}
(
TreeValue::List(combined.into_iter().collect()),
ConflictResolution::Union,
)
}
(TreeValue::Set(local_set), TreeValue::Set(remote_set)) => {
let mut combined = remote_set.clone();
for (member_id, content) in local_set {
combined.insert(member_id.clone(), content.clone());
}
(TreeValue::Set(combined), ConflictResolution::Union)
}
(TreeValue::String(_), TreeValue::String(_)) => (local.clone(), ConflictResolution::Local),
_ => (local.clone(), ConflictResolution::Local),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn key(name: &str) -> Key {
Key {
target_type: crate::types::TargetType::Commit,
target_value: "abc123".to_string(),
key: name.to_string(),
}
}
fn string_value(value: &str) -> TreeValue {
TreeValue::String(value.to_string())
}
#[test]
fn test_three_way_merge_reports_concurrent_add_conflict() {
let mut local = BTreeMap::new();
local.insert(key("agent:model"), string_value("local"));
let mut remote = BTreeMap::new();
remote.insert(key("agent:model"), string_value("remote"));
let (merged, conflicts) = three_way_merge(&BTreeMap::new(), &local, &remote, 100, 200)
.expect("merge should succeed");
assert_eq!(
merged.get(&key("agent:model")),
Some(&string_value("local"))
);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].reason, ConflictReason::ConcurrentAdd);
assert_eq!(conflicts[0].resolution, ConflictResolution::Local);
}
#[test]
fn test_three_way_merge_reports_local_modified_remote_removed() {
let mut base = BTreeMap::new();
base.insert(key("agent:model"), string_value("base"));
let mut local = BTreeMap::new();
local.insert(key("agent:model"), string_value("local"));
let (merged, conflicts) = three_way_merge(&base, &local, &BTreeMap::new(), 100, 200)
.expect("merge should succeed");
assert_eq!(
merged.get(&key("agent:model")),
Some(&string_value("local"))
);
assert_eq!(conflicts.len(), 1);
assert_eq!(
conflicts[0].reason,
ConflictReason::LocalModifiedRemoteRemoved
);
assert_eq!(conflicts[0].resolution, ConflictResolution::Local);
}
#[test]
fn test_three_way_merge_reports_remote_modified_local_removed() {
let mut base = BTreeMap::new();
base.insert(key("agent:model"), string_value("base"));
let mut remote = BTreeMap::new();
remote.insert(key("agent:model"), string_value("remote"));
let (merged, conflicts) = three_way_merge(&base, &BTreeMap::new(), &remote, 100, 200)
.expect("merge should succeed");
assert_eq!(
merged.get(&key("agent:model")),
Some(&string_value("remote"))
);
assert_eq!(conflicts.len(), 1);
assert_eq!(
conflicts[0].reason,
ConflictReason::RemoteModifiedLocalRemoved
);
assert_eq!(conflicts[0].resolution, ConflictResolution::Remote);
}
#[test]
fn test_two_way_merge_no_common_ancestor_local_wins_value_conflict() {
let mut local_values = BTreeMap::new();
local_values.insert(key("agent:model"), string_value("local"));
local_values.insert(key("local:only"), string_value("keep-local"));
let mut remote_values = BTreeMap::new();
remote_values.insert(key("agent:model"), string_value("remote"));
remote_values.insert(key("remote:only"), string_value("keep-remote"));
let (merged_values, merged_tombstones, conflicts) = two_way_merge_no_common_ancestor(
&local_values,
&BTreeMap::new(),
&remote_values,
&BTreeMap::new(),
);
assert!(merged_tombstones.is_empty());
assert_eq!(
merged_values.get(&key("agent:model")),
Some(&string_value("local"))
);
assert_eq!(
merged_values.get(&key("local:only")),
Some(&string_value("keep-local"))
);
assert_eq!(
merged_values.get(&key("remote:only")),
Some(&string_value("keep-remote"))
);
assert_eq!(conflicts.len(), 1);
assert_eq!(
conflicts[0].reason,
ConflictReason::NoCommonAncestorLocalWins
);
assert_eq!(conflicts[0].resolution, ConflictResolution::Local);
}
}