zit 0.3.32 makes zit purge undo take the repository's write lock before it
restores anything, like every other command that writes the store. It also
moves the lock into the storage layer, so no future writer can append without
it, and adds the tests that should have caught the bug in the first place.
No artifact, tag namespace, sync message, or on-disk repository format changed.
purge undo waits its turn
A zit repository is one append-only file, and every writer takes an
exclusive lock on it before appending: commit, clone, pull, a server
receiving a push, config and user-table writes, tags, tickets, wiki and
forum posts. Two writers that are not serialised both scan the file for its
end and both append there, so the second writes on top of the first. The
overwriting frames are complete and checksum-valid, so neither zit verify
nor crash recovery can see that anything happened: one of the two writes is
simply gone.
zit purge undo restores buried artifacts by appending them, but it opened
the store without that lock. Run while a commit, a pull loop or a server
push was writing to the same repository, from this checkout or another
worktree of the same store, either the restored artifacts or the other
writer's check-in could be lost without a trace.
It now opens the store the way every other writer does, so it waits for a concurrent writer to finish rather than racing it. The same change makes it resolve a detached worktree's store, which the old direct open did not.
The regression test holds the repository's write lock, starts
zit purge undo, and checks that the store is byte-for-byte unchanged a
second and a half later. It then releases the lock and checks that the undo
completes and the restored store verifies. Against the old code, the store
changes while the lock is held and the test fails.
The lock now lives where the appends happen
The purge undo bug was possible because the lock was taken by each command
when it opened the file, and one command opened it a different way. The log
itself now takes the lock: opening a store for appending takes the exclusive
lock first, and only then reads the file's length and scans for its end. A
store opened for reading has no append path at all, so a writer that opens
the file the wrong way fails loudly instead of writing unlocked.
A source check in the test suite also fails if any code outside the one shared opener opens the repository file for writing.
Tests for the whole class, not the one bug
The deterministic simulation already swept crashes, torn writes and faulty disks, but always with a single writer, and a lock that no one contends for is never exercised. A new sweep runs two to four writers against one simulated log, interleaved step by step, through a file that models the lock as the operating system provides it. Every writer follows a real command's order: find the file's length, open the log, append, commit, close. Another writer can run between any two of those steps.
The simulated file reports an append by a handle that does not hold the lock, or an append that does not land at the file's end, at the moment it happens. Each writer must also see every artifact committed before it took the lock, and must read back what it just wrote, including deltas against other writers' artifacts. At the end, every committed artifact must be present and identical through a fresh reader. Removing the lock fails this sweep, and so does reading the file's length before taking the lock.
An end-to-end test also runs commits from two worktrees, tag writes and user table writes against one store at once, and checks that every one of them landed and the store verifies.
Compatibility
- No on-disk repository or wire format changed; Fossil RFC revision 00 remains the target.
zit purge undonow waits, without a message, while another zit process is writing to the same repository.
Qualification
zig fmt --check, zig build test, e2e, smoke, git-import-e2e,
git-export-e2e, and dst, plus the three-runner CI battery.