#![allow(clippy::many_single_char_names)] #![allow(clippy::similar_names)]
use mkit_core::{
Blob, Commit, ConflictKind, EntryMode, Identity, Object, ObjectStore, Tree, TreeEntry,
cherry_pick, diff_trees, find_merge_base, hash, is_ancestor, merge_trees, serialize,
};
use tempfile::TempDir;
type Hash = [u8; 32];
fn fresh() -> (TempDir, ObjectStore) {
let d = TempDir::new().unwrap();
let s = ObjectStore::init(d.path()).unwrap();
(d, s)
}
fn put_blob(s: &ObjectStore, data: &[u8]) -> Hash {
s.write(
&serialize::serialize(&Object::Blob(Blob {
data: data.to_vec(),
}))
.unwrap(),
)
.unwrap()
}
fn put_tree(s: &ObjectStore, entries: Vec<TreeEntry>) -> Hash {
s.write(&serialize::serialize(&Object::Tree(Tree { entries })).unwrap())
.unwrap()
}
fn entry(name: &[u8], mode: EntryMode, h: Hash) -> TreeEntry {
TreeEntry {
name: name.to_vec(),
mode,
object_hash: h,
}
}
fn put_commit(s: &ObjectStore, tree: Hash, parents: &[Hash], message: &str) -> Hash {
let c = Commit {
tree_hash: tree,
parents: parents.to_vec(),
author: Identity::ed25519([0; 32]),
signer: [0; 32],
message: message.as_bytes().to_vec(),
timestamp: message.len() as u64,
message_hash: [0; 32],
content_digest: [0; 32],
signature: [0; 64],
};
s.write(&serialize::serialize(&Object::Commit(c)).unwrap())
.unwrap()
}
fn diamond(s: &ObjectStore) -> (Hash, Hash, Hash, Hash, Hash) {
let blob = put_blob(s, b"x");
let tree = put_tree(s, vec![entry(b"f", EntryMode::Blob, blob)]);
let root = put_commit(s, tree, &[], "root");
let left = put_commit(s, tree, &[root], "left");
let right = put_commit(s, tree, &[root], "right");
let merge = put_commit(s, tree, &[left, right], "merge");
(root, left, right, merge, tree)
}
#[test]
fn diff_then_merge_round_trip() {
let (_d, s) = fresh();
let blob_a = put_blob(&s, b"aaa");
let blob_b = put_blob(&s, b"bbb");
let blob_c = put_blob(&s, b"ccc");
let base = put_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
let ours = put_tree(
&s,
vec![
entry(b"a.txt", EntryMode::Blob, blob_a),
entry(b"b.txt", EntryMode::Blob, blob_b),
],
);
let theirs = put_tree(
&s,
vec![
entry(b"a.txt", EntryMode::Blob, blob_a),
entry(b"c.txt", EntryMode::Blob, blob_c),
],
);
let dours = diff_trees(&s, Some(base), Some(ours)).unwrap();
let dtheirs = diff_trees(&s, Some(base), Some(theirs)).unwrap();
assert_eq!(dours.entries.len(), 1);
assert_eq!(dtheirs.entries.len(), 1);
assert_eq!(dours.entries[0].path, "b.txt");
assert_eq!(dtheirs.entries[0].path, "c.txt");
let m = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
assert!(!m.has_conflicts());
let merged = match s.read_object(&m.tree_hash).unwrap() {
Object::Tree(t) => t.entries,
_ => panic!("expected tree"),
};
assert_eq!(merged.len(), 3);
let dmerged = diff_trees(&s, Some(base), Some(m.tree_hash)).unwrap();
assert_eq!(dmerged.entries.len(), 2);
assert_eq!(dmerged.entries[0].path, "b.txt");
assert_eq!(dmerged.entries[1].path, "c.txt");
}
#[test]
fn find_merge_base_on_diamond_is_root() {
let (_d, s) = fresh();
let (root, left, right, _merge, _tree) = diamond(&s);
assert_eq!(find_merge_base(&s, left, right).unwrap(), Some(root));
}
#[test]
fn is_ancestor_on_diamond() {
let (_d, s) = fresh();
let (root, left, right, merge, _tree) = diamond(&s);
assert!(is_ancestor(&s, root, left).unwrap());
assert!(is_ancestor(&s, root, right).unwrap());
assert!(is_ancestor(&s, left, merge).unwrap());
assert!(is_ancestor(&s, right, merge).unwrap());
assert!(!is_ancestor(&s, left, right).unwrap());
}
#[test]
fn cherry_pick_target_then_diff_picks_only_target_changes() {
let (_d, s) = fresh();
let blob_a = put_blob(&s, b"aaa");
let blob_b = put_blob(&s, b"bbb");
let blob_c = put_blob(&s, b"ccc");
let base_tree = put_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
let base_commit = put_commit(&s, base_tree, &[], "base");
let add_b_tree = put_tree(
&s,
vec![
entry(b"a.txt", EntryMode::Blob, blob_a),
entry(b"b.txt", EntryMode::Blob, blob_b),
],
);
let add_b = put_commit(&s, add_b_tree, &[base_commit], "add b");
let add_c_tree = put_tree(
&s,
vec![
entry(b"a.txt", EntryMode::Blob, blob_a),
entry(b"b.txt", EntryMode::Blob, blob_b),
entry(b"c.txt", EntryMode::Blob, blob_c),
],
);
let _add_c = put_commit(&s, add_c_tree, &[add_b], "add c");
let r = cherry_pick(&s, add_b, base_tree).unwrap();
assert!(!r.has_conflicts());
assert_eq!(r.original_message, b"add b");
let d = diff_trees(&s, Some(base_tree), Some(r.tree_hash)).unwrap();
assert_eq!(d.entries.len(), 1);
assert_eq!(d.entries[0].path, "b.txt");
}
#[test]
fn cherry_pick_modify_modify_conflict_carries_message() {
let (_d, s) = fresh();
let v0 = put_blob(&s, b"v0");
let v1 = put_blob(&s, b"v1");
let v2 = put_blob(&s, b"v2");
let base_tree = put_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v0)]);
let base_commit = put_commit(&s, base_tree, &[], "base");
let target_tree = put_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
let target = put_commit(&s, target_tree, &[base_commit], "their change");
let ours_tree = put_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
let r = cherry_pick(&s, target, ours_tree).unwrap();
assert!(r.has_conflicts());
assert_eq!(r.conflicts.len(), 1);
assert_eq!(r.conflicts[0].kind, ConflictKind::ModifyModify);
assert_eq!(r.original_message, b"their change");
let merged = match s.read_object(&r.tree_hash).unwrap() {
Object::Tree(t) => t.entries,
_ => panic!("expected tree"),
};
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].object_hash, v2);
}
#[test]
fn merge_and_cherry_pick_are_byte_deterministic() {
let (_d, s) = fresh();
let v0 = put_blob(&s, b"phase5a:v0");
let v1 = put_blob(&s, b"phase5a:v1");
let v2 = put_blob(&s, b"phase5a:v2");
let base = put_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, v0)]);
let ours = put_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, v1)]);
let theirs = put_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, v0)]);
let m = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
assert!(!m.has_conflicts());
assert_eq!(
m.tree_hash, ours,
"single-side modify must produce the modifying side's tree verbatim"
);
let m2 = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
assert_eq!(m.tree_hash, m2.tree_hash);
let merged_bytes = s.read(&m.tree_hash).unwrap();
assert_eq!(hash::hash(&merged_bytes), m.tree_hash);
let target_commit = put_commit(&s, ours, &[put_commit(&s, base, &[], "base")], "modify");
let r1 = cherry_pick(&s, target_commit, v_to_tree(&s, v2)).unwrap();
let r2 = cherry_pick(&s, target_commit, v_to_tree(&s, v2)).unwrap();
assert_eq!(r1.tree_hash, r2.tree_hash);
assert_eq!(r1.original_message, r2.original_message);
}
fn v_to_tree(s: &ObjectStore, v: Hash) -> Hash {
put_tree(s, vec![entry(b"f.txt", EntryMode::Blob, v)])
}
#[test]
fn merge_uses_find_merge_base_on_diamond() {
let (_d, s) = fresh();
let (root, left, right, _merge, tree) = diamond(&s);
let base = find_merge_base(&s, left, right).unwrap().expect("has base");
assert_eq!(base, root);
let left_tree = match s.read_object(&left).unwrap() {
Object::Commit(c) => c.tree_hash,
_ => panic!(),
};
let right_tree = match s.read_object(&right).unwrap() {
Object::Commit(c) => c.tree_hash,
_ => panic!(),
};
let base_tree = match s.read_object(&base).unwrap() {
Object::Commit(c) => c.tree_hash,
_ => panic!(),
};
let m = merge_trees(&s, Some(base_tree), Some(left_tree), Some(right_tree)).unwrap();
assert!(!m.has_conflicts());
assert_eq!(m.tree_hash, tree);
}