Capture
Every hav command except init begins the same way: capture the current
workspace. Walk the tree, hash the files, and record a new version of the
workspace's knot with the new snap if anything changed. This is what "the
working copy is the knot" means mechanically. By the time any command runs,
the snapshot already exists.
Scan-on-command
Capture is a full re-scan and re-hash of every tracked file, on every command. There is no daemon, no watcher, and no inotify, and therefore no crash window and no stale cache. The cost is measured: comfortable to a few thousand tracked files, noticeable at 10k, sluggish past 30k (see the performance envelope).
A capture that changes the snap is recorded as its own op (verb=capture)
before the command's own op, so even the capture is undoable separately.
What gets captured
Files, directories, and the executable bit. Excluded:
- symlinks, skipped with a warning (up to 5 named individually; more
collapse into one
skipped N symlinksline); - empty directories (like git);
.hav/and.hav-ws(repo metadata), always;- anything matched by
.havignore.
On Windows, where the filesystem has no exec bit, capture preserves each tracked file's bit from history and projection skips the chmod, so a repo shared across platforms never sees phantom mode changes.
.havignore is yours
Ignore rules live in .havignore at the workspace root, in gitignore syntax.
The file is itself tracked.
hav ships no ignore rules of its own and never writes or auto-populates
the file, because what belongs in your repository is your call. The guardrails
below exist so that the absence of a .havignore is never silently expensive.
The guardrails: preview and the first-capture gate
hav preview shows what the next capture would ingest: total files and
bytes, a per-directory breakdown sorted by size, and the skipped-symlink
count. It uses the same walker and ignore rules as capture but reads nothing,
stores nothing, and records no op. Run it before the first capture on a real
project, then write .havignore from what it shows you.
The first-capture gate catches the rest. When a workspace has never captured
anything, capture counts the tree first, reading, hashing and storing nothing, and
prints first capture: N files (size). At ≥5000 files or ≥100 MB it stops and
asks proceed? [y/N], pointing at .havignore and preview. Anything but an
explicit y fails the command with nothing stored.
Later captures adding ≥5000 new files (an unignored dependency tree,
almost always) warn on stderr and point at .havignore and hav undo.
If a mistake does get through: ignore the path, let the capture age out of the
op journal, and hav sweep frees the space.
Capture resolves conflicts
Capture is conflict-aware. For each path stored as a conflict in the knot's snap:
- file bytes still equal the rendered markers → still conflicted, unchanged;
- file has no marker lines left → resolved, and the entry becomes a normal file;
- file changed but still contains markers → still conflicted, and
stshowscf-… (partially edited).
This is why there is no resolve --continue: fixing the file is the
resolution, and whichever command you run next notices.
Variants route before they store
A path carrying sealed variants routes by the workspace's active crew, and encrypts before any plaintext row exists, because the routing decision happens inside the scan. You edit what you see: with a crew active, edits at variant paths update that crew's variant, and with none, they update the public variant. The first capture that would create a public variant at a sealed-only path asks first (the publish gate), and declining stores nothing and drops nothing. A variant that never materialized here is carried through rather than recorded as a deletion. It was never checked out, so its absence means nothing.
Capture triggers the cascade
When capture changes the snap of a knot that has descendants (you are
editing an old knot), every unheld descendant re-merges automatically as
part of the same operation. See
Rewriting history.