Conflicts as data
In hav, a conflict is an object (base plus both sides) stored inside a snap
like any file. It materializes as markers only when projected into a working
tree, and you resolve it by editing the file. Merges, syncs, ties, and rebases
always complete and always return a snap. There is no mid-merge state, no
--continue, and no --abort anywhere.
The merge rules
Every merge in hav is the same 3-way tree merge (base B, ours O, theirs T), decided per path:
| Situation | Result |
|---|---|
| O == T | take it |
| only one side differs from B | take that side |
| both differ, both text | line-level 3-way merge; clean → merged file, conflicted → conflict object {base, ours, theirs} |
| add/add with different content | conflict (no base) |
| modify/delete | conflict (one side deleted) |
| both modified, binary | conflict, no text merge attempted |
| directory/file collision | take ours and warn (a documented v1 limitation) |
"Ours" is the destination (the buoy head being tied into, your knot when syncing), and "theirs" is the source.
Materialization
When a snap containing a conflict is projected into a working tree, the conflict renders as familiar, deterministic markers:
<<<<<<< ours
…your side…
||||||| base
…the common ancestor…
=======
…their side…
>>>>>>> theirsA deleted side renders empty with a (deleted) note. The rendering is
byte-stable, which is how capture can tell "untouched conflict" from "edited
file."
Resolution is just editing
There is no resolve ceremony. Capture checks every conflicted path:
- bytes unchanged from the rendering → still conflicted;
- no marker lines left → resolved, and the entry becomes a normal file;
- changed but markers remain → partially edited, still conflicted (
stsays so).
Edit the file until the markers are gone, and whatever command you run next records the resolution. Two conveniences:
hav resolve <path> --take ours|theirswrites the chosen side for you.hav conflictslists what's outstanding in the workspace knot.
Where conflicts land
Deliberately asymmetric:
syncputs conflicts in the syncing workspace's knot, your tree, because you understand your half. The buoy never carries them.tie(a genuine merge of diverged histories) stores conflicts inside the merge knot's snap. The tie completes, the buoy moves, the conflict is data on the new head, andtietells you:K conflicts stored — hav conflicts.- The cascade stores conflicts inside the descendants it re-merges.
Conflict variants
A conflict on a sealed variant
follows the same rules with one twist: its base and sides are commitments
rather than content. It is a public conflict object anyone can record, even a
merger without the keys, and only members can materialize and resolve it. Each
variant conflicts independently, so the public variant and each crew's variant
reconcile separately, and cross-crew edits never conflict at all. hav conflicts tags a conflicted variant with its crew, and non-members carry it
opaquely until a member settles it. Clean text merges of a sealed variant
still happen automatically for members, resealed as they land.
Conflicts never nest
When a conflicted file is itself an input to a later merge (a cascade re-merging on top of a conflict), the conflict entry flattens to its rendered marker bytes as a plain file first. Conflict sides are always plain content, so a recursive pile-up of conflicts inside conflicts cannot happen, and every tree is always materializable.