src/Merging.bend
ber-core-store@0.1.2.0 · 466 lines · 0 definitions
1import Base
2import ber-core-store@0.1.2.0/src/Reading.bend as Reading
3import ber-core-store@0.1.2.0/src/LogicalKey.bend as LogicalKey
4import ber-core-store@0.1.2.0/src/Comparison.bend as Comparison
5import ber-core-store@0.1.2.0/src/StateTree.bend as StateTree
6import ber-core-store@0.1.2.0/src/History.bend as History
7import ber-core-store@0.1.2.0/src/Store.bend as Store
8
9# Three-way merge, strategy "union-disjoint". The pure core decides per key
10# from (base, first, second) with an explicit truth table: equal inputs take
11# the value, single-side changes win, delete-vs-unchanged drops, every other
12# divergence conflicts (both delete-vs-modify directions — asymmetry would
13# break commutativity). Results are sorted before hashing, so compatible
14# merges commute as trees by construction.
15
16
17
18# Pure merge result: merged entries or conflicting keys.
19type MergeOutcome is Data:
20 MergedEntries{merged_entries: List<&2, Reading.ChainEntry>}
21 ConflictingKeys{conflicting_keys: List<&2, LogicalKey.LogicalKey>}
22
23# One named law verdict.
24type LawCheck is Data:
25 Check{check_name: String, check_passed: Bool}
26
27# Parents, base, result hash, strategy and checked laws.
28type MergeCertificate is Data:
29 Make{first_parent_commit: String, second_parent_commit: String, base_commit: String, result_tree_hash: String, strategy_name: String, checked_laws: List<&2, LawCheck>}
30
31# Success with commit and certificate, conflict, or unprovable.
32type MergeResult is Data:
33 MergeSuccess{resulting_commit: History.Commit, certificate: MergeCertificate}
34 MergeConflict{conflicting_keys: List<&2, LogicalKey.LogicalKey>}
35 MergeUnprovable{reason: String}
36
37# True only for successful merges.
38def merge_succeeded(merge_result: MergeResult) -> Bool:
39 match merge_result:
40 case MergeSuccess{resulting_commit, certificate}:
41 True{}
42 case MergeConflict{conflicting_keys}:
43 False{}
44 case MergeUnprovable{reason}:
45 False{}
46
47# Counts conflicting keys; zero otherwise.
48def conflict_key_count(merge_result: MergeResult) -> Nat:
49 match merge_result:
50 case MergeSuccess{resulting_commit, certificate}:
51 0n
52 case MergeConflict{conflicting_keys}:
53 List.length(&2, LogicalKey.LogicalKey, conflicting_keys)
54 case MergeUnprovable{reason}:
55 0n
56
57# Unwraps the resulting commit of a success.
58def success_commit_of(merge_result: MergeResult) -> Maybe<&2, History.Commit>:
59 match merge_result:
60 case MergeSuccess{resulting_commit, certificate}:
61 Some{resulting_commit}
62 case MergeConflict{conflicting_keys}:
63 None{}
64 case MergeUnprovable{reason}:
65 None{}
66
67# Unwraps the certificate of a success.
68def success_certificate_of(merge_result: MergeResult) -> Maybe<&2, MergeCertificate>:
69 match merge_result:
70 case MergeSuccess{resulting_commit, certificate}:
71 Some{certificate}
72 case MergeConflict{conflicting_keys}:
73 None{}
74 case MergeUnprovable{reason}:
75 None{}
76
77# Projects the first parent out of a certificate.
78def cert_first_parent(certificate: MergeCertificate) -> String:
79 match certificate:
80 case Make{first_parent_commit, second_parent_commit, base_commit, result_tree_hash, strategy_name, checked_laws}:
81 first_parent_commit
82
83# Projects the second parent out of a certificate.
84def cert_second_parent(certificate: MergeCertificate) -> String:
85 match certificate:
86 case Make{first_parent_commit, second_parent_commit, base_commit, result_tree_hash, strategy_name, checked_laws}:
87 second_parent_commit
88
89# Projects the base commit out of a certificate.
90def cert_base_commit(certificate: MergeCertificate) -> String:
91 match certificate:
92 case Make{first_parent_commit, second_parent_commit, base_commit, result_tree_hash, strategy_name, checked_laws}:
93 base_commit
94
95# Projects the result tree out of a certificate.
96def cert_result_tree(certificate: MergeCertificate) -> String:
97 match certificate:
98 case Make{first_parent_commit, second_parent_commit, base_commit, result_tree_hash, strategy_name, checked_laws}:
99 result_tree_hash
100
101# Projects the strategy name out of a certificate.
102def cert_strategy_name(certificate: MergeCertificate) -> String:
103 match certificate:
104 case Make{first_parent_commit, second_parent_commit, base_commit, result_tree_hash, strategy_name, checked_laws}:
105 strategy_name
106
107# Projects the checked laws out of a certificate.
108def cert_checked_laws(certificate: MergeCertificate) -> List<&2, LawCheck>:
109 match certificate:
110 case Make{first_parent_commit, second_parent_commit, base_commit, result_tree_hash, strategy_name, checked_laws}:
111 checked_laws
112
113# Projects the verdict out of a law check.
114def law_check_passed(law_check: LawCheck) -> Bool:
115 match law_check:
116 case Check{check_name, check_passed}:
117 check_passed
118
119# Projects merged entries; conflicts project as empty.
120def merged_entries_of(merge_outcome: MergeOutcome) -> List<&2, Reading.ChainEntry>:
121 match merge_outcome:
122 case MergedEntries{merged_entries}:
123 merged_entries
124 case ConflictingKeys{conflicting_keys}:
125 Nil{}
126
127# Projects conflicting keys; merges project as empty.
128def conflicting_keys_of(merge_outcome: MergeOutcome) -> List<&2, LogicalKey.LogicalKey>:
129 match merge_outcome:
130 case MergedEntries{merged_entries}:
131 Nil{}
132 case ConflictingKeys{conflicting_keys}:
133 conflicting_keys
134
135# True short-circuits the membership scan.
136def decide_contains_member(head_matches: Bool, recursive_result: Bool) -> Bool:
137 match head_matches:
138 case True{}:
139 True{}
140 case False{}:
141 recursive_result
142
143# Scans the visit list for the identifier.
144def list_contains_member(+target_identifier: String, visit_list: List<&2, String>) -> Bool:
145 match visit_list:
146 case Nil{}:
147 False{}
148 case Con{head_identifier, remaining_identifiers}:
149 decide_contains_member(String.eq(head_identifier, target_identifier), list_contains_member(target_identifier, remaining_identifiers))
150
151# Unknown commits contribute no parents.
152def parent_list_or_empty(commit_maybe: Maybe<&2, History.Commit>) -> List<&2, String>:
153 match commit_maybe:
154 case None{}:
155 Nil{}
156 case Some{found_commit}:
157 History.commit_parents_of(found_commit)
158
159# Skips visited parents in the queue.
160def decide_queue_member(is_member: Bool, head_identifier: String, tail_queue: List<&2, String>) -> List<&2, String>:
161 match is_member:
162 case True{}:
163 tail_queue
164 case False{}:
165 Con{head_identifier, tail_queue}
166
167# Keeps only unvisited parents.
168def filter_unvisited(parent_identifiers: List<&2, String>, +visited_accum: List<&2, String>) -> List<&2, String>:
169 match parent_identifiers:
170 case Nil{}:
171 Nil{}
172 case Con{+head_identifier, remaining_parents}:
173 decide_queue_member(list_contains_member(head_identifier, visited_accum), head_identifier, filter_unvisited(remaining_parents, visited_accum))
174
175# Deduplicates the visited accumulator.
176def decide_visited_member(already_visited: Bool, +head_identifier: String, tail_result: List<&2, String>) -> List<&2, String>:
177 match already_visited:
178 case True{}:
179 tail_result
180 case False{}:
181 Con{head_identifier, tail_result}
182
183# Breadth-first ancestor collection with fuel.
184def collect_ancestors(remaining_fuel: Nat, visit_queue: List<&2, String>, +visited_accum: List<&2, String>) -> Store.Op<&2, List<&2, String>>:
185 match remaining_fuel:
186 case 0n:
187 do Store.Op<&2, List<&2, String>>:
188 return visited_accum
189 case 1n+fuel_left:
190 match visit_queue:
191 case Nil{}:
192 do Store.Op<&2, List<&2, String>>:
193 return visited_accum
194 case Con{+head_identifier, remaining_queue}:
195 do Store.Op<&2, List<&2, String>>:
196 commit_maybe : Maybe<&2, History.Commit> <- History.load_commit(head_identifier)
197 +extended_queue : List<&2, String> = List.append(&2, String, remaining_queue, filter_unvisited(parent_list_or_empty(commit_maybe), visited_accum))
198 tail_result : List<&2, String> <- collect_ancestors(fuel_left, extended_queue, Con{head_identifier, visited_accum})
199 return decide_visited_member(list_contains_member(head_identifier, visited_accum), head_identifier, tail_result)
200
201# Some on the first common commit, otherwise the tail.
202def decide_lca_found(is_common: Bool, +commit_identifier: String, tail_result: Maybe<&2, String>) -> Maybe<&2, String>:
203 match is_common:
204 case True{}:
205 Some{commit_identifier}
206 case False{}:
207 tail_result
208
209# A hit ends the ascent: deeper links cannot beat the first common commit.
210def lca_fuel_left(is_common: Bool, fuel_left: Nat) -> Nat:
211 match is_common:
212 case True{}:
213 0n
214 case False{}:
215 fuel_left
216
217# Walks the second branch for the first ancestor-set hit. checker_fuel
218# decreases every level for the termination checker; gated_fuel carries the
219# early stop. Callers pass the same fuel twice.
220def find_first_common(+ancestor_set: List<&2, String>, checker_fuel: Nat, gated_fuel: Nat, +commit_identifier: String) -> Store.Op<&2, Maybe<&2, String>>:
221 match checker_fuel gated_fuel:
222 case 0n _:
223 History.empty_text_maybe()
224 case 1n+checker_left 0n:
225 History.empty_text_maybe()
226 case 1n+checker_left 1n+gated_left:
227 do Store.Op<&2, Maybe<&2, String>>:
228 commit_maybe : Maybe<&2, History.Commit> <- History.load_commit(commit_identifier)
229 +parent_text : String = History.first_parent_text(commit_maybe)
230 tail_result : Maybe<&2, String> <- find_first_common(ancestor_set, checker_left, lca_fuel_left(list_contains_member(commit_identifier, ancestor_set), gated_left), parent_text)
231 return decide_lca_found(list_contains_member(commit_identifier, ancestor_set), commit_identifier, tail_result)
232
233# Emits the first side entry.
234def keep_first_entry(+combined_key: String, +entry_value: String, tail_outcome: MergeOutcome) -> MergeOutcome:
235 match tail_outcome:
236 case ConflictingKeys{conflicting_keys}:
237 ConflictingKeys{conflicting_keys}
238 case MergedEntries{merged_entries}:
239 MergedEntries{Con{Reading.make_entry(combined_key, entry_value), merged_entries}}
240
241# Emits the second side entry.
242def keep_second_entry(+combined_key: String, +entry_value: String, tail_outcome: MergeOutcome) -> MergeOutcome:
243 match tail_outcome:
244 case ConflictingKeys{conflicting_keys}:
245 ConflictingKeys{conflicting_keys}
246 case MergedEntries{merged_entries}:
247 MergedEntries{Con{Reading.make_entry(combined_key, entry_value), merged_entries}}
248
249# Records the key as conflicting.
250def raise_merge_conflict(+combined_key: String, tail_outcome: MergeOutcome) -> MergeOutcome:
251 match tail_outcome:
252 case ConflictingKeys{conflicting_keys}:
253 ConflictingKeys{Con{LogicalKey.split_combined_key(combined_key), conflicting_keys}}
254 case MergedEntries{merged_entries}:
255 ConflictingKeys{Con{LogicalKey.split_combined_key(combined_key), Nil{}}}
256
257# Unchanged second keeps first; changed conflicts.
258def decide_second_changed(second_unchanged: Bool, +combined_key: String, +first_text: String, +second_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
259 match second_unchanged:
260 case True{}:
261 keep_first_entry(combined_key, first_text, tail_outcome)
262 case False{}:
263 raise_merge_conflict(combined_key, tail_outcome)
264
265# Unchanged first keeps second; otherwise checks second.
266def decide_divergent_base(first_unchanged: Bool, second_unchanged: Bool, +combined_key: String, +first_text: String, +second_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
267 match first_unchanged:
268 case True{}:
269 keep_second_entry(combined_key, second_text, tail_outcome)
270 case False{}:
271 decide_second_changed(second_unchanged, combined_key, first_text, second_text, tail_outcome)
272
273# Resolves two present values against the base.
274def resolve_divergent(+combined_key: String, base_value: Maybe<&2, String>, +first_text: String, +second_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
275 match base_value:
276 case None{}:
277 raise_merge_conflict(combined_key, tail_outcome)
278 case Some{+base_text}:
279 decide_divergent_base(String.eq(first_text, base_text), String.eq(second_text, base_text), combined_key, first_text, second_text, tail_outcome)
280
281# Equal sides keep the value; unequal consults the base.
282def decide_both_equal(both_equal: Bool, +combined_key: String, base_value: Maybe<&2, String>, +first_text: String, +second_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
283 match both_equal:
284 case True{}:
285 keep_first_entry(combined_key, first_text, tail_outcome)
286 case False{}:
287 resolve_divergent(combined_key, base_value, first_text, second_text, tail_outcome)
288
289# Resolves two present values.
290def resolve_both_present(+combined_key: String, base_value: Maybe<&2, String>, +first_text: String, +second_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
291 decide_both_equal(String.eq(first_text, second_text), combined_key, base_value, first_text, second_text, tail_outcome)
292
293# Unchanged present drops; modified conflicts.
294def decide_present_delete(first_unchanged: Bool, +combined_key: String, +first_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
295 match first_unchanged:
296 case True{}:
297 tail_outcome
298 case False{}:
299 raise_merge_conflict(combined_key, tail_outcome)
300
301# Keeps added values; checks modified-against-base.
302def resolve_present_vs_deleted(+combined_key: String, base_value: Maybe<&2, String>, +first_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
303 match base_value:
304 case None{}:
305 keep_first_entry(combined_key, first_text, tail_outcome)
306 case Some{base_text}:
307 decide_present_delete(String.eq(first_text, base_text), combined_key, first_text, tail_outcome)
308
309# Dispatches on the second side presence.
310def resolve_first_present(+combined_key: String, base_value: Maybe<&2, String>, +first_text: String, second_value: Maybe<&2, String>, tail_outcome: MergeOutcome) -> MergeOutcome:
311 match second_value:
312 case None{}:
313 resolve_present_vs_deleted(combined_key, base_value, first_text, tail_outcome)
314 case Some{second_text}:
315 resolve_both_present(combined_key, base_value, first_text, second_text, tail_outcome)
316
317# Unchanged second drops; modified conflicts.
318def decide_delete_modify(second_unchanged: Bool, +combined_key: String, +second_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
319 match second_unchanged:
320 case True{}:
321 tail_outcome
322 case False{}:
323 raise_merge_conflict(combined_key, tail_outcome)
324
325# Keeps added values; checks modified-against-base.
326def resolve_deleted_vs_present(+combined_key: String, base_value: Maybe<&2, String>, +second_text: String, tail_outcome: MergeOutcome) -> MergeOutcome:
327 match base_value:
328 case None{}:
329 keep_second_entry(combined_key, second_text, tail_outcome)
330 case Some{base_text}:
331 decide_delete_modify(String.eq(second_text, base_text), combined_key, second_text, tail_outcome)
332
333# Dispatches on the second side presence.
334def resolve_first_absent(+combined_key: String, base_value: Maybe<&2, String>, second_value: Maybe<&2, String>, tail_outcome: MergeOutcome) -> MergeOutcome:
335 match second_value:
336 case None{}:
337 tail_outcome
338 case Some{second_text}:
339 resolve_deleted_vs_present(combined_key, base_value, second_text, tail_outcome)
340
341# Decides one key from its (base, first, second) values.
342def resolve_key_triple(+combined_key: String, base_value: Maybe<&2, String>, first_value: Maybe<&2, String>, second_value: Maybe<&2, String>, tail_outcome: MergeOutcome) -> MergeOutcome:
343 match first_value:
344 case None{}:
345 resolve_first_absent(combined_key, base_value, second_value, tail_outcome)
346 case Some{first_text}:
347 resolve_first_present(combined_key, base_value, first_text, second_value, tail_outcome)
348
349# Emits unseen keys once.
350def decide_unique_emit(is_seen: Bool, +head_key: String, tail_keys: List<&2, String>) -> List<&2, String>:
351 match is_seen:
352 case True{}:
353 tail_keys
354 case False{}:
355 Con{head_key, tail_keys}
356
357# Collects candidate keys in first-seen order.
358def unique_seen_keys(entries: List<&2, Reading.ChainEntry>, +seen_keys: List<&2, String>) -> List<&2, String>:
359 match entries:
360 case Nil{}:
361 Nil{}
362 case Con{+head_entry, remaining_entries}:
363 decide_unique_emit(list_contains_member(Reading.entry_key_of(head_entry), seen_keys), Reading.entry_key_of(head_entry), unique_seen_keys(remaining_entries, Con{Reading.entry_key_of(head_entry), seen_keys}))
364
365# Concatenates base, first and second entries.
366def all_candidate_entries(+base_entries: List<&2, Reading.ChainEntry>, +first_entries: List<&2, Reading.ChainEntry>, +second_entries: List<&2, Reading.ChainEntry>) -> List<&2, Reading.ChainEntry>:
367 List.append(&2, Reading.ChainEntry, base_entries, List.append(&2, Reading.ChainEntry, first_entries, second_entries))
368
369# Merges every unique key against the three entry lists.
370def merge_unique_keys(unique_keys: List<&2, String>, +base_entries: List<&2, Reading.ChainEntry>, +first_entries: List<&2, Reading.ChainEntry>, +second_entries: List<&2, Reading.ChainEntry>) -> MergeOutcome:
371 match unique_keys:
372 case Nil{}:
373 MergedEntries{Nil{}}
374 case Con{+head_key, remaining_keys}:
375 resolve_key_triple(head_key, Comparison.value_of_key(head_key, base_entries), Comparison.value_of_key(head_key, first_entries), Comparison.value_of_key(head_key, second_entries), merge_unique_keys(remaining_keys, base_entries, first_entries, second_entries))
376
377# Sorts merged entries by key for canonical hashing.
378def sort_entries(unsorted_entries: List<&2, Reading.ChainEntry>) -> List<&2, Reading.ChainEntry>:
379 match unsorted_entries:
380 case Nil{}:
381 Nil{}
382 case Con{head_entry, remaining_entries}:
383 StateTree.insert_single_entry(sort_entries(remaining_entries), head_entry)
384
385# Sorts merged entries; conflicts pass through.
386def finalize_merge(merge_outcome: MergeOutcome) -> MergeOutcome:
387 match merge_outcome:
388 case MergedEntries{merged_entries}:
389 MergedEntries{sort_entries(merged_entries)}
390 case ConflictingKeys{conflicting_keys}:
391 ConflictingKeys{conflicting_keys}
392
393# Pure three-way merge core over entry lists.
394def merge_entry_lists(+base_entries: List<&2, Reading.ChainEntry>, +first_entries: List<&2, Reading.ChainEntry>, +second_entries: List<&2, Reading.ChainEntry>) -> MergeOutcome:
395 finalize_merge(merge_unique_keys(unique_seen_keys(all_candidate_entries(base_entries, first_entries, second_entries), Nil{}), base_entries, first_entries, second_entries))
396
397# Wraps conflicting keys as a Sess result.
398def conflict_merge_result(conflicting_keys: List<&2, LogicalKey.LogicalKey>) -> Store.Op<&2, MergeResult>:
399 do Store.Op<&2, MergeResult>:
400 return MergeConflict{conflicting_keys}
401
402# Wraps an unprovable reason as a Sess result.
403def unprovable_merge_result(+reason: String) -> Store.Op<&2, MergeResult>:
404 do Store.Op<&2, MergeResult>:
405 return MergeUnprovable{reason}
406
407# Issues the certificate with pairwise-compatibility evidence.
408def build_merge_certificate(+first_parent_commit: String, +second_parent_commit: String, +base_commit: String, +result_tree_hash: String, +strategy_name: String) -> MergeCertificate:
409 Make{first_parent_commit, second_parent_commit, base_commit, result_tree_hash, strategy_name, Con{Check{"pairwise-compatibility", True{}}, Nil{}}}
410
411# Persists tree, commit and index for merged entries.
412def persist_merged_result(+first_identifier: String, +second_identifier: String, +base_identifier: String, +merged_entries: List<&2, Reading.ChainEntry>) -> Store.Op<&2, MergeResult>:
413 do Store.Op<&2, MergeResult>:
414 +tree_hash_value : String <- History.persist_tree(merged_entries)
415 +resulting_commit : History.Commit <- History.persist_commit(Con{first_identifier, Con{second_identifier, Nil{}}}, Con{History.Field{"strategy", "union-disjoint"}, Nil{}}, tree_hash_value)
416 indexed_unit : Unit <- History.write_each_index(merged_entries, resulting_commit)
417 return MergeSuccess{resulting_commit, build_merge_certificate(first_identifier, second_identifier, base_identifier, tree_hash_value, "union-disjoint")}
418
419# Persists merges; conflicts become results directly.
420def persist_merge_outcome(merge_outcome: MergeOutcome, +first_identifier: String, +second_identifier: String, +base_identifier: String) -> Store.Op<&2, MergeResult>:
421 match merge_outcome:
422 case ConflictingKeys{conflicting_keys}:
423 conflict_merge_result(conflicting_keys)
424 case MergedEntries{merged_entries}:
425 persist_merged_result(first_identifier, second_identifier, base_identifier, merged_entries)
426
427# Loads the three trees and merges them.
428def merge_three_way(+first_identifier: String, +second_identifier: String, +base_identifier: String) -> Store.Op<&2, MergeResult>:
429 do Store.Op<&2, MergeResult>:
430 base_tree_maybe : Maybe<&2, StateTree.StateTreeNode> <- History.read_tree_at(base_identifier)
431 first_tree_maybe : Maybe<&2, StateTree.StateTreeNode> <- History.read_tree_at(first_identifier)
432 second_tree_maybe : Maybe<&2, StateTree.StateTreeNode> <- History.read_tree_at(second_identifier)
433 +base_entries : List<&2, Reading.ChainEntry> = Comparison.entries_or_empty(base_tree_maybe)
434 +first_entries : List<&2, Reading.ChainEntry> = Comparison.entries_or_empty(first_tree_maybe)
435 +second_entries : List<&2, Reading.ChainEntry> = Comparison.entries_or_empty(second_tree_maybe)
436 merged_outcome : MergeOutcome = finalize_merge(merge_entry_lists(base_entries, first_entries, second_entries))
437 persisted_result : MergeResult <- persist_merge_outcome(merged_outcome, first_identifier, second_identifier, base_identifier)
438 return persisted_result
439
440# Merges on the ancestor; missing ancestors are unprovable.
441def branch_lca_search(lca_maybe: Maybe<&2, String>, +first_identifier: String, +second_identifier: String) -> Store.Op<&2, MergeResult>:
442 match lca_maybe:
443 case None{}:
444 unprovable_merge_result("no-common-ancestor")
445 case Some{base_identifier}:
446 merge_three_way(first_identifier, second_identifier, base_identifier)
447
448# Finds the lowest common ancestor, then merges.
449def merge_with_ancestor_search(+first_identifier: String, +second_identifier: String) -> Store.Op<&2, MergeResult>:
450 do Store.Op<&2, MergeResult>:
451 ancestor_set : List<&2, String> <- collect_ancestors(10000n, Con{first_identifier, Nil{}}, Nil{})
452 lca_maybe : Maybe<&2, String> <- find_first_common(ancestor_set, 10000n, 10000n, second_identifier)
453 merged_result : MergeResult <- branch_lca_search(lca_maybe, first_identifier, second_identifier)
454 return merged_result
455
456# Only union-disjoint merges; anything else is unprovable.
457def decide_strategy_name(strategy_matches: Bool, +first_identifier: String, +second_identifier: String) -> Store.Op<&2, MergeResult>:
458 match strategy_matches:
459 case True{}:
460 merge_with_ancestor_search(first_identifier, second_identifier)
461 case False{}:
462 unprovable_merge_result("unknown-strategy")
463
464# Merges two commits by strategy name.
465def merge_commits(+first_identifier: String, +second_identifier: String, +strategy_name: String) -> Store.Op<&2, MergeResult>:
466 decide_strategy_name(String.eq(strategy_name, "union-disjoint"), first_identifier, second_identifier)