Artifact d7b6ec8caa95993ce01c4ed9ddd0cda327db823bdbde4e27dff4966591be25f5:
- File
src/extras.zig
-
2026-07-19 15:07:32
— part of check-in
[1aeb531cac]
on branch blockers
— add/addremove/commit: refuse symlinks consistently (blocker B3)
A symlink anywhere in the checkout permanently wedged `commit`. `extras.zig` classifies a symlink as an extra (B-extras-9), and the recursive `add DIR` / `addremove` paths reuse that sweep as their add set without re-applying the kind test that explicit `zit add sym.txt` and `commit` both apply. The recursive path therefore TRACKED exactly what the direct path REFUSED: after `zit add .`, every subsequent `zit commit` died with "cannot commit 'sym.txt': not a regular file", `zit status` called an on-disk file missing, and the only way out (`zit rm sym.txt`) was named nowhere. `add .` is the documented whole-tree workflow, so any repo containing a symlink was stuck.
WHAT CHANGED
Consistent refusal: 0.1.0 does not version symlinks, on every path.
1. `extras.Extras` entries widen from a bare path to `Entry{path, is_symlink}`. The sweep already lstat-classified every entry (`resolveKind`); the verdict now rides out on the entry instead of being re-derived by callers. That is the point of the shape: `add` and `addremove` do NOT re-stat swept items, so the two paths cannot drift from the deep module's classification again — which is the mechanism of this bug. Ordering is unchanged (sort is by `path`; `is_symlink` never participates), and every postcondition assert is preserved.
2. The five `items()` consumers split by role. `extras` and `clean` ignore the tag — enumerating verbs do not judge, and a symlink stays a listable, deletable extra. `add`'s directory sweep and `addremove`'s insert loop filter on it.
3. `classifyPath`'s catch-all `else => .not_regular` splits: `.sym_link` gets its own `Outcome` and the wording `symlink (not versioned in 0.1.0)`; a fifo/socket/device keeps "not a regular file". Those are not the same refusal and must not read as one. The wording is one shared const so the explicit refusal and the recursive skip notice cannot diverge.
4. `commit`'s `Capture.not_regular` carries whether the entry was a symlink, and `commitNotRegular` renders the recovery for that case: "run 'zit rm PATH' to stop tracking it". This is how repos already wedged by a pre-fix `add .` get out; there is no migration code and none is needed.
REPORTING DECISION (stated, not implied)
`add` prints each skip to stderr, alongside its existing already-tracked notice: add's contract is per-argument feedback. `addremove` withholds symlinks SILENTLY. Its report is a two-channel ADDED/REMOVED ledger of state transitions, and a skipped symlink is a non-transition; growing a third notice channel there would change the shape of a report other tooling reads. `zit extras` is the verb that shows the symlink is still present and untracked.
`status` needed no change: post-fix the symlink is simply untracked.
TESTS
Five e2e regressions, marker `// REGRESSION B3:`: the breakdown doc's repro chain verbatim (init/add ./commit/status/extras); an `addremove` variant with symlinks sorting BOTH before and after the regular file the sweep also yields, so a filter that latched off the previous item's kind fails one of the two; explicit `add sym.txt` refusing with the symlink wording and tracking nothing; the wedge-recovery chain (pre-tracked symlink -> commit names `zit rm` -> rm -> commit succeeds -> status clean); and a `clean --dry-run` contract-partner guard that was green before the fix and stays green through it, proving clean still lists a symlink as an extra after the `Extras` type change. A unit assertion in `extras.zig` pins the tag itself, and `commitNotRegular`'s renderer test covers both branches.
Two pre-existing e2e expectations changed, both because they encoded the bug or its wording:
- `cli addremove B-addremove-2` asserted the ADDED set was byte-identical to `zit extras`, symlink included. That identity IS the defect. It now asserts the add set is the extras set MINUS symlinks, and additionally that the symlink remains an extra. - `cli add B-add-5` asserted the fifo-generic wording for a named symlink. It now asserts the symlink-specific wording.
Four `commit` invocations inside the new B3 tests use `runCommitEnv` with an explicit user rather than bare `runZit`; the isolated e2e environment has no resolvable author, which is a harness requirement, not a behavior change.
TEST EVIDENCE
In this worktree at the committed tree: `zig fmt --check .`, `zig build`, `zig build test`, `zig build e2e` (417 tests) and `zig build dst` each exit 0 — full battery, no filtering. Pre-fix the four B3 repro tests failed and the `clean --dry-run` guard passed, as designed.
No borrowed technique: the entry-tag shape is original to this change. No performance claim is made. (user: dmestas, size: 28787) [annotate] [blame] [check-ins using]
-
2026-07-19 15:31:24
— part of check-in
[01dc9f8e8a]
on branch trunk
— Merge blockers: refuse symlinks consistently across add/addremove/commit (B3)
Lands the B3 increment (1aeb531cac) on trunk. The fix itself is unchanged from that commit and its reasoning is recorded there; this message covers the landing, because the merge was not a formality.
WHAT B3 FIXES
A symlink anywhere in the checkout permanently wedged `commit`. `extras.zig` classified a symlink as an extra, and the recursive `add DIR` / `addremove` paths reused that sweep as their add set without re-applying the kind test that explicit `zit add sym.txt` and `commit` both apply. The recursive path tracked exactly what the direct path refused, so after `zit add .` every subsequent `zit commit` died with "cannot commit 'sym.txt': not a regular file" and the only way out (`zit rm sym.txt`) was named nowhere. Consistent refusal is the contract: 0.1.0 does not version symlinks on any path.
WHY IT LANDED SEPARATELY
B3 was committed to `blockers` off the old branch tip and never reached trunk; trunk meanwhile took B1, B5 and a CI change. The sibling B6 commit (d8e5e811) had already absorbed B3 into its own tree, so B3 would eventually have arrived on trunk bundled inside B6. Landing it on its own keeps each increment gated and merged on its own evidence rather than riding in on another's, and it shrinks the diff B6 still has to land.
CONFLICT RESOLUTION, STATED
One conflict, src/e2e.zig, positional rather than semantic: trunk's B5 block and B3's block are both appended at the end of the file, over an empty common ancestor. Both blocks are kept, one copy each.
Worth recording because it is a trap: fossil emitted a five-part conflict, not the usual four. Between the local copy and the ancestor section it inserted a "####### SUGGESTED CONFLICT RESOLUTION follows" section carrying its own proposed merge. Splitting only on the four familiar markers silently swallows that separator line into the kept text, which then compiles as garbage — it surfaced here as `zig fmt` and the compiler both failing at that exact line, not as anything that looked like a merge problem. The resolution keeps the local copy plus fossil's suggested section (B5 then B3) and drops the duplicate merged-in copy, which was verified to be a strict substring of what was kept. Post-resolution the file holds 13 tests in that region with no duplicated test name and no residual marker of any of the five kinds.
TEST EVIDENCE ON THE MERGED TREE
Re-verified on trunk after absorbing the latest sibling (861b223e3c), not inherited from the branch: `zig fmt --check .`, `zig build`, `zig build test`, `zig build e2e`, `zig build dst` (6 seeds x 1000 steps) and `zig build smoke` (S1-S6) each exit 0. Full battery, no filtering. `fossil extras --dotfiles` is empty.
The B3 repro was also driven by hand against the rebuilt binary: `zit add .` skips sym.txt and says so on stderr, `zit commit` succeeds, `zit status` reports nothing, `zit extras` lists sym.txt, explicit `zit add sym.txt` refuses with the symlink wording at exit 1, and `addremove` withholds it silently.
No borrowed technique. No performance claim is made. (user: dmestas, size: 28787) [annotate] [blame] [check-ins using]
-
2026-07-19 15:07:32
— part of check-in
[1aeb531cac]
on branch blockers
— add/addremove/commit: refuse symlinks consistently (blocker B3)