Skip to content
zit Timeline
Login

Timeline

50 descendants and 50 ancestors of 5d2f9a7e6e8fb029

2026-09-09
22:53
0.3.10: version bump and release notes check-in: 77f89b0bec user: dmestas tags: trunk, version-0.3.10
20:04
derive: a disposition sidecar skips what cannot contribute a record (zit#182)

One bit per log frame in .zit-cache/dispositions: crosslink-relevant or not. A full derive consults it and never reconstructs the file blobs and SUBMANIFEST tree nodes that were most of a wide store's walk; the walk distills a fresh map either way. Same trust tier as the log-scan index: self-checksummed, keyed to a validated log prefix, stamped with the shun fingerprint, absent on any doubt. pkgsrc submanifest cold derive 302 s to 22 s; a consuming derive's body is byte-identical, pinned by test. check-in: a18d2edd67 user: dmestas tags: trunk

17:58
derive: write the cache in place, never a second copy (zit#182)

repocache.store and checkpoint.store concatenated header + body + image into a full resident copy before writing; both now stream the pieces to the file with the checksum folded incrementally, byte-identical output. The clone path frees its view before the reopen builds a second derived state. ZIT_DERIVE_FOOTPRINT reports the derive body's composition. pkgsrc cold open: peak RSS 2.32 GB to 2.07 GB. check-in: 3107e23e24 user: dmestas tags: trunk

14:26
0.3.9: version bump and release notes check-in: c641321230 user: dmestas tags: trunk, version-0.3.9
14:13
clone: persist each round as it converges, release what is written (zit#172)

A dense clone no longer holds the repository in memory until the exchange ends: each round's artifacts land in the log and their resident bytes are released, the shape of git's index-pack. A delta whose basis has not arrived defers on its hash; a basis first seen as a phantom is revisited when its content lands. Cone-scoped clones keep the batched pass, whose keep set needs the converged store. Streamed and batched logs are byte-identical, pinned by test. ZIT_PERSIST_WAITING reports the drain's peak deferred set. check-in: 533b0a6490 user: dmestas tags: trunk

2026-09-07
16:17
README: describe zit as it is today check-in: bc8f8da929 user: dmestas tags: trunk
01:44
0.3.8: version bump and release notes check-in: f0b4cd974f user: dmestas tags: trunk, version-0.3.8
01:37
settings: read the checkout the way fossil does (zit#175)

Fossil-dialect globs treat ! as a class member; only ^ negates. open reads a versioned-only manifest setting out of the check-in and remembers --manifest=off so update cannot re-enable stamps. add --force adds a named ignore-glob match; directory expansion always honours ignore and clean globs. .fos root names compare case-sensitively on Linux. check-in: b7e077e430 user: dmestas tags: trunk

01:09
annotate: per-file hashes in the crosslink record, Myers diff

The crosslink record now carries each changed file's artifact hash (fossil's mlink shape), so annotate reads blobs by hash instead of re-deriving every manifest on the walk. merge3 moves to a linear-space Myers LCS and annotate caches plaintext and baselines across versions. Repocache format 5, checkpoint format 5: the first open after upgrading re-derives. Help states the 12-character prefix annotate actually prints. check-in: a6c2215859 user: dmestas tags: trunk

00:09
info: many names in one call, -F json check-in: e751c1c8a2 user: dmestas tags: trunk
2026-09-06
21:49
0.3.7: version bump and release notes check-in: 1f12f7b9a9 user: dmestas tags: trunk, version-0.3.7
21:39
info: render Q cards from the state, not the manifest (zit#177)

The crosslink record, the state and the image now carry each check-in's Q cards (sign and target), so the cherrypick/backout line needs no manifest read. Record format 4, image layout 7. info NAME 16 ms to 6 ms on libspatialite, level with leaves; output unchanged. check-in: adb05cc14e user: dmestas tags: trunk

20:59
repocache: a hit rewrites a stale or missing image (zit#180)

A cache whose image is absent or at an old layout replayed the record body on every fast_read open, since only a miss stored. The hit now serializes the image from the view it just built and stores it, so the fallback is paid once. check-in: 98c5d6c61a user: dmestas tags: trunk

20:47
timeline: read the image, not a full derive (zit#181)

The image has carried adjacency since layout 2 and comment/login since layout 6, so every default row renders from the zero-parse path; only full/-v/-p still read manifests. 60 ms to 8 ms on a 22k check-in store, output byte-identical across formats. check-in: 18ffce5678 user: dmestas tags: trunk

20:06
grep: read manifests only where the record says PATH changed (zit#178)

One record scan, as annotate, instead of a manifest per check-in: 20,937 reads to 272 on VERSION, 19s to 0.4s. Each version is reported once under its oldest introducer, matching fossil grep's list byte for byte. check-in: 6be0341149 user: dmestas tags: trunk

14:06
sync: read the Content-Type media type, not the whole header (zit#179)

A front web server that appends "; charset=utf-8" made the clone reply look compressed, so its plaintext was read as a 1.6 GB container and refused as too large. parseMediaType now ignores parameters and case, as fossil does; serve uses it instead of its own copy. check-in: 8488af309e user: dmestas tags: trunk, version-0.3.6

14:06
0.3.6: version bump and release notes check-in: 762cd0d3cc user: dmestas tags: trunk
2026-09-05
17:34
0.3.5: version bump and release notes check-in: 882eaddc57 user: dmestas tags: trunk, version-0.3.5
15:13
Honour every Fossil checkout setting file (#174)

Glob lists follow Fossil's tokenizer and '#' comment lines; crnl-glob, clean-glob, empty-dirs, encoding-glob, dotfiles and mimetypes work at every tier. Dotfiles are hidden by default and Fossil's checkout-state names are never listed. UTF-16 and invalid UTF-8 classify as Fossil's lookslike.c does; tokenizer rules from glob.c. check-in: 982afa3d8f user: dmestas tags: trunk

2026-09-02
22:25
0.3.4: version bump and release notes check-in: f785e3286b user: dmestas tags: trunk, version-0.3.4
22:15
zit#173: route ordering tiebreaks through the hashOf door; trap the raw store under image backing check-in: 8216f43a66 user: dmestas tags: trunk
02:57
0.3.3: version bump and release notes check-in: 441d99370f user: dmestas tags: trunk, version-0.3.3
01:32
annotate walks the file's versions, not the whole ancestry

Every ancestor cost a manifest read and a full-file line diff — ~36k of each on SQLite where ~400 touched the file. The cache records already say who changed PATH (their diff is against the primary parent, so unlisted means byte-equal there): untouched ancestors inherit the parent's content pointer, the engine copies the parent's origins on pointer-identity, and the LCS trims the common prefix/suffix before its O(n*m) table.

Unfinishable on SQLite before; 0.5-1.2s typical, 57s for a 15k-line heavily-edited file (the multi-hunk quadratic middle remains — a real diff core is the named follow-up). Attribution of a repeated line between equally-valid origins can shift, as any LCS tiebreak may.

zit#172 check-in: 22bdf84fd3 user: dmestas tags: trunk

2026-09-01
23:10
timeline reads no manifest for what the state answers; count the reads that remain

The default-format timeline inflated a manifest per row — the fetch condition still said 'a.format != .oneline' from when oneline was the default, so zit#170's medium default made every row read one for two strings the image now carries. 66.5s to 0.37s for the full SQLite timeline, output identical.

Repository.manifest counts its reads (ZIT_MANIFEST_READS=1 reports on exit), and an e2e runs ten listings over two history sizes and requires identical counts — a per-row reader fails by construction, no timing, no constants.

zit#172 check-in: 09fa1a7fb3 user: dmestas tags: trunk

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:46
merge: hardware crc32 and index v3 sorted directory (#143, #145) check-in: 2a676a5df1 user: dmestas tags: trunk
18:33
logstore: index v3 carries a sorted directory, so opening builds no hash map

Adopting an index inserted every entry into a StringHashMap before answering anything: 255,167 keys on a 302MB store, to serve the one or two lookups a tree listing makes. Measured at 5.5ms of a 24ms open by building the map twice and taking the delta.

v3 appends entry_count little-endian u32 rids ordered by digest, and lookup binary-searches it -- ~18 comparisons against a quarter-million insertions. Opening tcl drops 24.0ms to 20.0ms; with the hardware crc32 change before it, 38.5ms to 20.0ms. Output byte-identical.

The directory carries RIDS and the entry records stay in scan order. Sorting the entries themselves would renumber every rid, and ancestry, phantom snapshots and the sync walk all read that numbering.

lookup consults the map first, then the directory: entries appended after adoption -- a tail resume past the index boundary, and every write on a scan-path store -- are only in the map. Every held-check that was reading index.get directly now goes through lookup, because intern runs during a tail resume and a miss there mints a duplicate entry for a digest already held.

A v2 sidecar is refused and rewritten rather than adopted, as v1 was: adopting one would leave lookup with neither a map nor a directory and answer null for held artifacts. Pinned by a test that downgrades a v3 blob and re-checksums it.

The tests pin what equality cannot: that the map is EMPTY after adoption and every digest still resolves, that a digest which is not held returns null rather than a neighbour -- the failure a binary search has and a map does not -- and that digests written out of sort order still resolve, so a directory that merely mirrored insertion order would fail.

Closes #145 closed check-in: 0d16bc966d user: dmestas tags: index-v3-145

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:53
crc32: fold with ARMv8's crc32 instruction where the target has it

Checksumming was half the cost of opening a repository: 18.8ms of a 37ms `ls` on a 302MB store, across the two sidecars (26.3MB derived-state image at 13.5ms, 9.7MB log index at 5.3ms). Both measured by doubling the work and taking the delta, so the number is the checksum and not its neighbours.

ARMv8's crc32b/crc32x implement CRC-32/ISO-HDLC -- the same generator this file is already defined in terms of, in the same reflected convention -- so this is an instruction-count change and never a value change, which is what the module doc says every edit here must be. tcl opens in 24.5ms against 38.5ms, output byte-identical.

aarch64 only, deliberately. x86_64's SSE4.2 crc32 instruction is CRC-32C/Castagnoli, a DIFFERENT generator; taking it because the mnemonic matches would orphan every store already written. x86_64 keeps the table path, which stays load-bearing rather than becoming a fallback nobody runs.

The new test recomputes CRC-32 as a plain bit-at-a-time recurrence, independent of update and its tables, and holds both paths to it at every length 0..1024 -- all eight residues mod 8, including the lengths below the fold width. The two paths differ only in where the fold stops and the tail begins, so a divergence would appear at particular residues and nowhere else, which is the shape a spot check misses. Chunked updates are pinned too: the store checksums payloads in 4 KiB pieces, so a path correct only for single-shot input would corrupt exactly the largest artifacts.

Refs #143 closed check-in: b82aacc805 user: dmestas tags: crc-hardware-143

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