Skip to content
zit Timeline
Login

Timeline

Parents and children of check-in [33a2ceb6d1]

2026-07-19
21:36
timeline: read fossil-legacy check-in manifests instead of dropping them

Repository.manifest parsed check-in manifests through the strict reference parser, which rejects fossil's own 2008 cvs2fossil-era manifests: their F cards carry a dangling trailing space after the hash, where the §4.2 perm token (1*tchar) permits none. On a large fossil-origin repository this made `zit timeline` stop at the first such check-in and exit non-zero, silently losing that check-in and every older one — a functional loss of history, surfaced as a misleading "input/output error".

Parse through parseForImport, the same read-path interop the diff baseline already relies on: the strict parser still runs first and stays the sole grammar oracle; the trailing-separator tolerance engages only when an otherwise well-framed, Z-valid artifact is rejected. OwnedManifest now also owns the normalized copy the manifest borrows when that tolerance fires. closed check-in: b503efb9f2 user: dmestas tags: iss7

20:37
Merge iss8: pull converges past the server's unclustered window

A fossil server folds artifacts older than its ~100-entry unclustered window into clusters and stops announcing those members with igot -- the M cards are the announcement. The client stored a received cluster as ordinary content and read nothing from it, so a pull past that window fetched only the cluster and then terminated, silently, having gained nothing. Cluster membership now folds into the phantom set, so the next round gimmes the members. A pull that reaches a clean terminal still lacking artifacts the peer announced now exits nonzero, rather than reporting a success that leaves a repository quietly out of date. check-in: 8d3ebc0c9d user: dmestas tags: trunk

19:15
Add -R/--repo and -n N to the read verbs

-R/--repo PATH names the repository to read instead of the one found by walking up from the current directory. It is lifted out of argv before verb dispatch, so one implementation serves every read verb and each verb's grammar is unchanged. Resolution requires only the .zit store, not the .zit-checkout discovery also demands: reading a repository with no working tree is the point of the option. PATH may name the directory holding .zit or the .zit file itself. Verbs that read or write a working tree refuse -R by name; accepting it and silently operating on the current checkout is the failure worth a diagnostic to prevent. The spelling matches fossil.

-n N (alias --limit N) shows the N most recent entries on timeline and log; 0 means all, as in fossil. This deliberately overloads -n, which remains --dry-run on add and rm, just as fossil overloads it between timeline and clean. A non-numeric or negative value is refused.

An expectation asserting log rejected -n is inverted: it recorded the absence of the limit, not a rule about the grammar.

timeline still defaults to the whole listing, not fossil's 20 entries. check-in: b25e01be95 user: dmestas tags: iss12

19:14
sync: absorb cluster membership so pull converges past the unclustered window

A fossil server folds artifacts past its ~100-entry unclustered window into cluster artifacts and stops announcing them with igot: the M cards become the announcement. The client stored a received cluster as ordinary content and read nothing from it, so no phantom appeared, the 3.7(a) continuation saw an empty table, and the exchange terminated after one round having fetched only the cluster. Repository size, not delta size, was the trigger; clone converges on the 8.2 RID cursor, not on gimme, so it was unaffected.

Client.absorbCluster folds the members into the phantom set with the disposition an igot for each would produce, so the next round gimmes them. Membership is validated before any is applied; a clone skips it for the reason it announces no igot. On the cfile path the container inflates into a scratch buffer, not through store.content, which would memoise plaintext.

Client.unresolvedPhantoms counts announced-but-unobtained artifacts; pull exits nonzero when a run reaches a clean terminal with any outstanding, so a pull that stops short of what the peer holds is no longer reported as success. closed check-in: 5535de0f45 user: dmestas tags: iss8

19:07
open: add --manifest[=r,u,t] for the working-tree manifest files

Repositories that enable fossil's `manifest` setting — fossil, althttpd, pikchr — expect `manifest`, `manifest.uuid` and `manifest.tags` in the checkout; their builds read them to stamp a version. A zit checkout could not produce them, so such a build failed or, worse, succeeded with no version embedded.

`zit open --manifest` writes them; the bare flag means `ru`, a value picks any combination of `r` (the check-in artifact), `u` (its hash) and `t` (its branch and symbolic tags). The choice is recorded in the checkout record, so every later `update` refreshes the files. Absent flag writes nothing.

The selection is explicit rather than read from the cloned repository because the two places fossil keeps it disagree and only one syncs: althttpd's versioned setting says `on`, its local config `rtu`. A checkout reading the setting could diverge from a fossil checkout carrying a local override while appearing to promise fidelity.

Contents match fossil byte for byte, including the sterilizing comment it appends to `manifest`. A tracked file of one of those names is never replaced. closed check-in: dc36496c3b user: dmestas tags: iss6

18:46
Add `zit stat`: the store's own byte accounting and artifact roll

The store could not be interrogated about its contents: no way to ask how many artifacts it holds, which ones, or how many of its bytes are artifact content rather than framing. `zit stat` answers that from the frames themselves, emitting `field value` lines: store-bytes, header-bytes, content-bytes, frame-overhead-bytes, config-bytes, artifacts, full-form, deltas. The byte fields account for the file exactly.

content-bytes and store-bytes stay separate on purpose. Content is measured by summing each artifact frame's payload length; it is never derived from the `.zit` size, which also carries the store header, a header per frame, and alignment padding. A size-derived figure would look like a result and be a fabrication, so the overhead separating the two is reported alongside and stays checkable.

`--artifacts` lists the stored hashes one per line in ascending order, so two stores' rolls compare with `comm`; `--type=full|delta` narrows by storage form, read from the index the open already built.

The frame walk and its accounting live in `storestat`, shared with the storage benchmark, so the two cannot drift. check-in: 7c2262c837 user: dmestas tags: iss4

17:32
Merge blockers: a failed mv changes nothing, and its recovery is exact (B4)

mv recorded a rename in the managed set and persisted it before attempting the filesystem move, with no rollback. A move into a nonexistent directory left the set naming a path that held no file: status invented a missing entry, commit refused over it, and the recovery mv --help documents did not work.

mv now validates that the destination's parent exists and refuses before touching anything, so the failing case records nothing.

Two further defects on the same path. An inverse rename replaced the pending record instead of dropping it, so 'mv a b' followed by 'mv b a' left a b-to-a rename queued; the next commit carrying a real change would then emit an old-name F card asserting the file was formerly named b, a name never committed. That is false provenance in a permanent artifact, produced by the exact sequence the help text advertises as recovery. mvRecord now nets an inverse rename back out.

The error path printed raw Zig error names — a user saw 'AccessDenied' where clean and init already print 'permission denied' through reasonOf. It now uses the same mapping.

A residual window remains: a crash between the move and the record write is not atomic, documented at the site. Full working-tree crash consistency is a separate concern. check-in: 33a2ceb6d1 user: dmestas tags: trunk

17:27
mv: a reversal cancels the rename record instead of replacing it

`mv --help` advertises `zit mv NEW OLD` as the way to undo a rename, so it is the sequence a user is most likely to run. It left the pending record replaced rather than cleared: OLD->NEW->OLD queued a §4.7.6 old-name F card asserting OLD was formerly NEW -- a name no manifest has ever carried. The next commit with any real change would write that false claim about the past into a permanent artifact, where it cannot be taken back. A rename and its inverse are a no-op, so the pair now nets the record out.

This survived the guard because the guard's oracle was indirect: commit reports "nothing to commit" on CONTENT alone, so a stranded record passes it silently and surfaces only at the next commit that carries a change. The record is now read back directly.

mv's move errors render through `reasonOf` rather than `@errorName`, so EACCES reads "permission denied" instead of "AccessDenied". This was deferred as errno-sweep work, but the mapping already exists and `init` and `clean` print through it -- reuse, not new scope. A raw Zig error name is an internal identifier, not a diagnostic. closed check-in: 7235a86bcd user: dmestas tags: blockers

16:36
Merge blockers: `zit tag add NAME CHECKIN` mints a SINGLETON sym card (B6)

Lands the B6 increment (d8e5e811) on trunk. The fix itself is unchanged from that commit and its full reasoning is recorded there; this message covers the landing, because the merge was not a formality.

WHAT B6 FIXES

`tag` was read-only. `tag list` was the only subcommand, so a repository could not be given a name for a check-in at all. Unlike the other five blockers this was not a wrong answer — the verb told the truth about what existed — but the capability was simply absent. `zit tag add NAME CHECKIN` now mints one CONTROL artifact carrying a single `T +sym-NAME` card, targeting the resolved check-in's 64-hex hash.

THE SIGN IS THE WHOLE CORRECTNESS ARGUMENT

The card is a SINGLETON (`+`), not a propagating (`*`) tag. `resolveName` (src/repostate.zig:891) picks the max-`event.mtime` check-in among ALL active `sym-` rows, so a propagating `*sym-v1` would be inherited by every descendant and then resolve to the NEWEST of them — the name would silently drift off the check-in the user tagged, which is the same class of wrong answer as B1. `applyTagOps` (src/repostate.zig:727) gates propagation on `op.type != .singleton`, so a `+` card pins the name with ZERO repostate changes. The contrast with a branch is deliberate: a *branch* sym propagates precisely because descendants must inherit it; a *tag* sym does not, precisely because they must not.

CONFLICT RESOLUTION, STATED

One conflict, src/e2e.zig, positional rather than semantic: trunk's B5 block and B6's block are both appended at the end of the file over an empty common ancestor. Fossil again emitted the five-part conflict rather than the usual four — a "####### SUGGESTED CONFLICT RESOLUTION follows" section carrying its own proposed merge sits between the local copy and the ancestor section. Splitting only on the four familiar markers swallows that separator into the kept text, which then compiles as garbage; B3's landing recorded the same trap and it recurred here verbatim.

The two candidate sections were compared before choosing: the SUGGESTED section and the MERGED IN section are byte-identical (257 lines each), and the common ancestor is a single empty line. The resolution keeps trunk's 8 B5 tests plus exactly one copy of B6's 5-test block. Verified post-resolution: no residual marker of any of the five kinds, 432 tests in the file, no duplicated test name, and the B3 (5), B5 (8) and B6 (7) blocks all present at their expected counts. One further edit was needed — the splice left a doubled blank line at the seam, which `zig fmt` flagged; the reformat is that one blank line and nothing else.

TEST EVIDENCE ON THE MERGED TREE

Re-verified on trunk after the merge, not inherited from the branch: `zig fmt --check .`, `zig build`, `zig build test`, `zig build e2e` (432/432, no filtering), `zig build dst` (6 seeds x 1000 steps) and `zig build smoke` (S1-S6) each exit 0. `fossil extras --dotfiles` is empty.

The blocker repro was also driven by hand against the rebuilt merged binary rather than inferred from the source: `zit tag add v1 tip` exits 0 and prints the artifact hash; after a second commit `zit cat a.txt v1` is the tagged content while `zit cat a.txt tip` is the newer one, and `zit tag list tip` does NOT carry v1 — the singleton pins and is not inherited. `zit tag add v1` alone is a usage error at exit 2, and `zit tag add v2 nosuchcheckin` exits 1 naming the check-in and leaves no tag behind.

CHANGED EXPECTATIONS (all three encoded the missing capability, nothing else)

cli tag B-tag-7 (src/cli.zig) and cli tag B-tag-13 (src/e2e.zig) used the literal token `add` as their stand-in for "not a subcommand". `add` IS one now, so both use `cancel`, a word that still is not — cancel/remove semantics stay deferred to 0.2. cli tag B-tag-11's verbatim `tag_help_expected` gains the `add` usage line and its paragraph, because `tag --help` must teach the new grammar. The diff was audited for other expectation changes and there are none.

RECORDED, NOT ASSUMED: branch creation shares no read-only gap. It is already served by `commit --branch`, which mints the propagating branch/sym pair directly on the check-in manifest (src/cli.zig `composeCommitTags`), so there is no `branch add` to write.

No borrowed technique: the singleton-sign argument and the `mintControlArtifact` extraction are original to this change. No performance claim is made. check-in: a5b6db7a8c user: dmestas tags: trunk