···1111 type parse = H.block -> children
1212 type serialize = children -> H.block
13131414+ type merge =
1515+ ancestor:H.block ->
1616+ H.block ->
1717+ H.block ->
1818+ (H.block, [ `Conflict of string ]) result
1919+1420 type packed = Pack : t -> packed
1521 and rule = Rule : string * t -> rule
16221723 and t =
1824 | Opaque : t
1919- | Node : { parse : parse; serialize : serialize; rules : rule list } -> t
2525+ | Node : {
2626+ parse : parse;
2727+ serialize : serialize;
2828+ merge : merge option;
2929+ rules : rule list;
3030+ }
3131+ -> t
2032 | Rec : (unit -> t) -> t
21332234 let opaque = Opaque
2335 let ( => ) pat schema = Rule (pat, schema)
2424- let node ~parse ~serialize rules = Node { parse; serialize; rules }
3636+3737+ let node ~parse ~serialize ?merge rules =
3838+ Node { parse; serialize; merge; rules }
25392640 let fix f =
2741 let r = ref None in
···4862 let rec resolve_packed : packed -> packed =
4963 fun p -> match p with Pack (Rec f) -> resolve_packed (Pack (f ())) | _ -> p
50645151- type node_ops = { parse : parse; serialize : serialize; rules : rule list }
6565+ type node_ops = {
6666+ parse : parse;
6767+ serialize : serialize;
6868+ merge : merge option;
6969+ rules : rule list;
7070+ }
52715372 let get_node : packed -> node_ops option = function
5454- | Pack (Node { parse; serialize; rules }) ->
5555- Some { parse; serialize; rules }
7373+ | Pack (Node { parse; serialize; merge; rules }) ->
7474+ Some { parse; serialize; merge; rules }
5675 | _ -> None
57765877 (* ===== Children cache ===== *)
···299318 {
300319 parse;
301320 serialize;
321321+ merge = None;
302322 rules =
303323 [
304324 Rule ("tree", tree);
···306326 Rule ("*", Opaque);
307327 ];
308328 }
329329+330330+ (* ===== Merge ===== *)
331331+332332+ let merge (heap : (H.hash, H.block, _) Heap.t) schema ~ancestor ~ours ~theirs
333333+ =
334334+ (* 3-way merge: walk cursors in parallel *)
335335+ let rec merge_hash schema a_hash o_hash t_hash =
336336+ if H.hash_equal o_hash t_hash then Ok o_hash (* no conflict *)
337337+ else if H.hash_equal a_hash o_hash then
338338+ Ok t_hash (* only theirs changed *)
339339+ else if H.hash_equal a_hash t_hash then Ok o_hash (* only ours changed *)
340340+ else
341341+ (* Both changed — need to merge *)
342342+ match
343343+ (Heap.find heap a_hash, Heap.find heap o_hash, Heap.find heap t_hash)
344344+ with
345345+ | Some a_block, Some o_block, Some t_block ->
346346+ merge_block schema a_block o_block t_block
347347+ | _ -> Error (`Conflict "missing block")
348348+ and merge_block schema a_block o_block t_block =
349349+ let resolved = resolve_packed (Pack schema) in
350350+ match get_node resolved with
351351+ | None ->
352352+ (* Leaf: use the node's merge function, or conflict *)
353353+ Error (`Conflict "leaf conflict, no merge function")
354354+ | Some ops -> (
355355+ match ops.merge with
356356+ | Some merge_fn -> (
357357+ (* Leaf with merge function *)
358358+ match merge_fn ~ancestor:a_block o_block t_block with
359359+ | Ok merged ->
360360+ let h = H.hash_block merged in
361361+ Heap.put heap h merged;
362362+ Ok h
363363+ | Error _ as e -> e)
364364+ | None ->
365365+ (* Interior node: structural merge *)
366366+ let a_kids = ops.parse a_block in
367367+ let o_kids = ops.parse o_block in
368368+ let t_kids = ops.parse t_block in
369369+ let is_empty = function
370370+ | Named [] -> true
371371+ | Indexed [||] -> true
372372+ | _ -> false
373373+ in
374374+ if is_empty a_kids && is_empty o_kids && is_empty t_kids then
375375+ (* All three are childless (leaves) but different — conflict *)
376376+ Error (`Conflict "leaf values differ, no merge function")
377377+ else merge_children ops schema a_kids o_kids t_kids)
378378+ and merge_children ops schema a_kids o_kids t_kids =
379379+ (* Collect all names from all three *)
380380+ let names_of = function
381381+ | Named l -> List.map fst l
382382+ | Indexed arr -> List.init (Array.length arr) string_of_int
383383+ in
384384+ let find_child name = function
385385+ | Named l -> List.assoc_opt name l
386386+ | Indexed arr -> (
387387+ match int_of_string_opt name with
388388+ | Some i when i >= 0 && i < Array.length arr -> Some arr.(i)
389389+ | _ -> None)
390390+ in
391391+ let all_names =
392392+ let tbl = Hashtbl.create 16 in
393393+ List.iter (fun n -> Hashtbl.replace tbl n ()) (names_of a_kids);
394394+ List.iter (fun n -> Hashtbl.replace tbl n ()) (names_of o_kids);
395395+ List.iter (fun n -> Hashtbl.replace tbl n ()) (names_of t_kids);
396396+ Hashtbl.fold (fun n () acc -> n :: acc) tbl []
397397+ |> List.sort String.compare
398398+ in
399399+ (* Find the child schema for a name *)
400400+ let child_schema name =
401401+ match find_rule ops.rules name with
402402+ | Some (Pack s) -> s
403403+ | None -> Opaque
404404+ in
405405+ (* Merge each child *)
406406+ let rec merge_names acc = function
407407+ | [] ->
408408+ let merged = Named (List.rev acc) in
409409+ let block = ops.serialize merged in
410410+ let h = H.hash_block block in
411411+ Heap.put heap h block;
412412+ Ok h
413413+ | name :: rest -> (
414414+ let a = find_child name a_kids in
415415+ let o = find_child name o_kids in
416416+ let t = find_child name t_kids in
417417+ let child_sch = child_schema name in
418418+ match merge_child child_sch a o t with
419419+ | Ok None -> merge_names acc rest (* deleted *)
420420+ | Ok (Some child) -> merge_names ((name, child) :: acc) rest
421421+ | Error _ as e -> e)
422422+ and merge_child schema a o t =
423423+ match (a, o, t) with
424424+ | _, Some oc, Some tc when oc = tc -> Ok (Some oc) (* same *)
425425+ | Some ac, Some oc, _ when ac = oc -> Ok t (* only theirs changed *)
426426+ | Some ac, _, Some tc when ac = tc -> Ok o (* only ours changed *)
427427+ | None, None, None -> Ok None
428428+ | None, Some oc, None -> Ok (Some oc) (* ours added *)
429429+ | None, None, Some tc -> Ok (Some tc) (* theirs added *)
430430+ | None, Some _, Some _ ->
431431+ (* both added — try merge with empty ancestor *)
432432+ Error (`Conflict "both sides added same key")
433433+ | Some _, None, None -> Ok None (* both deleted *)
434434+ | Some _, Some _, None ->
435435+ Ok None (* theirs deleted, ours modified — conflict? *)
436436+ | Some _, None, Some _ ->
437437+ Ok None (* ours deleted, theirs modified — conflict? *)
438438+ | Some (`Link ah), Some (`Link oh), Some (`Link th) -> (
439439+ (* Both modified a linked child — recurse *)
440440+ match merge_hash schema ah oh th with
441441+ | Ok h -> Ok (Some (`Link h))
442442+ | Error _ as e -> e)
443443+ | Some (`Inline _), Some (`Inline ob), Some (`Inline tb) -> (
444444+ (* Both modified an inline child *)
445445+ match a with
446446+ | Some (`Inline ab) -> (
447447+ match merge_block schema ab ob tb with
448448+ | Ok h -> Ok (Some (`Link h))
449449+ | Error _ as e -> e)
450450+ | _ -> Error (`Conflict "inline merge without ancestor"))
451451+ | _ -> Error (`Conflict "incompatible child types")
452452+ in
453453+ merge_names [] all_names
454454+ in
455455+ merge_hash schema ancestor ours theirs
309456310457 (* ===== Proof ===== *)
311458
+29-4
lib/schema.mli
···4343 type serialize = children -> H.block
4444 (** How to reconstruct a block from children. *)
45454646+ type merge =
4747+ ancestor:H.block ->
4848+ H.block ->
4949+ H.block ->
5050+ (H.block, [ `Conflict of string ]) result
5151+ (** [merge ~ancestor ours theirs] merges two leaf values given their common
5252+ ancestor. *)
5353+4654 type t
4755 (** A schema node. *)
4856···5462 ["*.json"] matches by suffix, exact names match exactly. First matching
5563 rule wins. *)
56645757- val node : parse:parse -> serialize:serialize -> rule list -> t
5858- (** [node ~parse ~serialize rules] is a block with named children. [parse]
5959- extracts children, [serialize] reconstructs the block, [rules] determine
6060- each child's schema by name. *)
6565+ val node :
6666+ parse:parse -> serialize:serialize -> ?merge:merge -> rule list -> t
6767+ (** [node ~parse ~serialize ?merge rules] is a block with named children.
6868+ [merge] is used for leaf-level 3-way merge. Interior nodes are merged
6969+ structurally (child by child). *)
61706271 val fix : (t -> t) -> t
6372 (** [fix f] is a recursive schema. *)
···153162 (** [commit_node ~parse ~serialize ~tree] is a schema for commit blocks.
154163 Children: ["tree"] links to [tree], ["parent/0"], ["parent/1"], ... link
155164 to parent commits, ["message"] and ["author"] are inline metadata. *)
165165+166166+ (** {1:merge Merge}
167167+168168+ 3-way merge: given a common ancestor and two diverged states, produce a
169169+ merged state. Interior nodes are merged structurally (child by child).
170170+ Leaves use the node's [~merge] function. *)
171171+172172+ val merge :
173173+ (H.hash, H.block, _) Heap.t ->
174174+ t ->
175175+ ancestor:H.hash ->
176176+ ours:H.hash ->
177177+ theirs:H.hash ->
178178+ (H.hash, [ `Conflict of string ]) result
179179+ (** [merge heap schema ~ancestor ~ours ~theirs] performs a 3-way merge.
180180+ Returns the hash of the merged tree, or a conflict. *)
156181157182 (** {1:proof Proofs}
158183
+89
test/tar/test.ml
···179179 Alcotest.(check (option string)) "verified digestif" (Some {|"2.0"|}) v
180180 | Error (`Proof_failure msg) -> Alcotest.failf "verify: %s" msg
181181182182+(* ===== Merge tests ===== *)
183183+184184+(* Counter merge: sum the deltas *)
185185+let counter_parse : S.parse = fun _ -> S.Named []
186186+let counter_serialize : S.serialize = fun _ -> ""
187187+188188+let counter_merge : S.merge =
189189+ fun ~ancestor ours theirs ->
190190+ let to_int s = try int_of_string s with _ -> 0 in
191191+ let a = to_int ancestor and o = to_int ours and t = to_int theirs in
192192+ Ok (string_of_int (o + t - a))
193193+194194+let tree_with_counters =
195195+ let ( => ) = S.( => ) in
196196+ S.fix (fun self ->
197197+ S.node ~parse:Irmin_tar.dir_parse ~serialize:Irmin_tar.dir_serialize
198198+ [
199199+ "*.counter"
200200+ => S.node ~parse:counter_parse ~serialize:counter_serialize
201201+ ~merge:counter_merge [];
202202+ "*" => self;
203203+ ])
204204+205205+let test_merge_structural () =
206206+ let store = Hashtbl.create 64 in
207207+ let heap = H.v store in
208208+ (* Ancestor: a.txt, b.txt *)
209209+ let ancestor =
210210+ Irmin_tar.of_entries heap [ ("a.txt", "hello"); ("b.txt", "world") ]
211211+ in
212212+ (* Ours: add c.txt *)
213213+ let ours =
214214+ Irmin_tar.of_entries heap
215215+ [ ("a.txt", "hello"); ("b.txt", "world"); ("c.txt", "new-ours") ]
216216+ in
217217+ (* Theirs: add d.txt *)
218218+ let theirs =
219219+ Irmin_tar.of_entries heap
220220+ [ ("a.txt", "hello"); ("b.txt", "world"); ("d.txt", "new-theirs") ]
221221+ in
222222+ match S.merge heap Irmin_tar.tree ~ancestor ~ours ~theirs with
223223+ | Error (`Conflict msg) -> Alcotest.failf "merge conflict: %s" msg
224224+ | Ok merged_root ->
225225+ let c = S.at heap Irmin_tar.tree merged_root in
226226+ let kids = S.list c |> List.map fst |> List.sort String.compare in
227227+ Alcotest.(check (list string))
228228+ "merged"
229229+ [ "a.txt"; "b.txt"; "c.txt"; "d.txt" ]
230230+ kids;
231231+ (* Check values *)
232232+ Alcotest.(check (option string))
233233+ "c.txt" (Some "new-ours") (S.find c [ "c.txt" ]);
234234+ Alcotest.(check (option string))
235235+ "d.txt" (Some "new-theirs") (S.find c [ "d.txt" ])
236236+237237+let test_merge_counter () =
238238+ let store = Hashtbl.create 64 in
239239+ let heap = H.v store in
240240+ let ancestor = Irmin_tar.of_entries heap [ ("views.counter", "10") ] in
241241+ let ours = Irmin_tar.of_entries heap [ ("views.counter", "15") ] in
242242+ let theirs = Irmin_tar.of_entries heap [ ("views.counter", "12") ] in
243243+ match S.merge heap tree_with_counters ~ancestor ~ours ~theirs with
244244+ | Error (`Conflict msg) -> Alcotest.failf "merge conflict: %s" msg
245245+ | Ok merged_root ->
246246+ let c = S.at heap tree_with_counters merged_root in
247247+ (* 15 + 12 - 10 = 17 *)
248248+ Alcotest.(check (option string))
249249+ "counter merged" (Some "17")
250250+ (S.find c [ "views.counter" ])
251251+252252+let test_merge_conflict () =
253253+ let store = Hashtbl.create 64 in
254254+ let heap = H.v store in
255255+ let ancestor = Irmin_tar.of_entries heap [ ("a.txt", "original") ] in
256256+ let ours = Irmin_tar.of_entries heap [ ("a.txt", "ours-edit") ] in
257257+ let theirs = Irmin_tar.of_entries heap [ ("a.txt", "theirs-edit") ] in
258258+ (* No merge function for opaque blobs → conflict *)
259259+ match S.merge heap Irmin_tar.tree ~ancestor ~ours ~theirs with
260260+ | Ok _ -> Alcotest.fail "should conflict"
261261+ | Error (`Conflict _) -> ()
262262+182263let () =
183264 Alcotest.run "irmin-tar"
184265 [
···190271 [
191272 Alcotest.test_case "navigate and prove JSON in tar" `Quick
192273 test_tar_json_proof;
274274+ ] );
275275+ ( "merge",
276276+ [
277277+ Alcotest.test_case "structural merge (add/add)" `Quick
278278+ test_merge_structural;
279279+ Alcotest.test_case "CRDT counter merge" `Quick test_merge_counter;
280280+ Alcotest.test_case "conflict on opaque leaf" `Quick
281281+ test_merge_conflict;
193282 ] );
194283 ]