| Comment: | B5: an add-only merge registers its file, shows as pending, and commits
A merge whose only effect was an ADDED file was silently lost. `merge` wrote the incoming file to the working tree but never inserted it into `.zit-managed`, so the file was simultaneously on disk and untracked: `status` said "nothing to report", `extras` listed it as junk, and `commit` refused with "nothing to commit (no changes)" — dropping the merge parent that `merge` had recorded in `.zit-checkout`. The branch existed on exactly one disk and `undo` was the only escape. No new state was needed. CheckoutState.merge_parent already carries the pending merge (src/checkout.zig), runMerge already records it, and commit already consumes it as a two-token P card and clears it. The gap was entirely in the managed set and in two refusals that did not know about it. applyMerge now DERIVES the membership mutation from the same entry stream that drives the filesystem effects, rather than accepting a parallel list from the caller: a `.write` registers its path (insert is idempotent, so an ordinary edit-merge is unaffected) and a `.delete` deregisters it. One stream, one truth. Deriving it there rather than in computeAndApplyMerge also fixes the symmetric adjacent bug for free — a merge-deleted file used to stay managed and invent a phantom "missing" that commit then refused over — and covers cherrypick and backout, which share the same seam. The pre-merge set joins the in-process rollback images so a mid-apply failure restores membership along with the files and the checkout; the undo journal already snapshotted `.zit-managed`, so abort and undo deregistration fall out unchanged. The set is read twice rather than rebuilt from `old.iterate()`: the on-disk record carries a pending `mv` rename beyond the bare membership, and rebuilding would have silently dropped it. status now names a pending merge/cherrypick/backout in its header block. A merge whose file effects are already materialized was otherwise indistinguishable from a clean tree, with no way to see that a second parent was queued. commit's no-op refusal no longer fires while a merge or special op is pending. A convergent-content merge — both sides making the same edit — composes to F cards identical to the parent's, so `unchanged` went true and the pending merge wedged with no commit path at all. The P/Q card is itself the change. Test-expectation change: the new "zit add of a merge-added file is idempotent" case asserted the "already tracked" notice on stdout. That notice has always been a stderr diagnostic (three shipped e2e cases assert it there); the test had the stream wrong, not the product. The assertion's intent — add sees the merge-registered file as already tracked and does not re-add it — is unchanged. No technique was borrowed here: the mechanism rides zit's existing applyMerge, ManagedSet and journal seams. Fossil's vmerge/vfile registration was consulted as an analog for the SHAPE of the problem only, and none of its code or algorithm is reproduced. Verified in this worktree: fmt, build, zig build test, full zig build e2e (420 tests, all eight REGRESSION B5 cases green, contract-partner undo guard still green), and smoke S1-S6. The blocker repro was also driven by hand against the rebuilt binary: merge reports 1 file changed, status shows "pending merge with <hash>" and "added feat.txt", extras is empty, commit lands, and info tip shows both parents. |
|---|---|
| Timelines: | family | ancestors | descendants | both | blockers |
| Files: | files | file ages | folders |
| SHA3-256: |
de05152541a83748861b77a338772031 |
| User & Date: | dmestas on 2026-07-19 15:09:07.784 |
| Other Links: | branch diff | manifest | tags |
|
2026-07-19
| ||
| 15:13 |
Merge blockers: an add-only merge registers its file and commits (B5)
A merge whose only effect was an ADDED file was silently lost: the file landed on disk but never in .zit-managed, so status said "nothing to report", extras listed it as junk, and commit refused "nothing to commit (no changes)" while dropping the merge parent. The branch existed on one disk and undo was the only escape. No new state was needed — CheckoutState.merge_parent already carried the pending merge and commit already consumed it as a two-token P card. The gap was the managed set and two refusals that did not know about it. applyMerge now derives the membership mutation from the same entry stream that drives the filesystem effects, which also fixes the symmetric bug for free (a merge-deleted file used to stay managed and invent a phantom "missing") and covers cherrypick/backout, which share the seam. status names a pending merge/cherrypick/backout. commit's no-op refusal no longer fires over a pending merge, so a convergent-content merge can no longer wedge the tree. One test expectation changed: the new add-idempotence case asserted the "already tracked" notice on stdout; it has always been a stderr diagnostic (three shipped e2e cases assert it there). The test had the stream wrong, not the product. Merged tree re-verified on trunk rather than inherited from the branch: zig fmt --check ., zig build, zig build test, full zig build e2e (420 tests, all eight REGRESSION B5 cases green including the contract-partner undo guard) and smoke S1-S6 all exit 0. fossil extras --dotfiles is empty. The blocker repro was also driven by hand against the rebuilt binary end to end. Landed alongside, not merged here: another session committed B3 (1aeb531cac) to blockers off the OLD blockers tip, leaving that branch with two leaves. This increment branched off trunk, so the two do not interact and B3 remains that session's to land. check-in: 99f4895c04 user: dmestas tags: trunk | |
| 15:09 |
B5: an add-only merge registers its file, shows as pending, and commits
A merge whose only effect was an ADDED file was silently lost. `merge` wrote the incoming file to the working tree but never inserted it into `.zit-managed`, so the file was simultaneously on disk and untracked: `status` said "nothing to report", `extras` listed it as junk, and `commit` refused with "nothing to commit (no changes)" — dropping the merge parent that `merge` had recorded in `.zit-checkout`. The branch existed on exactly one disk and `undo` was the only escape. No new state was needed. CheckoutState.merge_parent already carries the pending merge (src/checkout.zig), runMerge already records it, and commit already consumes it as a two-token P card and clears it. The gap was entirely in the managed set and in two refusals that did not know about it. applyMerge now DERIVES the membership mutation from the same entry stream that drives the filesystem effects, rather than accepting a parallel list from the caller: a `.write` registers its path (insert is idempotent, so an ordinary edit-merge is unaffected) and a `.delete` deregisters it. One stream, one truth. Deriving it there rather than in computeAndApplyMerge also fixes the symmetric adjacent bug for free — a merge-deleted file used to stay managed and invent a phantom "missing" that commit then refused over — and covers cherrypick and backout, which share the same seam. The pre-merge set joins the in-process rollback images so a mid-apply failure restores membership along with the files and the checkout; the undo journal already snapshotted `.zit-managed`, so abort and undo deregistration fall out unchanged. The set is read twice rather than rebuilt from `old.iterate()`: the on-disk record carries a pending `mv` rename beyond the bare membership, and rebuilding would have silently dropped it. status now names a pending merge/cherrypick/backout in its header block. A merge whose file effects are already materialized was otherwise indistinguishable from a clean tree, with no way to see that a second parent was queued. commit's no-op refusal no longer fires while a merge or special op is pending. A convergent-content merge — both sides making the same edit — composes to F cards identical to the parent's, so `unchanged` went true and the pending merge wedged with no commit path at all. The P/Q card is itself the change. Test-expectation change: the new "zit add of a merge-added file is idempotent" case asserted the "already tracked" notice on stdout. That notice has always been a stderr diagnostic (three shipped e2e cases assert it there); the test had the stream wrong, not the product. The assertion's intent — add sees the merge-registered file as already tracked and does not re-add it — is unchanged. No technique was borrowed here: the mechanism rides zit's existing applyMerge, ManagedSet and journal seams. Fossil's vmerge/vfile registration was consulted as an analog for the SHAPE of the problem only, and none of its code or algorithm is reproduced. Verified in this worktree: fmt, build, zig build test, full zig build e2e (420 tests, all eight REGRESSION B5 cases green, contract-partner undo guard still green), and smoke S1-S6. The blocker repro was also driven by hand against the rebuilt binary: merge reports 1 file changed, status shows "pending merge with <hash>" and "added feat.txt", extras is empty, commit lands, and info tip shows both parents. closed check-in: de05152541 user: dmestas tags: blockers | |
| 14:32 |
Merge blockers: resolve same-mtime ties by DAG ancestry (B1/B2)
Three commits inside one second made 'zit cat f.txt tip' return the root's content and 'trunk' the middle's, while 'zit leaves' named the newest — the binary contradicting itself, exit 0, no warning. The diagnosis that opened the campaign was wrong and is worth recording: it blamed a single lexicographic-hash tiebreak in mergeBaseOf. Building the equal-mtime chain in BOTH hash orderings produced identical failures, which ruled hash order out as the mechanism. There were three independent tie sites — tipForm settled on events insertion order, resolveName on tagxref iteration order, mergeBaseOf on artifact hash. A fix confined to the originally-blamed line would have left tip and trunk still wrong. Equal event.mtime is now settled by ancestry: a descendant beats its ancestor. Genuinely concurrent nodes keep a deterministic least-hash fallback — hash order between unrelated leaves was never the defect. Timeline gets a second pass re-ordering equal-mtime runs descendants-first. Ride-alongs on the same surface: 'update' accepts symbolic names (SF1), and a 'tip' naming a non-leaf is now an in-binary assertion (SF5) — the tripwire that would have caught this originally. Merged tree re-verified on trunk: fmt, build, test, e2e, dst 6x1000 all green. Remote CI is unavailable (self-hosted runner offline); the local battery is the gate per operator direction. check-in: bc7b914316 user: dmestas tags: trunk | |