hav
Concepts

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:

SituationResult
O == Ttake it
only one side differs from Btake that side
both differ, both textline-level 3-way merge; clean → merged file, conflicted → conflict object {base, ours, theirs}
add/add with different contentconflict (no base)
modify/deleteconflict (one side deleted)
both modified, binaryconflict, no text merge attempted
directory/file collisiontake 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…
>>>>>>> theirs

A 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 (st says 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|theirs writes the chosen side for you.
  • hav conflicts lists what's outstanding in the workspace knot.

Where conflicts land

Deliberately asymmetric:

  • sync puts 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, and tie tells 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.

On this page