Skip to content
zit Timeline
Login

Timeline

50 descendants of 4290d14e17ab79e3

2026-08-24
14:47
merge readpath-141: diff and artifact read through the image path (#141) check-in: 9bcc83b530 user: dmestas tags: trunk
14:36
diff and artifact: read through the image path, not a whole-store derive

Both opened `.full`, so each invocation re-derived the entire repository view before reading anything. The cost tracked the STORE, not the work: a one-line diff on a 302MB store took 0.33s, and cost the same whether the diff was 13 lines or 4126. diff read two file lists -- exactly what ls reads at 0.04s -- and artifact never touched the derived view at all, resolving through logStore alone.

diff inherited `.full` by omission: it called Repository.open directly instead of going through main.openRepo, which is where every other read verb chooses a mode. Nothing failed, so nothing caught it.

fossil 112MB 0.06s -> 0.01s tk 240MB 0.14s -> 0.03s sqlite 236MB 0.25s -> 0.03s tcl 302MB 0.33s -> 0.04s

Output byte-identical on all four.

readpath_test pins which verbs take which path, and pins the comment justifying each deliberate `.full` -- timeline reads the merge mark off adjacency, annotate walks ancestry. An unexplained `.full` is indistinguishable from this bug. grep is named there as an open question rather than pinned, since it also wires --no-cache, which has to move with the mode.

The OpenOptions comment claimed ls, cat, info, branch and tag were `.full`. All five have been fast_read since #98; reading it was how this stayed hidden.

Fixes #141 closed check-in: e845821a65 user: dmestas tags: readpath-141

11:27
merge update-dirty-tree-135: carry uncommitted work across the move (#135) check-in: 5f6402dd84 user: dmestas tags: trunk
11:15
merge landing-title-138: first line of -m is the landing title (#138) check-in: da4fa8dcbe user: dmestas tags: trunk
11:06
landing open: the first line of -m is the title, the rest is the prose

The whole message went into the H card and the body both, so a multi-paragraph -m minted a multi-paragraph heading and carried the same text twice. An H card is one token (§4.7.7). The split is at the first newline, not the first blank line, so a two-line message cannot mint a two-line title either.

ls and show read only a title's first line: landings minted before this are immutable, so a reader has to cope with them, not merely stop making more. A message with no title line is refused before the branch lookups and the store scan.

Fixes #138 closed check-in: 3e565c1caa user: dmestas tags: landing-title-138

10:16
update: carry uncommitted work across the move instead of refusing it

A dirty tree is now three-way merged (current check-in, working file, target) rather than rejected, so a branch tip that advanced under uncommitted work can be joined. Previously update, merge and stash all refused that state, leaving commit --allow-fork as the only exit -- the guard that keeps forks deliberate was manufacturing them. The fork guard itself is unchanged.

applyCheckinWith scans the tree once and calls back for the merge policy; handing carries in instead would scan every clean update twice, which cost a 20k-file no-op update 63%.

Paths that cannot merge either way (type change, edited here / deleted there) are named and refused before anything mutates.

Fixes #135 closed check-in: 7fcc9b5ee1 user: dmestas tags: update-dirty-tree-135

02:26
merge merge-probe: mergeability for a caller-named pair (#134) check-in: 83734fcc3d user: dmestas tags: trunk
02:17
merge --probe: mergeability for a pair the caller names (#134)

landing show answers the same question but must find the record first, which is a full-store walk. A consumer with its own index already knows the record and both tips; --probe lets it ask about the pair and pay only for the merge. Release build on a 20MB store: 0.01s against 0.90s.

Writes nothing, never autosyncs, and exits 0 whatever the answer -- 'has conflicts' is an answer, not a failure, so a caller reads the line rather than the status. Being a read it takes -R, and then --into is required because there is no checkout to supply the other side.

The probe moves out of landing and next to the merge engine: one implementation, three callers. A real merge now refuses -R by name instead of failing as a discovery miss that tells you to pass the flag you just passed. closed check-in: aff1d04310 user: dmestas tags: merge-probe

01:35
merge diff-ignore-ws: zit diff -w ignores whitespace-only changes (#130) check-in: f863b8d6ef user: dmestas tags: trunk
00:52
diff: -w ignores whitespace-only changes (#130)

Lines differing only in whitespace compare equal, so a reindent or a CRLF/LF normalization produces no hunks and drops the file from --brief entirely. Comparison only: a line that does survive is emitted with its original bytes.

--brief reads content under -w, which it otherwise never does, and runs the same unified() the hunk path runs so the two cannot disagree about whether a file changed. A no-hunk result now claims 'mode changed' only on real byte equality -- with -w it also means a difference that was ignored. closed check-in: 23784ef673 user: dmestas tags: diff-ignore-ws

00:41
merge ncard-rule: fossil's per-type N card write rule (#136) check-in: 7f9f9f8927 user: dmestas tags: trunk
00:31
N card: follow fossil's per-type write rule (#136)

landing hardcoded N text/plain with no way to say otherwise, overriding the text/x-fossil-wiki default of artifact-format 4.7.12 on every record it wrote. It now takes --mimetype and, minting a forum artifact, follows forum's rule.

The wider bug was in what landing copied. Verified against fossil's source: wiki (wiki.c:2084) and forum (forum.c:1784) write N only when the mimetype is present, non-empty and NOT the default; technote (event.c:284) writes it whenever non-empty, with no suppression. zit wrote it unconditionally for all three, so an explicit --mimetype text/x-fossil-wiki minted different bytes, and a different artifact id, than fossil produces from the same input.

The draft states the READ default, not when the card is written. Only the source shows the suppression, and only the source shows that the three types disagree -- so tests pin the divergence rather than tidying it away. closed check-in: baa15c3362 user: dmestas tags: ncard-rule

2026-08-23
23:47
fossil_convert: mint FLAT when the names do not partition (#126)

Committed under a wrong description, corrected here. The message claimed this silenced an unused parameter and left two tests failing; it does neither. The content is the finished #126 fix -- appendRun honours flat, materializing the whole effective entry set rather than falling through to the S run -- and the suite is green.

The stub I described was a transient reversion in the shared checkout, made to prove the new regression test fails without the fix. It was restored before I committed. fossil commit auto-syncs, so my commit captured the restored tree while my message described the tree I had read minutes earlier.

Authored by the #126 work, not by this commit's message. check-in: 98ddd81b75 user: dmestas tags: trunk

23:32
merge landing-doc: correct the cost note, drop the forge name check-in: 95ea2cb6b1 user: dmestas tags: trunk
23:25
landing: correct the cost note, and stop naming a particular forge

The module doc claimed stores of this size read in milliseconds. They do not -- a 117MB mirror scanned for 62 seconds. The claim was carried over from a WRITE path taken once per ticket change onto a READ path taken per view, where neither the frequency nor the scale held. Removed rather than softened, with what the scan actually costs written down: still linear in store bytes, which the constant-factor fix did not change.

The doc also named quarry throughout. zit provides the hooks; what is built on them is not zit's business, and naming one forge in the code reads as though it were. closed check-in: 78d56a2177 user: dmestas tags: landing-doc

22:46
merge landing-scan: the fold stops parsing every blob check-in: 0ef543c27a user: dmestas tags: trunk
22:36
landing: stop parsing every blob to find the records

The fold called artifact.parse on every artifact in the store and let it fail on the content blobs, which are most of a real store's bytes -- and a parse attempt pays a whole-bytes Z checksum before rejecting one. It now applies the reject-fast pre-screen frames --structural already uses, and reuses one plaintext buffer instead of allocating per artifact.

zit landing ls, release build: quarry 1.06s -> 0.15s, zit 2.19s -> 0.31s, libfossil 8.49s -> 0.91s. A forum-shaped store is unchanged, because there is nothing to reject there; it was never the slow case.

The screen only rejects. Every accept still takes the full parse, so nothing the fold would refuse can be admitted by being shaped like a card. closed check-in: 8776c0e5bb user: dmestas tags: landing-scan

21:50
merge landing-show: the check-ins being proposed, and the thread check-in: 54badbba6a user: dmestas tags: trunk
21:41
landing show: list the check-ins being proposed and the thread

A reviewer decides from the commits before they look at a diff, and a record whose discussion the display omits is one people hold somewhere else. Both were already computed: the fold walks every reply to derive state, and the ancestry query answers the rest -- so this prints what was read and thrown away.

Ordering comes from ancestryLinearized, not mtime: two check-ins made in the same second must not invert, and the verb owns no ancestry ordering of its own. A state change renders as an event rather than a remark, so the on-the-wire marker never reaches a reader. Both lists are bounded and say when they cut. closed check-in: cff154d675 user: dmestas tags: landing-show

20:55
merge landing-bare-store: mergeability probe and bare-store accept

zit landing show reports whether a branch would land cleanly, read from the store without writing anything. zit landing accept -R lands one with no checkout, merging in a working area it creates and removes, and refuses a conflicting landing with exit 5 and nothing written. check-in: d2bbc4f459 user: dmestas tags: trunk

20:45
landing: accept against a bare store, refusing conflicts

accept -R merges in a working area it creates and removes, materializing only the files both sides changed -- sparse-cone 4.4 resolves the rest into the record without writing them. The check-in is minted by the same commit path a checkout uses, so its F, P and R cards match a dense merge exactly.

A landing that does not merge cleanly exits 5 with the store byte-unchanged and each conflicting path named. A forge never holds a half-finished merge and never commits conflict markers; the author resolves it from their own checkout.

runMergeIn/runCommitIn take the checkout as a parameter instead of reading the process's cwd, which is what makes an ephemeral tree addressable without mutating global state. closed check-in: 8951bc9d1e user: dmestas tags: landing-bare-store

19:41
landing: report mergeability from the store alone

show answers whether a branch would land cleanly with no working tree and without writing anything, so a forge can flag conflicts before anyone accepts. It calls merge3.classifyFile -- the same classifier the working-tree merge runs -- so the verdict agrees with the merge rather than being a second opinion. Content-level, because hash-level resolution refuses any file touched on both sides, which is every long-lived branch. check-in: dea1cc66d3 user: dmestas tags: landing-bare-store

13:30
landing: propose a branch for trunk as a reviewable record

A landing is a forum thread plus a raw tag on the check-in that lands, so a stock fossil clone reads all of it and no new artifact kind is minted. open/ls/show/comment/accept/reject; accept merges and commits first, then records the outcome, so a partial failure leaves an open landing over merged work rather than an accepted record over an unmerged branch. check-in: c9ff7a71ea user: dmestas tags: trunk

00:40
docs: catch the reference up with this line of changes

pack no longer re-encodes profitable deltas and answers immediately on an unchanged store -- which also means a no-op pack checks no integrity, so the reference now says verify is the pass that always reads everything. stat reports the user table; verify's untouched-row list includes user rows. caching.html gains the treestat and packed memos, and the log-scan index tier says what it actually carries now. check-in: 87b58a3943 user: dmestas tags: trunk

2026-08-22
21:07
config pull user: adopt a fossil peer's user table

The client can request /user and latch its rows, and 'zit config pull user --user=NAME --password=' applies them -- merging by default, --overwrite to replace, fossil's own two modes. Credentials are required in practice because fossil serves the area to an administrator alone; without them the peer sends nothing and the verb says why instead of reporting empty success. zit's own serve does not emit /user: it has no capability model to gate it with. Smoke S12 drives a real fossil server for both halves (#125). check-in: 2170688c47 user: dmestas tags: trunk

17:29
users: carry fossil's user table, and read it

A .user frame kind holds login/pw/cap/info/mtime rows, repo-local like shun and folded newest-mtime-wins; pack carries the folded table and the scan index (v2) carries it too. 'zit config import|merge FILE' applies a fossil configuration export -- whose cards are byte-identical to the sync protocol's -- so a repository's users cross with no SQLite reader and no server. 'zit users' prints them; the password hash needs --raw. Caps are opaque and verbatim (#124). check-in: 709c457d95 user: dmestas tags: trunk

14:34
pack: trust profitable at-rest deltas; no-op an unchanged log

Re-beating every existing delta paid best-of-N delta.create plus a zlib trial per artifact and never converged -- each run re-encoded ~100k frames again for ~16 bytes each (#122). A delta whose declared raw length clears pickBase's own 75% bar is copied verbatim; one that fails it (a peer's poor base) still re-encodes. A .zit-cache/packed generation key makes a repeat pack over an unchanged log a no-op. tk: 121.6s -> 76.7s first pack, 0.2s repeat. check-in: 27c4f30036 user: dmestas tags: trunk

13:25
stat: buffered census walk, indexed config read

The census pread 10 bytes per frame -- an O(frames) syscall floor, ~150k calls on tk (#121). A 64KB read-ahead window keeps the walk a measured physical pass at ~1/40th the syscalls, and the config/roll read adopts the scan index like every other cached verb. tk stat: 0.234s -> 0.026s, now ahead of fossil info. Output byte-identical. check-in: c17e0233ca user: dmestas tags: trunk

13:12
diff/status: adopt the scan index for the baseline open

CheckinBaseline and the extras-family config read each paid a full frame-header scan per invocation (#120). A shared openLogAdoptingIndex adopts the .zit-cache sidecar (#112's pattern), refreshing on miss; extras' path is read-only per B-extras-16. tk single-file diff: 0.220s -> 0.020s. check-in: be281ab760 user: dmestas tags: trunk

02:27
sync client: log-backed holdings replace the whole-store snapshot

pull/push/sync loaded every artifact into memory before the first wire byte -- 3.7GB resident on a 148k-artifact store, OOM beyond it (#119). The client now sits on LogIndexStore as serve does: names, held/phantom, shun mask, and hash-policy off the frame index, content materialized per-rid on demand, received artifacts in the appendix. No-op pull on tk: 3.7GB -> 88MB, 64s -> 13s. check-in: 46d49702f7 user: dmestas tags: trunk

2026-08-21
22:51
status/diff: stat memo skips re-hashing unchanged files

Record (mtime, size, hash) per tracked file in .zit-cache/treestat after each scan; a matching stat answers without reading. Fossil's vfile heuristic (D. Richard Hipp), race window included. Advisory tier rules: malformed parses as empty, deletion costs a re-hash, never an answer. check-in: f34bbc730a user: dmestas tags: trunk

21:08
merge materialize-perf into trunk (#114)

Both sides had made the repository-tier setting read adopt the scan index (#112); trunk's write-on-miss refresh is kept inside the shared helper the autosync read now also uses. check-in: 6f540c5d77 user: dmestas tags: trunk

20:56
settings: honour .fossil-settings, fossil's spelling winning (#117)

A repository cloned from a Fossil server carries its versioned policy in .fossil-settings/, which zit ignored entirely -- extras, addremove, and clean answered differently from fossil on the same checkout. Every versionable setting now reads fossil's spelling first and zit's as the fallback, through the one resolver funnel; the ignore seam and the shadow note follow. Settings zit does not model stay unread. check-in: 296f2e5cc2 user: dmestas tags: trunk

20:50
update/open: materialize 2x faster on large trees (#114)

Stage payloads once and rename them into place instead of writing them twice with an fsync each; keep the parent directory open across a path-sorted plan instead of re-walking it per file; read the repository setting through the persisted log index instead of a full frame scan; reuse the verb's open repository for the target's branch facts rather than deriving repo state a second (update: third) time.

tk (146k artifacts, 1,079 files): open 1.1s -> 0.48s; fossil is 0.34s. Rename-into-place also closes the checkout_sweep H2 finding: no crash cut can leave a hybrid file, so the pinned test now asserts that. closed check-in: 244523acfa user: dmestas tags: materialize-perf

20:38
repository: read checkout state from the invoking root, not the store dir (#116)

open() resolves root_dir to the store's home, which for a detached worktree is another checkout's directory -- so currentCheckoutRid read THAT checkout's .zit-checkout, and leaves, branch, bare ls, and the update/merge plan base all took the other tree's branch as current while status told the truth. A new checkout_root field carries the invoking directory; checkout-state reads use it. Pinned by a two-checkout e2e asserting every verb stays in its own tree. check-in: 6f0ba0fd3d user: dmestas tags: trunk

20:12
settings: adopt the scan index for the repository-tier read (#112)

repositorySetting opened the store with a bare openReadOnly -- a full frame-header scan to reach one config frame, 5.6s on a 1.17M-artifact store, and blind to the .zit-cache scan index because it had no directory in hand. It now takes Repository.logStore's own fast path: adopt the persisted index, resume over the tail, refresh on miss, with the cache directory resolved through the checkout's store pointer so a worktree never grows a stray sidecar. 5.6s to 0.12s. check-in: cf01ef34ba user: dmestas tags: trunk

20:05
serve: read push policy from the snapshot, not a per-round log scan (#112)

serveAnswer resolved binary-glob and crlf-glob through a fresh LogStore.openReadOnly per setting per round -- a full frame-header scan each time, ~11s per round on a 1.17M-artifact store, turning a clone into half an hour of server CPU. The snapshot's per-build config read now carries the pair, like the submanifest facts beside it; freshness rides the same (size, mtime) key. Server CPU for the same 181-round clone: ~34 minutes to 15 seconds. check-in: 262ca49578 user: dmestas tags: trunk

16:51
timeline: --weekday, Ben Joffe's branchless day-of-week (#111)

An mtime divided by a day is already the day number Joffe's identity consumes (benjoffe.com/fast-day-of-week), so the UTC weekday falls out with no calendar decomposition. Flag-gated; unflagged output is byte-identical, pinned by test against a Sakamoto reference over 40,000 days.

log --long's record grammar is now a stated contract: consumers parse it positionally and no display option will change it. The flag skips info, which prints no date line. check-in: 4652c5cc31 user: dmestas tags: trunk

02:31
crosslink: stream derived per-check-in records (#109)

The record body now captures the U-card user and the changed-file set, diffed against the primary parent at derive time where the parsed manifest is already in hand (layout 5, checkpoint 4; one-time rebuild per store). 'zit crosslink' replays it: identity, comment, tags, and file changes as a versioned text stream, cache-served under the same key discipline as every read verb, byte-identical to --no-cache.

sqlite dumps 36,797 check-ins in 0.11s / 14.9MB against the 3.87GB structural stream. A parent the store does not hold reads as 'files unknown', never as an empty change set -- sqlite's corpus carries exactly one, a 2009 check-in with a genuinely absent parent.

Mixed binary versions thrash across the layout bump: each side rejects the other's cache and re-derives. Update every binary that shares a store. check-in: 2a68d2fc79 user: dmestas tags: trunk

01:00
frames: --structural emits only what the crosslink links (#108)

Content blobs dominate a full stream (13.2GB on sqlite) and indexing consumers discard them. The gate is artifact.parseForImport on the reconstructed bytes -- never a cache -- behind an O(1) reject-fast screen that admits card shapes and signature wrappers; skipped frames are not hashed or emitted, and the cursor advances over them, so filtered and unfiltered polls compose against one cursor. sqlite 13.2GB -> 3.9GB, tcl -> 682MB. check-in: a2fb222e32 user: dmestas tags: trunk

2026-08-20
19:17
repostate: index the two tag lookups the cache serializer loops over

serialize called tagsOf per rid and resolveName per symbol, each a full tagxref scan -- O(rids x rows) and O(symbols x rows). A one-check-in append cost the next reader 42s on a 42K-check-in store (#107); now 0.8s, and a full derive drops 74s to 21s.

tagsOf routes through the existing rid_tags index; resolveName gets the inverse tag_rids index, maintained by the same helper pair so the two stay precise together. Selection semantics unchanged. check-in: 0aa75ac13d user: dmestas tags: trunk

2026-08-19
17:45
release: put the manifest assembler in the tree

release-manifest.json is the authoritative record of a release -- checksums, targets, qualification -- and nothing that produced it was in the repository. Its source_tree_sha256 had to be reverse-engineered from published output to cut 0.2.0: sha256 over 'sha256 path' lines sorted BY LINE, so by hash, which is the sha256sum | sort | sha256sum idiom.

The release whose manifest nobody could check is the one carrying false fields. 0.1.3 labelled a native build x86_64-linux-musl and claims 104 source files against a tarball holding 93.

The script re-verifies every archive against its own metadata fragment before describing it as qualified, and reproduces the published 0.2.0 manifest exactly. check-in: 520e388d6a user: dmestas tags: trunk

16:58
release: prepare zit 0.2.0

The 0.1.x line grew from 38 dispatched verbs to 61, gained the sub-manifest dialect, and grew a layered derived-state cache. 0.1.4's notes mention sub-manifests once and never say cache, so the notes here cover that machinery rather than only what changed since 0.1.4. check-in: 09e80bc9ef user: dmestas tags: trunk, version-0.2.0

02:50
reference: pin the flags, not just the verbs

An audit of all 57 verbs against their own --help found two gaps the existing pins could not see, because reference_test.zig checks that a verb HAS a section and #68 checks options-table flags against the usage block.

pack --drop-elided was in neither. The flag is real -- it is the sparse spec's narrow-then-compact deletion, destructive and irreversible -- and it appeared in no help output at all, so nothing compared it to anything. Now documented, including that unlike shun it leaves no tombstone, so a later widening re-fetches from a peer.

settings --global was accepted but missing from the reference.

The new e2e pin holds every verb's real --help against its reference section both ways: a flag the verb accepts that the section never names, and a flag the section documents that --help never mentions. The second direction matters more than it looks -- an omission is discoverable from --help, a promise sends someone hunting for something that is not there.

It lives in the e2e lane because it needs the help TEXT, and the help constants are in main.zig, outside the library test module. Conventions flags (-R, --no-cache) count as covered wherever they are documented once; matching is whole-token so --cone cannot satisfy --cone-not.

Both directions were verified by breaking them deliberately. check-in: 03e606ea49 user: dmestas tags: trunk

2026-08-18
23:30
merge -X ours|theirs: resolve content conflicts wholesale (#91)

merge3 already resolved every region except one -- the case where both sides changed it differently. That branch now takes a side instead of always writing an envelope; the clean cases above it are untouched, which is the whole point. A region only one side changed is still that side's under either strategy, so -X is not 'discard their work', it is 'stop asking me'. The mechanical cases are what it is for: a regenerated lockfile, a vendored tree taken whole from upstream.

Applies to content disagreements -- conflicting hunks, add-vs-add, and both-modified binaries. It deliberately does NOT resolve modify-vs-delete: that is a disagreement about whether the file should exist, not about its contents, and either reading silently destroys work. git declines it too.

-X ours is not --backout, and the help says so outright: --backout reverses NAME's change, -X ours keeps your side where the two collide and reverses nothing. They read alike and do different things.

Composes with precedent (#103) without either knowing about the other: precedent is a pass over the outcome set, so a strategy that leaves no conflict leaves it nothing to replay or record. Verified both ways -- -X records no precedent, and a plain merge still conflicts and records.

A bad or missing -X value is a usage error rather than a silent fall back to markers, which would look exactly like a strategy that ran and found nothing. check-in: 52b8c57f4c user: dmestas tags: trunk

22:40
export-git: drop a verb-list line pasted into its usage block

'zit export-git --help' printed " \ import-fossil bring a fossil repository's history into zit" as its second line, backslash included. The verbatim help pins cover merge and stash only.

The new pin checks shape instead: no rendered help line may start with a backslash, which catches the paste for every verb. The reference gains merge --no-precedent, which was documented under the wrong verb. check-in: 5fe0d9eafe user: dmestas tags: trunk

21:28
stash: reserve .zit-stash so it cannot be tracked

The sidecar was absent from the control-path registry, so extras listed it and add accepted it: an ordinary add put the contents of stashed work into a check-in and synced it to every peer, recoverable only by shunning. The stash help already promised "never an artifact"; nothing enforced it.

Authoritative tier, matching what losing the file costs. check-in: 503637a369 user: dmestas tags: trunk

20:37
precedent: remember conflict resolutions and replay them

A merge records each conflict in the local .zit-precedent sidecar; commit attaches the resolution the operator settled on; a later merge whose conflicted bytes match replays it instead of writing markers. Keyed by path and conflicted bytes, exact because the diff3 envelope carries all three sides. Whole-file, so a file whose other conflict moved will not replay.

Replay is named per file, never silent. --no-precedent declines for one merge without forgetting. 'zit precedent list|show|forget', alias rerere. The sidecar is a reserved advisory name, so it can never become an artifact. check-in: a9a24a857c user: dmestas tags: trunk

19:39
fix(release): export tag-only mirror changes check-in: 91cc1528fc user: dmestas tags: trunk
19:22
release: prepare zit 0.1.4 check-in: 4c2286bcb4 user: dmestas tags: trunk, version-0.1.4
19:05
release: qualify portable packaged binaries check-in: 4290d14e17 user: dmestas tags: trunk