Rewriting history
History curation in hav is cheap, ordinary, and safe, because identity (knots) is separate from content (snaps) and because conflicts are data. This page covers the five curation verbs and the machinery that makes them safe: the auto-rebase cascade.
The verbs
edit: change an old knot in place
hav edit <knotish> moves your workspace onto an existing knot. From then on,
your captures append versions to that knot, editing history directly instead
of stacking fixups. Every snap-changing capture triggers the cascade (below),
so tied descendants follow automatically.
edit pins the workspace (it stops tracking a buoy) and enforces one
workspace per knot. If another workspace already holds the knot, it refuses,
and the escape is hav new on top with a squash later.
squash: fold a knot into its parent
hav squash [<knot>] folds a knot into its first parent (or base): the parent
takes the child's snap, descriptions concatenate, and the child is retired,
meaning hidden from log and never deleted. The parent's other descendants
cascade.
split: additive, by file or by hunk
hav split --take <path>[@N]... --into-new -m <msg> moves selected work,
either whole files or individual hunks by their path@N tags from st and
diff, into a new knot beneath the current one. The remainder stays where
it was. You select what leaves, not what stays.
The result is an untied stack, and a later bare tie lands the whole stack,
bottom-up, in one command.
absorb: route fixes to the knots that own the lines
hav absorb takes the workspace knot's edits and distributes them into the
untied ancestors that own the touched lines, per-line blame style. The rules:
- Never guesses. A change moves only when every replaced line has exactly one unambiguous owner. Anything ambiguous, conflicted, or owned by tied history stays put, and whole-file additions and deletions always stay.
- Never touches landed history. The candidate chain stops at the first tied knot or workspace-held knot.
- Never changes your tree. Only attribution moves, and working-tree bytes are identical before and after. Your knot's diff shrinks to just its own work.
discard: abandon a draft
hav discard abandons the workspace's untied draft: the tree reverts to the
draft's base, a fresh empty draft takes its place, and the abandoned knot is
retired. It refuses tied history (dropping landed history is move, pointer
surgery stated as such) and drafts with knots stacked on them. Like everything
else, it is one op, so hav undo brings the work back, files included.
The auto-rebase cascade
Whenever a knot's snap changes under descendants, whether from a capture on an
edited knot or a squash fold, every descendant re-merges automatically,
atomically with the change:
- Each descendant 3-way merges its own content onto the rewritten ancestor, its ancestry edge re-pins to the new snap, and the process recurses while snaps keep changing.
- Conflicts are stored as data inside whichever descendant they arise in. The cascade never stops, never asks, and never leaves a half-rebased state. (Conflicted inputs flatten first; conflicts never nest.)
- Buoy heads keep their ids. Buoys point at knot ids, and knot ids are stable, so heads absorb rewrites without moving.
- The whole cascade records inside the triggering command's op: one
undorestores everything.
Workspaces are never cascaded
The one exclusion is the files-never-move
principle again:
knots held by a workspace are skipped, with a warning. That workspace's base
is now stale, its st says base kn-… rewritten — hav sync, and an explicit
sync refreshes it in place. Nothing ever auto-rebases under a running build.
Unheld knots, such as tied history or a split stack's intermediates, have no files and rebase freely.
What "retired" means
Squashed and discarded knots are hidden from log, not destroyed. Their objects
remain reachable through the op journal until they age out of
retention, and hav sweep is the only thing that physically frees them.
Nothing in hav's curation vocabulary deletes bytes.