Skip to content
zit Timeline
Login

Timeline

50 descendants and 50 ancestors of b450374bced68dcd

2026-08-31
17:02
image: carry the comment and login per rid, so a listing reads no manifest

zit leaves read and parsed a whole manifest per row — through its delta chain — for the comment's first line and the login. A warm store is image-backed and the image carried neither, so commentOf answered null and every row fell back to that read: 472 leaves, one second, on sqlite.

The image now materializes both: the comment CSR-addressed per rid, the login interned (a repository has a handful of committers). layout_version 6 — an older image is a miss and rebuilds, as the disposable cache intends.

0.3.2. check-in: 71d327a695 user: dmestas tags: trunk, version-0.3.2

01:01
import-fossil help: keep 'zit submanifest enable' on one line check-in: edac449311 user: dmestas tags: trunk, version-0.3.1
2026-08-30
16:50
0.3.1: version bump and release notes check-in: b5afbdb45f user: dmestas tags: trunk
16:20
clone: diff a check-in against its parent by their changed files, not two whole file lists

The derive built the complete effective file list for every check-in and its parent, then compared them — O(files) per check-in, which on pkgsrc is 100k entries twice over, 305k times. Fossil's add_mlink merges the two deltas' own F cards and seeks the shared baseline; do the same, and walk the lists side by side when the baselines differ instead of materializing them. Keeps recently-read manifests parsed, pinned while a step holds them.

zit#172 check-in: 669593f624 user: dmestas tags: trunk

2026-08-29
23:53
import-fossil help: say the shape of pack's effect, not a corpus figure check-in: 97a7015b74 user: dmestas tags: trunk
22:30
import-fossil --submanifest: recommend 'zit pack' next; the source's chains are rarely the best bases (zit#169) check-in: 24bc789080 user: dmestas tags: trunk
20:54
An empty check-in name is refused, not a crash: guard the resolver's precondition at the repository door (zit#171) check-in: c2b38c3ca8 user: dmestas tags: trunk
20:30
diff prints Fossil's Index banner; update's changes line reads as a sentence (zit#170) check-in: 9e3ba34bb0 user: dmestas tags: trunk
20:11
Fossil's words by default: status rows, ADDED/DELETED/RENAME/REVERT/UNDO lines, blame's user column, a no-op update's checkout line, singular counts (zit#170) check-in: 201202f4c8 user: dmestas tags: trunk
19:39
muscle: fourteen more session steps (stash, branch cycle, conflicting merge, undo, help); zig build ux keeps every step's bytes under zig-out/ux (zit#170) check-in: 11b68d9d99 user: dmestas tags: trunk
19:21
Prefixes resolve through aliases and name the shortest spelling that works; verb help lists its aliases (zit#170) check-in: bb2f45e4ff user: dmestas tags: trunk
18:27
Receipts: commit summary and content notes, undo per-file rows, stash file names, revert undo hint, short hashes in update (zit#170) check-in: 69f708db73 user: dmestas tags: trunk
17:46
listings take Fossil's shape: ls names, leaves lines, day-grouped timeline, annotate line numbers; branch -v (zit#170)

Twelve-character hashes and the weekday in day headers and leaf lines are the departures; -F oneline keeps the one-line timeline for scripts. check-in: 16c197b4dc user: dmestas tags: trunk

16:57
status leads with Fossil's checkout block; changes drops it; bare info adds the count (zit#170)

Twelve-character names and the weekday in the times are the only departures from Fossil's shape. cli.shortHash and cli.humanTime are the primitives the rest of the terminal-speech work builds on. check-in: d72b6372ab user: dmestas tags: trunk

15:33
info with no NAME describes the current check-in; revert names only the files it changes

Both found by running one session through fossil and zit side by side. check-in: a0690841c1 user: dmestas tags: trunk

14:40
update: with no CHECKIN move to the branch's newest check-in, as Fossil does

A bare 'zit update' was a usage error; Fossil's means --latest. check-in: b618fc675d user: dmestas tags: trunk

12:44
logstore: putArtifactPreferring takes a known base before searching (zit#169)

The converter's hints are the source's own chains and each manifest's parent, so one delta usually settles it; the best-of-N window is the fallback. Conversion is now faster than the old convert-then-pack. check-in: 598d040de5 user: dmestas tags: trunk

12:04
import-fossil --submanifest: delta-encode the converted store as it is written (zit#169)

Blobs take the flat source's own delta base as the hint, manifests their parent, carried artifacts the recent window. A 28x full-form gap on a toy corpus became 1.3x of packed, with no second pass and no double-disk peak. check-in: 38b9743ff8 user: dmestas tags: trunk

10:18
release: prepare zit 0.3.0

Fossil parity on the everyday verbs, landings and strata, protected branches behind a trusted front, Fossil's user table, and the read paths that now scale with the change rather than the store. check-in: 1e2a7bbc0d user: dmestas tags: trunk, version-0.3.0

09:26
ui launches quarry's local mode (zit#165, quarry RFC 0010)

Runs quarry ui on the checkout root with the terminal inherited; the binary comes from ZIT_QUARRY or PATH, and an absent one is a three-line pointer, exit 1. check-in: 95553946e3 user: dmestas tags: trunk

09:07
diff --by, timeline -F html, and a reserved ui verb (zit#165)

--by writes a side-by-side page under $TMPDIR and opens it from a terminal; -F html renders the timeline as a table; ui names quarry's local mode and exits 1 until it exists. check-in: 87f4be2914 user: dmestas tags: trunk

08:46
settings editor and autosync pullonly, --changed/--value; grep -i -c -l -v --once; annotate -n -l (zit#164)

The two new settings ride the keyed config block; a configured editor makes commit editor-capable without a terminal. check-in: 7d9dcb2de1 user: dmestas tags: trunk

07:53
stash: snapshot, diff, goto, and apply --merge (zit#164)

--merge three-way merges a stashed change into a dirty file; conflicts get markers, are listed, and exit 5 with the entry kept. check-in: 21ce2c785d user: dmestas tags: trunk

07:35
merge: --integrate, --baseline, -n, and a bare merge onto the one other leaf (zit#164)

The checkout record carries the integrated leaf's hash as an optional sixth field; commit closes it with the same control artifact branch close mints. check-in: 2a522aab40 user: dmestas tags: trunk

07:01
diff: PATH operands, --numstat, --invert, --checkin, --branch (zit#164)

-Z is -w; -N is accepted (an added file's hunks already carry it all). check-in: 39c88046ab user: dmestas tags: trunk

06:35
mv takes many operands and directories; revert -r/-n, undo -n (zit#164)

The managed set records one pending rename per moved file. mv moves files into a directory or a tracked directory as a unit, --soft records without moving, -n previews. revert -r VERSION restores another check-in's content; revert/undo/redo -n name what they would touch. add -f takes ignored files; clean gains -n/-f and --emptydirs. check-in: ece16d74a9 user: dmestas tags: trunk

05:30
branch sub-verbs; clone opens its tree, open URL clones (zit#164)

branch new/close/reopen/hide/unhide/current/info. clone DIR defaults from the URL and is followed by open (--no-open keeps the bare store); open URL delegates to clone. check-in: 5d2f9a7e6e user: dmestas tags: trunk

03:36
timeline filters and formats, status classification, -R for the sync family

timeline gains -b/-c/-p/-u filters, -r and --offset, -F medium|full (a date header per day, *CURRENT* on the checkout's check-in, user and tags) and -v for changed files; the default line is unchanged. status gains --classify (Fossil's words, aligned), --added/--edited/--missing/--conflict/--extra/ --differ filters, -v's header block, CONFLICT for an edited file still carrying markers under a pending merge, and MERGED_WITH. pull, push, sync and remote accept -R, so a bare mirror needs no working tree. current/ ckout/prev/next take effect in these verbs through the same hint. Refs zit#163. check-in: 6c84f304f4 user: dmestas tags: trunk

02:33
Everyday verbs: update receipt, network narration, commit editor, checkout-relative names

update prints Fossil's receipt on stderr (per-file UPDATE/ADD/REMOVE/MERGE/ CONFLICT, the from/to block, the undo hint) and gains -n, --latest and -v. Every sync-family line names the peer, a no-op is always 'already in sync', clone prints the project code, an unreachable peer is named, and pull/push/ sync show per-round progress on a terminal. commit takes -M FILE, composes the comment in $VISUAL/$EDITOR/--editor over a template of the change set (kept in .zit-ci-comment when the commit then fails), and gains -n and --allow-empty; --no-warnings is accepted as Fossil's spelling of --no-checks — the content gate stays opt-in by design. The names current/ ckout, prev/previous and next resolve against the checkout. The muscle harness now enforces all of it. Closes zit#157, zit#158, zit#159, zit#160, zit#161. check-in: e5beabd7c0 user: dmestas tags: trunk

00:59
fossil-muscle-memory: a differential transcript through fossil and zit

zig build muscle runs one scripted session — 27 lines a Fossil user types most — through the real fossil and the real zit against local servers, and compares each step's exit-status class and whether the verb spoke at all. Deliberate divergences carry their reason, known gaps carry their issue; anything else fails the run. CI runs it beside smoke. Closes zit#162. check-in: 741f6d428b user: dmestas tags: trunk

00:59
Verb prefixes, Fossil aliases, and a suggester that never says 'rm'

Any unambiguous prefix of a command dispatches (zit up, zit tim); an ambiguous one names its candidates; an exact verb always wins, so stat stays the store census. Aliases: ci/checkin, co/checkout, changes, delete/forget/ remove, rename, blame/praise, new. A token of three letters or fewer is only suggested at edit distance 1, and an unknown verb prints two lines, never the command list. Closes zit#156. check-in: e114f7a174 user: dmestas tags: trunk

2026-08-28
21:29
import-fossil --submanifest: read and parse each baseline manifest once

repo.files re-read and re-parsed the B-card baseline for every delta check-in — 79% of the mint phase on pkgsrc. Repository.filesMemo takes a caller-owned BaselineMemo keyed by artifact name; consecutive deltas over one baseline borrow the parsed manifest and read nothing. On a 30k-file history of 300 deltas, source enumeration went from 10s to 0.3s and the conversion from 13.5s to 3.7s, every check-in hash identical. The memo holds a heap entry with a stable address; the optional-by-value form did not persist under ReleaseFast. Closes zit#167. check-in: c80728261e user: dmestas tags: trunk

16:04
submanifest convert, and import-fossil --submanifest straight from a fossil URL

The rebirth arm of import-fossil required a zit store, so the verb that says 'import a fossil' could not take one. It now accepts the fossil's sync URL like --compatible does, cloning a transient flat store beside DIR and removing it after; a local store stays accepted. The zit->zit conversion gets its proper home as 'zit submanifest convert SRC DEST', beside 'enable', which now says what it does not do: existing check-ins stay flat, and fossil peers receive nothing new from the next commit. Diagnostics carry the verb the operator typed. Closes zit#168. check-in: abd850dc4c user: dmestas tags: trunk

03:59
import-fossil --submanifest: drop the whole-history replay gate and --force

The gate replayed every commit through the from-scratch tree builder three times to predict what the conversion itself measures, and on pkgsrc that was 14 of 26 hours before a byte was written. The conversion writes a new project beside an untouched source, so the honest evidence is the converted store: the summary now reports its bytes against the flat source's. The partition parameter comes from --k or the tip sweep; a history no k partitions is refused, as 'submanifest enable' refuses it. 'zit submanifest analyze --replay-all' keeps the model for anyone who wants it first. Closes zit#129. check-in: 04d944c1dd user: dmestas tags: trunk

01:56
import-fossil --submanifest: build each check-in's tree from its parent's with the sub-manifest editor

Every check-in went through artifact.buildTree from scratch, so a wide tree cost its whole file set per commit against a few changed slices actually written. The converter now applies each check-in's change set over its primary parent's root with TreeEditor — the commit verb's builder — keeps a parent cache released as children consume it, interns the retained cards, runs the pair proof through the run-scoped flatten memo, and parses each source manifest once. The Git importer's LogTreeStore moves to a shared module. Output is unchanged: every check-in hash matches the from-scratch build on libfossil and on a 30k-file synthetic history. Refs zit#167. check-in: 5e0155427e user: dmestas tags: trunk

01:00
import-fossil --submanifest: strip SSH signatures, verify per pair in the mint loop, fold the census into pass 1, skip the replay under --k --force

The converter only stripped PGP armor, so an SSH-signed check-in re-minted with its envelope inside and failed its own verify silently. Verification now runs as each pair is minted, the census counts come from the first pass instead of a second corpus read, and --k narrows the gate's replay to one arm (skipped with --force). Each phase reports its wall time on stderr; the summary points at 'zit pack'. Closes zit#155. check-in: 75c8669553 user: dmestas tags: trunk

2026-08-27
20:02
merge settings-config-repo-flag-153: accept -R on a bare store for settings and config (#153) check-in: 26ed9cbc86 user: dmestas tags: trunk
19:00
settings, config: accept -R on a bare store

Their repository and global tiers, and config's user/shun areas, all live in the store, not the working tree -- fossil serves them under -R too. Move both out of the -R blanket refusal, resolve the override, and name a bad -R path with the #97 diagnostic rather than the discovery message that advises the flag already given.

Issue #153. closed check-in: b342686fd9 user: dmestas tags: settings-config-repo-flag-153

11:04
landing: update -- bring the target into the proposed branch on the store

The reverse of accept, through the same ephemeral-checkout merge, now one function that does not know which direction it runs. Always merges in a working area of its own, checkout or not, so a tree is never left half-merged. Up to date is exit 0 with nothing written; a conflict is refused with its paths; a settled record is refused; a stacked landing is not, since refreshing a rung is what the rungs above it need. The refresh is a reply in the record's thread. Issue #152. check-in: c4b949ebbf user: dmestas tags: trunk

2026-08-26
19:43
merge serve-protected-branches: per-request branch protection behind a trusted front (#151) check-in: d46cb9a855 user: dmestas tags: trunk
16:57
serve: refuse a check-in's own singleton claim on a protected name; warm the image only after a push

A self-targeted +sym-B is not how a branch is joined, but nothing legitimate mints one on a protected name. maintain now derives the fast-read image only when the log grew, so a pull-heavy fronted server does not pay an image load per connection. Pins that a merge with a protected secondary parent stays on its primary's branch. Issue #151. closed check-in: 13760d0c92 user: dmestas tags: serve-protected-branches

16:14
serve: refuse membership changes to protected branches, per request, from a trusted front

A front that authenticates every push knows who is pushing; the store knows which branch a pushed check-in resolves to. The front sends only the rule -- X-Zit-Protect, a glob list -- and zit serve --fronted refuses the whole round when a check-in would join one of those branches or a tag would move a held check-in in or out of one. Same error card as a content-policy refusal, so a stock fossil client reads it.

Without --fronted the header is ignored and the server is unchanged. Nothing new on the wire or in the repository. Resolution is crosslink's own, through the fast-read image, which maintain keeps warm between connections. A parent not yet held is admitted: it is judged on arrival. Resolver faults fail closed. Issue #151. check-in: 63ec7d6dc8 user: dmestas tags: serve-protected-branches

14:35
merge landing-reply-to-149: --reply-to aims a landing comment's I-card (#149) check-in: 2892f40e8b user: dmestas tags: trunk
14:05
landing: --reply-to aims a comment's I-card at a post, not just the root

comment collapsed the G and I cards onto the record root, so a landing thread could not branch. Split mintForum's root and reply-to; landing comment takes --reply-to POST, resolving the thread root with the same helper forum uses and refusing a target outside the record's own thread. closed check-in: fed51b20cb user: dmestas tags: landing-reply-to-149

2026-08-25
21:19
merge rfc-vector-guard: vectors inert on disk check-in: ca7453995e user: dmestas tags: trunk
21:09
rfc vectors: keep them inert on disk

An unguarded vector parses as a real artifact, so Fossil crosslinks it into every clone -- two check-ins, a technote, twelve sub-manifest slices and a phantom pair that sync chases forever. Each vector now carries a leading # block; rfc_vectors.zig strips it at comptime so callers still see the exact RFC bytes.

Background and the cleanup that follows: see wiki page 'rfc-vector-ledger'. closed check-in: 32ef56aaf4 user: dmestas tags: rfc-vector-guard

18:50
merge fix-148-prefix-lookup: byte-aligned name lookup (#148) check-in: 3f8aa36a1d user: dmestas tags: trunk
18:30
logstore: answer a byte-aligned name prefix from the sorted directory

A complete SHA-1 name is shorter than max_hash_len, so it missed the full-width fast path and fell to the O(store) hex scan -- once per file under `ls --size` and `ls --long`. On tcl's tip the 545 SHA-1 named files cost ~54ms each against ~0.2ms for the 1,653 SHA3 ones.

The directory is ordered by raw digest, so a prefix's matches form one contiguous run. A binary search for the run plus a walk of it proves uniqueness rather than assuming it, which is what the SHA-1 case was surrendered to the scan for. Odd-length prefixes end mid-byte and keep the scan.

ls --size on tcl: 1.98s -> 0.03s. Issue #148. closed check-in: 4829f043b1 user: dmestas tags: fix-148-prefix-lookup

2026-08-24
21:41
merge strata: stacked landings (#144) check-in: 2e060aa7c0 user: dmestas tags: trunk
20:23
landing: pin the stack-depth bound the indent trusts

The cycle panic was in the indent computation, and rewriting it made the panic go away without my being able to explain the arithmetic -- 32*2 into a 64-byte buffer should not overflow. This asserts the bound directly, so the renderer's assumption is checked rather than believed. closed check-in: 5a35874240 user: dmestas tags: strata

20:10
landing: cycles, unresolvable targets, and already-contained branches

Three holes found by walking the scenario space rather than the happy path.

A retarget could make a cycle. Every segment in one then refuses forever, each waiting on the next, and 'ls' panicked outright computing an indent for a chain with no bottom. Cycles are now refused, direct and indirect.

A retarget accepted a branch name that does not resolve, minting a permanent record aimed at nothing -- the check 'open' has always made before minting one.

A branch already contained in its target reported 'did not merge cleanly; resolve and commit'. There was nothing to resolve: 'merge' had said 'already up to date'. The record could then only be closed by rejecting it, recording the opposite of what happened. It now settles as accepted with no merge, which is what is true. Reachable from a stack by landing the top segment first -- it carries the ones below it in. check-in: b450374bce user: dmestas tags: strata

19:17
landing: refuse a landing whose base was rejected or already landed

The ordering guard only looked for an OPEN base, so a rejected one slipped through and the accept SUCCEEDED -- 'accepted seg2 -> seg1', filing the work on a branch nobody would land. Nothing failed and nothing warned; the merge is fine, which is why only a rule catches it. Same hole for a base that already landed, reachable when the auto-retarget after that accept did not run.

All three cases now refuse and name the retarget that fixes them.

Documented the half of this that surprises: rejecting a landing rejects the proposal, not the code beneath a segment stacked on it. That branch descends from the rejected one and still carries its check-ins, so landing it on trunk lands them too. check-in: f91105346b user: dmestas tags: strata

18:52
landing: close the landed leaf without propagating

The propagating form marked every segment ABOVE a landed one closed too: in a stack those check-ins descend from the landed tip, so propagation followed the DAG straight into work that was still open. The landing records stayed open and correct; only 'branch' and 'leaves' lied, which is exactly where you look for what still needs doing.

Fossil's own propagating close is safe because it closes a check-in with no descendants. A landed segment is a leaf by the same-branch rule while still having children on the branch above it. check-in: d97b39b272 user: dmestas tags: strata

18:16
landing: close the branch an accepted landing lands

Every landed branch stayed an open leaf, which was survivable one landing at a time and is not with stacks: a six-deep stack left seven leaves, and 'leaves' is how you find work that still needs doing.

accept now writes fossil's propagating 'closed' tag on the branch it merged. reject does not -- a rejected branch is one the author may revise and propose again, and closing it would decide that for them.

Best-effort and last, like the retargets: a leaf that stayed open is untidy, never incorrect, and the diagnostic names the command that fixes it. check-in: c9b0147381 user: dmestas tags: strata

17:40
landing: stacked landings (strata)

A landing may target another branch that has its own open landing; that pair is a stack, read from the targets rather than recorded anywhere. No new card, no dependency field -- the edge is already implied.

Stacks land bottom-up. accept refuses a landing whose target has its own open landing, and accepting one retargets whatever was stacked on it onto the branch it just landed on. That is the whole restack: a metadata write per child, because a segment's branch already descends from the one below it, so after that lands the merge sees only the segment's own change.

Adds 'landing retarget', a target reply resolved by the same latest-wins rule as a state transition. ls indents a stack by depth; show names what a segment sits on, what sits on it, and when its base was rejected.

Verified six deep on a shared file, which is the depth the design claim was previously only reasoned about. check-in: 3767f1eafc user: dmestas tags: strata

16:50
merge index-borrow-143: adopted index entries slice the blob (#143) check-in: 3c3f936632 user: dmestas tags: trunk
16:41
logstore: adopted index entries slice the blob instead of copying it

Adopting a scan index duplicated every entry hash: 255,167 allocations and copies on a 348MB store to answer, typically, one lookup. The hashes are fixed-length slices of a buffer that was being freed at the end of the open, so the store now holds that buffer for its lifetime and the entries point into it. Opening tcl drops 44.0ms -> 37.5ms; smaller stores scale down with the index.

openReadOnlyIndexed takes the blob UNCONDITIONALLY -- on success the store frees it in deinit, on every refusal it frees there. The first cut transferred only on success, both call sites looked right, and the second one double-freed inside commit's autosync. No type and no unit test caught it; only e2e did. A conditional transfer makes every call site restate the rule.

Ownership is a prefix, not a per-entry flag: adopted entries come first and a tail resume appends owned ones after them, so borrowed_entries is the split point deinit and the batch rollback both read.

The unit test pins that the hashes lie INSIDE the held blob, not merely that they compare equal -- equal bytes prove nothing about where they live, and a future dupe would restore the cost with every assertion still green.

This is one part of #143. The CRC that issue names as the largest fixed cost measures 5.2ms of the 44ms (1,860 MB/s over the 9.7MB index), so it is not where the time goes and its integrity check should stay.

Refs #143 closed check-in: 1234842809 user: dmestas tags: index-borrow-143

16:08
merge merge-dirty-tree: merge into a dirty working tree (#135) check-in: 944ee007fe user: dmestas tags: trunk
15:17
merge: merge into a dirty working tree instead of refusing it

The current side becomes the working tree, so a divergent path compares what the operator actually has rather than what the check-in says is on disk -- which is the disagreement B-merge-14 existed to avoid. A clean tree scans to no divergences and every side reads from the store exactly as before.

Rollback needs no new machinery: captureMergeImages already reads the worktree, so an abandoned merge restores the uncommitted bytes.

Also adds the fault-injection coverage the carried update path was missing: a carried update that fails mid-apply must restore the WORKING bytes, not the check-in's. Verified by mutation -- breaking the baseline overlay fails both tests. closed check-in: 0627425be0 user: dmestas tags: merge-dirty-tree

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