use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::diff::ObjectResolver;
use crate::object::{Node, Object, ObjectId};
#[derive(Debug, Clone)]
pub enum MergeResult {
Success(Object),
Conflicts {
partial: Object,
conflicts: Vec<Conflict>,
},
FastForward(ObjectId),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Conflict {
pub path: String,
pub ours: Option<ConflictValue>,
pub theirs: Option<ConflictValue>,
pub base: Option<ConflictValue>,
pub suggested_resolution: Option<ConflictValue>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConflictValue {
Null,
Bool(bool),
Int(i64),
UInt(u64),
Float(f64),
String(String),
Complex(String),
}
impl ConflictValue {
pub fn from_object(obj: &Object) -> Self {
match obj {
Object::Atom(a) => match a {
crate::object::Atom::Null => ConflictValue::Null,
crate::object::Atom::Bool(b) => ConflictValue::Bool(*b),
crate::object::Atom::Int(i) => ConflictValue::Int(*i),
crate::object::Atom::UInt(u) => ConflictValue::UInt(*u),
crate::object::Atom::Float(f) => ConflictValue::Float(*f),
crate::object::Atom::String(s) => ConflictValue::String(s.clone()),
crate::object::Atom::Bytes(_) => ConflictValue::String("[bytes]".to_string()),
},
Object::Node(n) => match n {
Node::Map(m) => ConflictValue::Complex(format!("{{map: {} keys}}", m.len())),
Node::List(l) => ConflictValue::Complex(format!("[list: {} items]", l.len())),
Node::Set(s) => ConflictValue::Complex(format!("{{set: {} items}}", s.len())),
},
}
}
}
pub fn three_way_merge(
resolver: &dyn ObjectResolver,
base: &ObjectId,
ours: &ObjectId,
theirs: &ObjectId,
) -> MergeResult {
three_way_merge_collect(resolver, base, ours, theirs).0
}
pub fn three_way_merge_collect(
resolver: &dyn ObjectResolver,
base: &ObjectId,
ours: &ObjectId,
theirs: &ObjectId,
) -> (MergeResult, Vec<Object>) {
if base == ours {
return (MergeResult::FastForward(*theirs), Vec::new());
}
if base == theirs {
return (MergeResult::FastForward(*ours), Vec::new());
}
if ours == theirs {
return (MergeResult::FastForward(*ours), Vec::new());
}
let base_obj = match resolver.resolve(base) {
Some(obj) => obj,
None => return (MergeResult::FastForward(*theirs), Vec::new()),
};
let ours_obj = match resolver.resolve(ours) {
Some(obj) => obj,
None => return (MergeResult::FastForward(*theirs), Vec::new()),
};
let theirs_obj = match resolver.resolve(theirs) {
Some(obj) => obj,
None => return (MergeResult::FastForward(*ours), Vec::new()),
};
let path = String::from("/");
let mut conflicts = Vec::new();
let mut created = Vec::new();
let merged = merge_objects(
resolver,
&path,
&base_obj,
&ours_obj,
&theirs_obj,
&mut conflicts,
&mut created,
);
let result = if conflicts.is_empty() {
MergeResult::Success(merged)
} else {
MergeResult::Conflicts {
partial: merged,
conflicts,
}
};
(result, created)
}
pub fn try_three_way_merge_collect(
resolver: &dyn ObjectResolver,
base: &ObjectId,
ours: &ObjectId,
theirs: &ObjectId,
) -> Result<(MergeResult, Vec<Object>), ObjectId> {
let tracking = TrackingResolver {
inner: resolver,
missing: std::cell::Cell::new(None),
};
let out = three_way_merge_collect(&tracking, base, ours, theirs);
match tracking.missing.get() {
Some(id) => Err(id),
None => Ok(out),
}
}
struct TrackingResolver<'a> {
inner: &'a dyn ObjectResolver,
missing: std::cell::Cell<Option<ObjectId>>,
}
impl ObjectResolver for TrackingResolver<'_> {
fn resolve(&self, id: &ObjectId) -> Option<Object> {
let found = self.inner.resolve(id);
if found.is_none() && self.missing.get().is_none() {
self.missing.set(Some(*id));
}
found
}
}
fn merge_objects(
resolver: &dyn ObjectResolver,
path: &str,
base: &Object,
ours: &Object,
theirs: &Object,
conflicts: &mut Vec<Conflict>,
created: &mut Vec<Object>,
) -> Object {
if ours == theirs {
return ours.clone();
}
if base == ours {
return theirs.clone();
}
if base == theirs {
return ours.clone();
}
match (base, ours, theirs) {
(
Object::Node(Node::Map(base_entries)),
Object::Node(Node::Map(our_entries)),
Object::Node(Node::Map(their_entries)),
) => merge_maps(
resolver,
path,
base_entries,
our_entries,
their_entries,
conflicts,
created,
),
(
Object::Node(Node::List(base_items)),
Object::Node(Node::List(our_items)),
Object::Node(Node::List(their_items)),
) => merge_lists(
resolver,
path,
base_items,
our_items,
their_items,
conflicts,
created,
),
(
Object::Node(Node::Set(_base_items)),
Object::Node(Node::Set(our_items)),
Object::Node(Node::Set(their_items)),
) => merge_sets(our_items, their_items, created),
(Object::Atom(_), Object::Atom(_), Object::Atom(_)) => {
conflicts.push(Conflict {
path: path.to_string(),
ours: Some(ConflictValue::from_object(ours)),
theirs: Some(ConflictValue::from_object(theirs)),
base: Some(ConflictValue::from_object(base)),
suggested_resolution: None,
});
ours.clone()
}
_ => {
conflicts.push(Conflict {
path: path.to_string(),
ours: Some(ConflictValue::from_object(ours)),
theirs: Some(ConflictValue::from_object(theirs)),
base: Some(ConflictValue::from_object(base)),
suggested_resolution: None,
});
ours.clone()
}
}
}
fn merge_maps(
resolver: &dyn ObjectResolver,
path: &str,
base_entries: &BTreeMap<String, ObjectId>,
our_entries: &BTreeMap<String, ObjectId>,
their_entries: &BTreeMap<String, ObjectId>,
conflicts: &mut Vec<Conflict>,
created: &mut Vec<Object>,
) -> Object {
let mut merged = BTreeMap::new();
let mut all_keys: std::collections::BTreeSet<&String> = std::collections::BTreeSet::new();
all_keys.extend(base_entries.keys());
all_keys.extend(our_entries.keys());
all_keys.extend(their_entries.keys());
for key in all_keys {
let base_id = base_entries.get(key);
let our_id = our_entries.get(key);
let their_id = their_entries.get(key);
let child_path = format!("{}{}{}", path, if path == "/" { "" } else { "/" }, key);
match (base_id, our_id, their_id) {
(Some(b), Some(o), Some(t)) => {
if o == t {
merged.insert(key.clone(), *o);
} else if b == o {
merged.insert(key.clone(), *t);
} else if b == t {
merged.insert(key.clone(), *o);
} else {
let base_obj = resolver.resolve(b);
let our_obj = resolver.resolve(o);
let their_obj = resolver.resolve(t);
match (base_obj, our_obj, their_obj) {
(Some(bo), Some(oo), Some(to)) => {
let merged_child = merge_objects(
resolver,
&child_path,
&bo,
&oo,
&to,
conflicts,
created,
);
let merged_id = merged_child.id();
merged.insert(key.clone(), merged_id);
}
_ => {
if let Some(o) = our_id {
merged.insert(key.clone(), *o);
}
conflicts.push(Conflict {
path: child_path,
ours: None,
theirs: None,
base: None,
suggested_resolution: None,
});
}
}
}
}
(None, Some(o), None) => {
merged.insert(key.clone(), *o);
}
(None, None, Some(t)) => {
merged.insert(key.clone(), *t);
}
(None, Some(o), Some(t)) => {
let both_maps = || match (resolver.resolve(o), resolver.resolve(t)) {
(
Some(oo @ Object::Node(Node::Map(_))),
Some(to @ Object::Node(Node::Map(_))),
) => Some((oo, to)),
_ => None,
};
if o == t {
merged.insert(key.clone(), *o);
} else if let Some((oo, to)) = both_maps() {
let merged_child = merge_objects(
resolver,
&child_path,
&Object::empty_map(),
&oo,
&to,
conflicts,
created,
);
merged.insert(key.clone(), merged_child.id());
} else {
conflicts.push(Conflict {
path: child_path,
ours: resolver
.resolve(o)
.map(|obj| ConflictValue::from_object(&obj)),
theirs: resolver
.resolve(t)
.map(|obj| ConflictValue::from_object(&obj)),
base: None,
suggested_resolution: None,
});
merged.insert(key.clone(), *o); }
}
(Some(_), None, Some(t)) => {
if base_id == Some(t) {
} else {
conflicts.push(Conflict {
path: child_path,
ours: None, theirs: resolver
.resolve(t)
.map(|obj| ConflictValue::from_object(&obj)),
base: base_id
.and_then(|b| resolver.resolve(b))
.map(|obj| ConflictValue::from_object(&obj)),
suggested_resolution: None,
});
}
}
(Some(_), Some(o), None) => {
if base_id == Some(o) {
} else {
conflicts.push(Conflict {
path: child_path,
ours: resolver
.resolve(o)
.map(|obj| ConflictValue::from_object(&obj)),
theirs: None, base: base_id
.and_then(|b| resolver.resolve(b))
.map(|obj| ConflictValue::from_object(&obj)),
suggested_resolution: None,
});
merged.insert(key.clone(), *o); }
}
(Some(_), None, None) => {
}
(None, None, None) => {}
}
}
let obj = Object::map(merged);
created.push(obj.clone());
obj
}
fn merge_lists(
_resolver: &dyn ObjectResolver,
path: &str,
base_items: &[ObjectId],
our_items: &[ObjectId],
their_items: &[ObjectId],
conflicts: &mut Vec<Conflict>,
created: &mut Vec<Object>,
) -> Object {
if our_items == their_items {
let obj = Object::list(our_items.to_vec());
created.push(obj.clone());
return obj;
}
conflicts.push(Conflict {
path: path.to_string(),
ours: Some(ConflictValue::Complex(format!(
"[list: {} items]",
our_items.len()
))),
theirs: Some(ConflictValue::Complex(format!(
"[list: {} items]",
their_items.len()
))),
base: Some(ConflictValue::Complex(format!(
"[list: {} items]",
base_items.len()
))),
suggested_resolution: None,
});
let obj = Object::list(our_items.to_vec());
created.push(obj.clone());
obj
}
fn merge_sets(
our_items: &[ObjectId],
their_items: &[ObjectId],
created: &mut Vec<Object>,
) -> Object {
let mut combined: std::collections::BTreeSet<ObjectId> = std::collections::BTreeSet::new();
combined.extend(our_items.iter().copied());
combined.extend(their_items.iter().copied());
let obj = Object::set(combined.into_iter().collect());
created.push(obj.clone());
obj
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct TestResolver {
objects: HashMap<ObjectId, Object>,
}
impl TestResolver {
fn new() -> Self {
Self {
objects: HashMap::new(),
}
}
fn store(&mut self, obj: &Object) -> ObjectId {
let id = obj.id();
self.objects.insert(id, obj.clone());
id
}
fn store_json(&mut self, value: &serde_json::Value) -> ObjectId {
self.store_json_inner(value)
}
fn store_json_inner(&mut self, value: &serde_json::Value) -> ObjectId {
let obj = match value {
serde_json::Value::Null => Object::null(),
serde_json::Value::Bool(b) => Object::bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Object::int(i)
} else {
Object::float(n.as_f64().unwrap())
}
}
serde_json::Value::String(s) => Object::string(s.clone()),
serde_json::Value::Array(arr) => {
let ids: Vec<ObjectId> = arr.iter().map(|v| self.store_json_inner(v)).collect();
Object::list(ids)
}
serde_json::Value::Object(map) => {
let mut entries = BTreeMap::new();
for (k, v) in map {
entries.insert(k.clone(), self.store_json_inner(v));
}
Object::map(entries)
}
};
self.store(&obj)
}
}
impl ObjectResolver for TestResolver {
fn resolve(&self, id: &ObjectId) -> Option<Object> {
self.objects.get(id).cloned()
}
}
fn to_json(r: &mut TestResolver, obj: &Object, created: &[Object]) -> serde_json::Value {
for c in created {
r.store(c);
}
fn walk(r: &TestResolver, obj: &Object) -> serde_json::Value {
match obj {
Object::Node(Node::Map(m)) => serde_json::Value::Object(
m.iter()
.map(|(k, id)| (k.clone(), walk(r, &r.resolve(id).unwrap())))
.collect(),
),
Object::Atom(crate::object::Atom::Int(i)) => serde_json::json!(i),
Object::Atom(crate::object::Atom::String(s)) => serde_json::json!(s),
other => panic!("unexpected {other:?}"),
}
}
walk(r, obj)
}
#[test]
fn subtree_created_on_both_sides_is_unioned_not_conflicted() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"meta": 1}));
let ours =
r.store_json(&serde_json::json!({"meta": 1, "asd": {"v1": {"index": {"q": "s"}}}}));
let theirs =
r.store_json(&serde_json::json!({"meta": 1, "asd": {"v1": {"ledger": {"s": "e"}}}}));
let (result, created) = three_way_merge_collect(&r, &base, &ours, &theirs);
let MergeResult::Success(merged) = result else {
panic!("expected a clean merge, got {result:?}");
};
assert_eq!(
to_json(&mut r, &merged, &created),
serde_json::json!({"meta": 1, "asd": {"v1": {"index": {"q": "s"}, "ledger": {"s": "e"}}}})
);
}
#[test]
fn subtree_created_on_both_sides_still_conflicts_on_a_clashing_leaf() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({}));
let ours = r.store_json(&serde_json::json!({"cfg": {"mode": "a", "x": 1}}));
let theirs = r.store_json(&serde_json::json!({"cfg": {"mode": "b", "y": 2}}));
let (result, created) = three_way_merge_collect(&r, &base, &ours, &theirs);
let MergeResult::Conflicts { partial, conflicts } = result else {
panic!("expected a conflict on /cfg/mode, got {result:?}");
};
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].path, "/cfg/mode");
assert_eq!(
to_json(&mut r, &partial, &created),
serde_json::json!({"cfg": {"mode": "a", "x": 1, "y": 2}})
);
}
#[test]
fn unresolvable_base_is_an_error_not_a_fast_forward() {
let mut r = TestResolver::new();
let ours = r.store_json(&serde_json::json!({"a": 1, "mine": 1}));
let theirs = r.store_json(&serde_json::json!({"a": 1, "theirs": 1}));
let base = TestResolver::new().store_json(&serde_json::json!({"a": 1}));
assert!(matches!(
three_way_merge_collect(&r, &base, &ours, &theirs).0,
MergeResult::FastForward(id) if id == theirs
));
assert_eq!(
try_three_way_merge_collect(&r, &base, &ours, &theirs).unwrap_err(),
base
);
}
#[test]
fn test_fast_forward_base_equals_ours() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1}));
let theirs = r.store_json(&serde_json::json!({"a": 2}));
match three_way_merge(&r, &base, &base, &theirs) {
MergeResult::FastForward(id) => assert_eq!(id, theirs),
_ => panic!("expected fast-forward"),
}
}
#[test]
fn test_no_conflict_different_keys() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1, "b": 2}));
let ours = r.store_json(&serde_json::json!({"a": 10, "b": 2})); let theirs = r.store_json(&serde_json::json!({"a": 1, "b": 20}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(merged) => {
if let Object::Node(Node::Map(entries)) = &merged {
let a = r.objects.get(entries.get("a").unwrap());
let b = r.objects.get(entries.get("b").unwrap());
assert_eq!(a, Some(&Object::int(10)));
assert_eq!(b, Some(&Object::int(20)));
} else {
panic!("expected map");
}
}
MergeResult::Conflicts { conflicts, .. } => {
panic!("unexpected conflicts: {:?}", conflicts);
}
_ => panic!("expected success"),
}
}
#[test]
fn test_conflict_same_key_different_values() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"x": 1}));
let ours = r.store_json(&serde_json::json!({"x": 2}));
let theirs = r.store_json(&serde_json::json!({"x": 3}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, .. } => {
assert_eq!(conflicts.len(), 1);
assert!(conflicts[0].path.contains("x"));
}
_ => panic!("expected conflict"),
}
}
#[test]
fn test_both_add_same_key_same_value() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1}));
let ours = r.store_json(&serde_json::json!({"a": 1, "b": 2}));
let theirs = r.store_json(&serde_json::json!({"a": 1, "b": 2}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(_) | MergeResult::FastForward(_) => {} MergeResult::Conflicts { conflicts, .. } => {
panic!("unexpected conflicts: {:?}", conflicts);
}
}
}
#[test]
fn test_both_add_same_key_different_value() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1}));
let ours = r.store_json(&serde_json::json!({"a": 1, "b": 2}));
let theirs = r.store_json(&serde_json::json!({"a": 1, "b": 3}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, .. } => {
assert!(!conflicts.is_empty());
}
_ => panic!("expected conflict"),
}
}
#[test]
fn test_one_deletes_other_modifies() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1, "b": 2}));
let ours = r.store_json(&serde_json::json!({"a": 1})); let theirs = r.store_json(&serde_json::json!({"a": 1, "b": 99}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, .. } => {
assert!(!conflicts.is_empty(), "delete-vs-modify should conflict");
}
_ => panic!("expected conflict for delete-vs-modify"),
}
}
#[test]
fn test_nested_merge_no_conflict() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({
"config": {"network": {"subnet": "10.0.0.0/24"}, "dns": "8.8.8.8"}
}));
let ours = r.store_json(&serde_json::json!({
"config": {"network": {"subnet": "192.168.0.0/16"}, "dns": "8.8.8.8"}
}));
let theirs = r.store_json(&serde_json::json!({
"config": {"network": {"subnet": "10.0.0.0/24"}, "dns": "1.1.1.1"}
}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(_merged) => {
}
MergeResult::Conflicts { conflicts, .. } => {
panic!("unexpected conflicts: {:?}", conflicts);
}
_ => panic!("expected success"),
}
}
#[test]
fn test_both_sides_identical_changes() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"x": 1}));
let ours = r.store_json(&serde_json::json!({"x": 5}));
let theirs = r.store_json(&serde_json::json!({"x": 5}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(_) | MergeResult::FastForward(_) => {} MergeResult::Conflicts { .. } => panic!("identical changes should not conflict"),
}
}
#[test]
fn test_set_union_no_conflict() {
let mut r = TestResolver::new();
let a = r.store(&Object::int(1));
let b = r.store(&Object::int(2));
let c = r.store(&Object::int(3));
let base = r.store(&Object::set(vec![a]));
let ours = r.store(&Object::set(vec![a, b]));
let theirs = r.store(&Object::set(vec![a, c]));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(merged) => {
if let Object::Node(Node::Set(items)) = merged {
assert_eq!(
items.len(),
3,
"union of {{1,2}} and {{1,3}} should have 3 items"
);
assert!(items.contains(&a));
assert!(items.contains(&b));
assert!(items.contains(&c));
} else {
panic!("expected set");
}
}
MergeResult::Conflicts { conflicts, .. } => {
panic!("sets never conflict, got: {:?}", conflicts);
}
MergeResult::FastForward(_) => panic!("expected success"),
}
}
#[test]
fn test_set_disjoint_union() {
let mut r = TestResolver::new();
let a = r.store(&Object::int(1));
let b = r.store(&Object::int(2));
let c = r.store(&Object::int(3));
let base = r.store(&Object::set(vec![]));
let ours = r.store(&Object::set(vec![a, b]));
let theirs = r.store(&Object::set(vec![c]));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(merged) => {
if let Object::Node(Node::Set(items)) = merged {
assert_eq!(items.len(), 3);
} else {
panic!("expected set");
}
}
MergeResult::Conflicts { conflicts, .. } => {
panic!("sets never conflict: {:?}", conflicts);
}
MergeResult::FastForward(_) => panic!("expected success"),
}
}
#[test]
fn test_list_conflict_both_sides_modify() {
let mut r = TestResolver::new();
let x = r.store(&Object::int(10));
let y = r.store(&Object::int(20));
let z = r.store(&Object::int(30));
let w = r.store(&Object::int(40));
let base = r.store(&Object::list(vec![x, y]));
let ours = r.store(&Object::list(vec![x, z])); let theirs = r.store(&Object::list(vec![x, w]));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, .. } => {
assert_eq!(conflicts.len(), 1);
assert!(matches!(
&conflicts[0].ours,
Some(ConflictValue::Complex(s)) if s.contains("list")
));
assert!(matches!(
&conflicts[0].theirs,
Some(ConflictValue::Complex(s)) if s.contains("list")
));
assert!(matches!(
&conflicts[0].base,
Some(ConflictValue::Complex(s)) if s.contains("list")
));
}
_ => panic!("expected list conflict"),
}
}
#[test]
fn test_list_same_after_change_no_conflict() {
let mut r = TestResolver::new();
let x = r.store(&Object::int(10));
let y = r.store(&Object::int(20));
let base = r.store(&Object::list(vec![x]));
let ours = r.store(&Object::list(vec![x, y]));
let theirs = r.store(&Object::list(vec![x, y]));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(_) | MergeResult::FastForward(_) => {}
MergeResult::Conflicts { conflicts, .. } => {
panic!(
"identical list changes should not conflict: {:?}",
conflicts
);
}
}
}
#[test]
fn test_both_sides_delete_same_key() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1, "b": 2, "c": 3}));
let ours = r.store_json(&serde_json::json!({"a": 1, "c": 3})); let theirs = r.store_json(&serde_json::json!({"a": 1, "c": 3}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(merged) => {
if let Object::Node(Node::Map(entries)) = merged {
assert!(!entries.contains_key("b"), "b should have been deleted");
assert!(entries.contains_key("a"));
assert!(entries.contains_key("c"));
} else {
panic!("expected map");
}
}
MergeResult::FastForward(id) => {
assert!(id == ours || id == theirs);
}
MergeResult::Conflicts { conflicts, .. } => {
panic!("both-sides-delete should not conflict: {:?}", conflicts);
}
}
}
#[test]
fn test_one_side_deletes_unchanged_key() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1, "b": 2}));
let ours = r.store_json(&serde_json::json!({"a": 1})); let theirs = r.store_json(&serde_json::json!({"a": 1, "b": 2}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::FastForward(id) => {
assert_eq!(id, ours);
}
MergeResult::Success(merged) => {
if let Object::Node(Node::Map(entries)) = merged {
assert!(!entries.contains_key("b"));
} else {
panic!("expected map");
}
}
MergeResult::Conflicts { conflicts, .. } => {
panic!("clean delete should not conflict: {:?}", conflicts);
}
}
}
#[test]
fn test_deeply_nested_conflict_at_leaf() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({
"level1": {
"level2": {
"level3": {"value": 1}
}
}
}));
let ours = r.store_json(&serde_json::json!({
"level1": {
"level2": {
"level3": {"value": 42}
}
}
}));
let theirs = r.store_json(&serde_json::json!({
"level1": {
"level2": {
"level3": {"value": 99}
}
}
}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, .. } => {
assert_eq!(conflicts.len(), 1);
assert!(
conflicts[0].path.contains("value"),
"conflict path should identify the leaf: {}",
conflicts[0].path
);
}
_ => panic!("expected exactly one deep conflict"),
}
}
#[test]
fn test_deeply_nested_no_conflict_different_branches() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({
"a": {"x": 1, "y": 2},
"b": {"x": 1, "y": 2}
}));
let ours = r.store_json(&serde_json::json!({
"a": {"x": 99, "y": 2}, "b": {"x": 1, "y": 2}
}));
let theirs = r.store_json(&serde_json::json!({
"a": {"x": 1, "y": 2},
"b": {"x": 1, "y": 77} }));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Success(_) => {}
MergeResult::Conflicts { conflicts, .. } => {
panic!("no conflicts expected: {:?}", conflicts);
}
MergeResult::FastForward(_) => panic!("expected success"),
}
}
#[test]
fn test_large_map_exact_conflict_count() {
let mut r = TestResolver::new();
let mut base_map = serde_json::Map::new();
let mut our_map = serde_json::Map::new();
let mut their_map = serde_json::Map::new();
for i in 0..100_i64 {
let key = format!("key{:03}", i);
base_map.insert(key.clone(), serde_json::Value::Number(i.into()));
if i >= 1 && i <= 5 {
our_map.insert(key.clone(), serde_json::Value::Number((i + 1000).into()));
their_map.insert(key.clone(), serde_json::Value::Number((i + 2000).into()));
} else {
our_map.insert(key.clone(), serde_json::Value::Number(i.into()));
their_map.insert(key.clone(), serde_json::Value::Number(i.into()));
}
}
let base = r.store_json(&serde_json::Value::Object(base_map));
let ours = r.store_json(&serde_json::Value::Object(our_map));
let theirs = r.store_json(&serde_json::Value::Object(their_map));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, partial } => {
assert_eq!(conflicts.len(), 5, "exactly 5 conflicts expected");
if let Object::Node(Node::Map(entries)) = partial {
assert_eq!(entries.len(), 100);
} else {
panic!("expected map");
}
}
_ => panic!("expected exactly 5 conflicts"),
}
}
#[test]
fn test_conflict_values_populated() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"x": 1}));
let ours = r.store_json(&serde_json::json!({"x": 2}));
let theirs = r.store_json(&serde_json::json!({"x": 3}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, .. } => {
let c = &conflicts[0];
assert_eq!(c.ours, Some(ConflictValue::Int(2)));
assert_eq!(c.theirs, Some(ConflictValue::Int(3)));
assert_eq!(c.base, Some(ConflictValue::Int(1)));
assert!(c.suggested_resolution.is_none());
}
_ => panic!("expected conflict"),
}
}
#[test]
fn test_conflict_value_from_object_atoms() {
assert_eq!(
ConflictValue::from_object(&Object::null()),
ConflictValue::Null
);
assert_eq!(
ConflictValue::from_object(&Object::bool(true)),
ConflictValue::Bool(true)
);
assert_eq!(
ConflictValue::from_object(&Object::bool(false)),
ConflictValue::Bool(false)
);
assert_eq!(
ConflictValue::from_object(&Object::int(42)),
ConflictValue::Int(42)
);
assert_eq!(
ConflictValue::from_object(&Object::int(-1)),
ConflictValue::Int(-1)
);
assert_eq!(
ConflictValue::from_object(&Object::string("hello".to_string())),
ConflictValue::String("hello".to_string())
);
assert_eq!(
ConflictValue::from_object(&Object::bytes(vec![1, 2, 3])),
ConflictValue::String("[bytes]".to_string())
);
}
#[test]
fn test_conflict_value_from_object_nodes() {
let map = Object::map(std::collections::BTreeMap::from([
("a".to_string(), Object::int(1).id()),
("b".to_string(), Object::int(2).id()),
]));
assert_eq!(
ConflictValue::from_object(&map),
ConflictValue::Complex("{map: 2 keys}".to_string())
);
let list = Object::list(vec![
Object::int(1).id(),
Object::int(2).id(),
Object::int(3).id(),
]);
assert_eq!(
ConflictValue::from_object(&list),
ConflictValue::Complex("[list: 3 items]".to_string())
);
let set = Object::set(vec![Object::int(1).id()]);
assert_eq!(
ConflictValue::from_object(&set),
ConflictValue::Complex("{set: 1 items}".to_string())
);
}
#[test]
fn test_fast_forward_base_equals_theirs() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1}));
let ours = r.store_json(&serde_json::json!({"a": 2}));
match three_way_merge(&r, &base, &ours, &base) {
MergeResult::FastForward(id) => assert_eq!(id, ours),
_ => panic!("expected fast-forward to ours"),
}
}
#[test]
fn test_fast_forward_ours_equals_theirs() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"a": 1}));
let ours = r.store_json(&serde_json::json!({"a": 99}));
let theirs = ours;
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::FastForward(id) => assert_eq!(id, ours),
_ => panic!("expected fast-forward when ours==theirs"),
}
}
#[test]
fn test_type_mismatch_conflict() {
let mut r = TestResolver::new();
let base = r.store_json(&serde_json::json!({"x": 1}));
let ours = r.store_json(&serde_json::json!({"x": {"nested": true}}));
let theirs = r.store_json(&serde_json::json!({"x": [1, 2, 3]}));
match three_way_merge(&r, &base, &ours, &theirs) {
MergeResult::Conflicts { conflicts, .. } => {
assert!(!conflicts.is_empty());
assert!(conflicts[0].path.contains("x"));
}
_ => panic!("expected conflict on type mismatch"),
}
}
}