Skip to content
go-libfossil Timeline
Login

Timeline

50 ancestors of c851990f4135ea94

2026-10-19
00:33
Merge remote-tracking branch 'origin/main' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md check-in: c851990f41 user: [email protected] tags: feat/submanifest-reader, main, trunk

2026-10-18
21:59
fix(checkout): keep uncommitted changes across Update, as fossil does (#450)

* fix(checkout): keep uncommitted changes across Update, as fossil does

Update compared only committed versions: it wrote the target over a locally edited file, deleted an edited file the target removed, and merged the current and target check-ins with their ancestor while ignoring the working file.

It now follows fossil's update (src/update.c): scan the checkout, then for each file take the target's content when there is no local change, merge a local edit three ways (the checkout's version as base, the working file as local, the target as other) with conflicts listed in Conflicted, keep an edited file the target deletes as an untracked file, keep pending adds and removals, restore a missing file, and copy an untracked file the target adds over to NAME-original first. A dry run writes nothing. Files written with the target's content record the mtime of the write. Files are matched by name; a target-side rename is not followed.

The two tests that relied on merging two branches' committed changes now make the clashing edit locally. A fossil 2.28 interop test runs the same edits through fossil update and compares files, fossil changes and Status.

Tracked in #443

* fix(checkout): make Update safe on binary files, failures and dry runs

A binary file (a NUL byte, or a line over 32,767 bytes, as fossil's line diff refuses) is no longer line-merged. Update saves the working copy as NAME-original and writes the target, as fossil's update does; Merge keeps the checkout's copy, as fossil's merge does. Both used to splice the versions and report a clean merge. merge.IsText is fossil's test.

Update refuses before writing anything when a file it must write or delete has a directory or a read-only file in the way, moves the checkout database to the target in one transaction, and in a dry run leaves the database untouched (the scan's findings stay in memory). Every target file gets the target's executable bit, as fossil's checkout_set_all_exe does, so a bit-only change reaches the disk.

UpdateResult.Unmerged lists files left to reconcile without markers: edited files the target deletes, files added both locally and by the target, and binary conflicts. The interop test now covers a binary conflict against fossil 2.28.

Tracked in #443

* fix(checkout): check real write access before Update writes anything

The pre-write check read the owner-write bit: it missed an unwritable directory (a remove failed after a merge had been written), files owned by others, and wrongly refused for root. It now asks the storage, via an optional CheckWritable: OSStorage uses access(2) with W_OK on unix and a read-only-attribute check elsewhere (js/wasm, Windows). Every step needs its directory writable; a write or merge over an existing file needs the file writable too.

A read-only untracked file where the target adds one is backed up to NAME-original, deleted and replaced, as fossil does, instead of refused.

Merge keeps a binary file whatever its strategy, as fossil's merge does. setVVar's precondition now catches a nil *sql.DB or *sql.Tx inside the interface.

Tracked in #443 check-in: 6e6969bf97 user: [email protected] tags: main, trunk

20:59
fix(checkout): record and trust vfile.mtime as fossil does (#444)

* fix(checkout): record and trust vfile.mtime as fossil does

go-libfossil wrote vfile.mtime = 0 everywhere and hashed every file on every scan, so it and fossil rehashed a go-libfossil checkout on every command.

The scan is now fossil's vfile_check_signature: a file whose size matches its artifact (blob.size of mrid) and whose mtime matches vfile.mtime is taken as unchanged without being read, unless ScanHash is given or the mtime-changes setting is off; the scan stores the file's mtime in whole seconds and runs its updates in one transaction. Status, commit, merge, revert and extract use the default scan, as fossil's commands do.

Mtimes are recorded where fossil records them: extract and update store the mtime of each file written with its artifact's content, an unchanged file keeps its mtime through LoadVFile (fossil's update transfer and in-place commit), committed files keep theirs, revert stores the restored file's, and SetMTime stores the time it gives. Undoing a rename drops the mtime, since the file was not checked.

One deliberate difference: an mtime not older than the current second is stored as 0, so the next scan (ours or fossil's) hashes the file and an edit that keeps the size within that second is still seen. A stored 0 is never trusted.

MemStorage now stamps each write with a new mtime from its own clock, as a filesystem does, so in-memory checkouts see same-size edits.

Status on the sqlite repository's 2,220-file checkout: about 250 ms before, about 30 ms after.

* test(checkout): pin the same-second guard against fossil

A same-size edit in the second a file was scanned is committed, and fossil 2.28 reading a row the guard stored as 0 reports the edit. Both tests fail with the guard removed. The guard's comment and the CHANGELOG name it as the one deliberate difference from vfile_check_signature.

* fix(checkout): record the mtime of each write as it happens

Extract and update statted their files only after every file was written and vfile reloaded, so an edit in between was recorded as the written content. They now stat each file right after writing it, as fossil's vfile_to_disk does, and carry that mtime to the row. Revert and the new writtenMTime read the clock before the stat, as the scan does.

A caller's Env without a Clock now gets the real clock, as Storage defaults to the OS; scans read it. The mtime-changes setting is read from the repository only: go-libfossil reads no global fossil settings, which the comment and CHANGELOG now say.

New tests: an edit after extract's or update's write is seen, a stored 0 is never trusted, and a simulated clock at a realistic time drives the mtime fast path. check-in: a71bf662ee user: [email protected] tags: main, trunk

20:01
fix: handle time exactly as fossil does (#442)

* fix(manifest): round a caller's check-in and tag time as fossil does

Fossil reads a --date-override through SQLite's strftime, which rounds the seconds to the millisecond (holding a fraction past .999 in its second) rather than truncating them. A caller time finer than a millisecond now gets the D card fossil would write for it; the default clock time is still truncated, as fossil reads its clock.

fsltype.CardTime and CardTimeNow are the one place both rules live, and the checkout commit path and branch creation take their default time through them.

* fix(repo): stamp config and user rows in seconds since 1970

Fossil writes config.mtime and user.mtime with its now() SQL function, time(0). SetConfig, SetHashPolicy and CreateUser wrote julianday('now') instead, so fossil read those rows as dating from 1970 when comparing times in a configuration merge, and SetCaps and SetPassword left the user's mtime alone where fossil's user command restamps it.

* fix(auth): read and write user.cexpire as a Julian day

Fossil keeps user.cexpire as a Julian day (src/login.c). A sync login parsed it as RFC 3339 or 'YYYY-MM-DD HH:MM:SS' text, so any value fossil had written failed to parse and the login was refused as expired. The expiry is now read through julianday(), which takes both forms, and repo invite --ttl writes a Julian day; its write error is now checked.

* fix(checkout): set file times as fossil's --setmtime does

Fossil's --setmtime gives each unedited file the time of the newest check-in, among the version and its primary ancestors, that recorded the file's content, truncated to whole seconds (mtime_of_manifest_file), and it does so for every unedited file of the version once the checkout is up to date (vfile_check_signature with CKSIG_SETMTIME). SetMTime gave every written file the target check-in's time to the millisecond, and an update left the files it did not write alone.

* fix(stash): keep and show stash and bisect times as fossil does

Fossil stores stash.ctime as julianday('now') and lists it with datetime(ctime); go-libfossil stored CURRENT_TIMESTAMP text, which the two drivers returned in different forms, and showed a fossil-written time as a bare number. bisect's list now shows datetime(event.mtime), as fossil's bisect ls does, instead of the Julian day.

* test(verify): fossil rebuild keeps the times go-libfossil stores

A repository go-libfossil wrote, with check-ins at caller and clock times, a tag and a branch, is rebuilt by the fossil binary, and every check-in event, plink and tagxref time must come back unchanged.

* test(sync): a fossil clone and push agree on every synced time

The fossil binary clones a go-libfossil server, unversioned files included, commits at a time finer than a millisecond and pushes back. Each side derives check-in event, plink and tagxref times from the artifacts itself, and the unversioned time travels on the wire; both repositories must then hold the same values.

* fix(resolve): bound NAME:TIMESTAMP with julianday() as fossil does

Fossil resolves BRANCH:TIMESTAMP with event.mtime<=julianday(BOUND). The bound was computed in the text form event rows hold, so on a millisecond where that form is larger than julianday()'s, a check-in at the bound itself resolved here and not in fossil.

* fix(manifest): round a caller's card time with integer arithmetic

CardTime followed SQLite's printing of the seconds as a double, which is not a well-defined rounding: at exact half-millisecond ties fossil 2.28's SQLite gives .124 for .1245 but .020 for .0195, and other SQLite versions differ again. A caller time is now rounded to the nearest millisecond on its nanoseconds, ties up, held at .999 as fossil holds it. The fossil binary test compares every non-tie value, across second, minute, day and year ends; ties are asserted on their own.

* fix(auth): ignore user.cexpire at sync login, as fossil does

user.cexpire is the expiry of fossil's web-login cookie: fossil sets it at web login, writes 0 at logout, and its sync login (check_login) never reads it. Refusing a sync login on it locked out anyone who had logged into or out of fossil's web UI, or whose cookie had lapsed.

The invite TTL that relied on it now lives in a config row of its own, invite-expire:LOGIN, as a UTC instant checked at sync login; repo invite no longer writes cexpire, and the expiry is no longer written in local time and read as UTC. Deleting a user drops its invite expiry.

Tracked in #441.

* fix(repo): convert Julian config and user mtimes on open

Repositories go-libfossil wrote before config and user rows were stamped in seconds since 1970 still hold Julian days there, which fossil reads as January 1970. Open converts those rows to the seconds they stand for. It checks first and writes only when a row needs it, so it converts once and takes no write lock on an up-to-date repository.

* fix(checkout): set a file no check-in recorded to 1970, as fossil does

With no mlink row for a file's artifact in the version's ancestry, fossil's max(event.mtime) is NULL, which mtime_of_manifest_file reads as 0, so --setmtime sets the file to 1970. SetMTime skipped the file.

* fix(stash): list stashes by creation time, as fossil's stash ls does

fossil stash ls orders by ctime, oldest first. List ordered by stash id, newest first; it now orders by ctime with the id breaking a tie.

* fix(auth): clear a login's invite expiry with the user, atomically

DeleteUser removed the user and its invite expiry in two statements; a failure between them left the user's expiry behind or the user without it. Both now go in one transaction. A user fossil deleted leaves its invite-expire row behind, so CreateUser clears it in the same transaction as the insert; repo invite sets its own expiry afterwards.

The on-open conversion of Julian config and user mtimes is now best-effort: while another connection holds the write lock, Open skips it and a later open converts the rows, instead of failing. check-in: d810a8118f user: [email protected] tags: main, trunk

18:46
Merge remote-tracking branch 'origin/main' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md check-in: 92936436e5 user: [email protected] tags: feat/submanifest-reader, main, trunk

18:15
fix(manifest): record check-in and tag times to the millisecond (#440)

Fossil and zit write D cards with exactly three fractional digits. go-libfossil wrote whatever precision the time carried, and the default clock time carries nanoseconds: legal under artifact-format §4.6.4, but zit cannot read a D card with more than three digits (danmestas/zit#244), so zit could not open a repository go-libfossil had committed to.

Checkin and AddTag now truncate the time to the millisecond, whether the caller supplied it or the clock did. Parsed artifacts are untouched: the marshaller still keeps any precision it read, so existing artifacts round-trip byte for byte. check-in: fa1895d56b user: [email protected] tags: main, trunk

17:33
fix(manifest): re-read settings before auto-retune; queue republished leaves

The post-commit re-tune re-reads the settings in its own transaction and does nothing unless the pin is still the commit's k and submanifest-auto-retune is still on, so a manual re-tune or the automatic one switched off since the commit is never overwritten. A sub-manifest a public commit makes public is queued for the next push, since it was never sent while private. The CLI states the k range from the manifest constants, and buildCheckinDeck's R-card computation moves to checkinRCard. check-in: c907b009c7 user: [email protected] tags: feat/submanifest-reader, main, trunk

17:26
fix: select over the newest check-in off trunk; honest submanifests status

Repo.EnableSubmanifests(0) analyzes the main-branch tip, and when no check-in is on the main branch (a repository whose history sits on another branch name) the newest check-in by time, the highest rid breaking a tie, rather than refusing; an empty repository still refuses. The godoc names the check-in in each case, and DisableSubmanifests' says existing trees remain and keep being served and that no note is returned.

`repo submanifests status` fails when a setting cannot be read instead of printing defaults, and shows a stored k outside 1..12 as invalid, with commits falling back to flat manifests. check-in: a659919be4 user: [email protected] tags: feat/submanifest-reader, main, trunk

17:19
fix(cli): refuse an explicit --k 0, and restore the SDK source links

`repo submanifests enable --k 0` ran automatic selection; §8.1 refuses an explicit k outside 1..12, so only an absent --k selects now. The SDK pages are rebuilt with gomarkdoc's repository flags, which a worktree cannot detect, so their source links match the published pages again. SM-7.2-3's note says the pragma is not reachable from the public API. check-in: 7a2f6dbe7a user: [email protected] tags: feat/submanifest-reader, main, trunk

17:19
fix(manifest): tree storage, privacy and post-commit work per review

- A public check-in makes every sub-manifest it names public, so a leaf a private check-in stored first is sent with the tree. - Sub-manifests are named by the hash policy alone (StoreUnderPolicy), in sha1, sha3 and auto repositories alike, and only those the commit wrote are queued for sync. - A sub-manifest that fills a phantom links the check-ins waiting on it once the commit is durable (§7.1, §7.3), not at the next sweep. - The automatic re-tune runs after the commit is durable, as zit runs it; its failure is logged and never fails the commit. - Enabling selects outside any write transaction, replaying one check-in at a time, then re-reads the settings in a short write; a pin that appeared meanwhile takes §8.1's precedence. - Checkin's transaction body moves to checkinTx, so neither function exceeds 70 lines. check-in: 0f543785e1 user: [email protected] tags: feat/submanifest-reader, main, trunk

17:19
feat(blob): store under the policy's name alone, reporting the outcome

StoreUnderPolicy names content by the repository's hash-policy algorithm and never reuses a copy held under the other algorithm's name, as draft-fossil-submanifests-00 §6.2 requires of an S card's hash. It reports whether the row was found, inserted, or filled a phantom, so a caller can queue only what it wrote and retry what waited on a phantom. Store shares its write-and-verify tail. check-in: 315b5021fc user: [email protected] tags: feat/submanifest-reader, main, trunk

17:04
feat: select over the main-branch tip, and add submanifests status

Repo.EnableSubmanifests(k) now analyzes the current trunk, the check-in the next commit most plausibly descends from, instead of the newest check-in; with no trunk check-in only an explicit k succeeds. A test puts a newer one-file check-in on another branch to show which one is read. Repo.DisableSubmanifests returns only an error and documents that existing trees stay.

`libfossil repo submanifests status` prints the three settings and whether the repository holds a tree; `retune` requires --k, and `disable` prints zit's note when trees exist. The SDK pages are rebuilt with make docs-gen-sdk. check-in: 5377023003 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:58
test(conformance): check enable-time selection against zit

On four flat two-commit histories zit wrote, go-libfossil reproduces every manifest byte for byte and then enables at the k zit pinned; in each the replay moves the pin off the snapshot's value, so the replay port is what is being checked. The pins are recorded for CI under testdata/zit-writer, and SM-6.3-7 states the refinement. The changelog describes the API and CLI. check-in: aea1e207f4 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:58
feat: sub-manifest settings in the Repo API and the CLI

Repo.EnableSubmanifests(k) now selects over the newest check-in itself, so callers pass only k (0 to select); Repo.DisableSubmanifests joins RetuneSubmanifests. `libfossil repo submanifests enable [--k N]`, `retune --k N` and `disable` drive them, printing the pin and the operator warning. The libfossil and cli SDK reference pages are regenerated. check-in: 0ebee94560 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:58
feat(manifest): select k on enable as zit does, and add disabling

The draft's automatic selection is advisory, and zit lets history outrank the snapshot: on enable it replays the newest 200 check-ins through the tree builder at the snapshot's k and its two neighbours and pins the value whose steady-state commits materialize the fewest bytes, the lowest on a tie, keeping the snapshot when fewer than two check-ins replay or every candidate stays flat. replayK ports that step by step, so two writers enabling on one history pin alike.

DisableSubmanifests turns emission off and keeps the pin, returning zit's note when trees already exist. The out-of-range fallback logs zit's report. check-in: 6d79130418 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:44
docs(changelog): sub-manifest writer check-in: 8d3962592c user: [email protected] tags: feat/submanifest-reader, main, trunk
16:44
test(conformance): cover the sub-manifest writer, with zit as oracle

The submanifest table gains the writer's requirements: the §6.1 predicate, §6.2's construction, limits, merge, stops, canonical encoding and storage order, §6.3's pinning, out-of-range fallback and selection, §6.4's manual and automatic re-tune, §8.1's enable rules and disabling, and the writer vectors of A.5. The writer reproduces every Appendix A tree, the A.5 three-level vector at k = 2 and the height-0 merge and encoding vectors at k = 3. 71 requirements: 66 covered, 3 partial, 2 deviations, 0 failing.

TestZitWriterInterop, with zit present, has zit commit ten scenarios (the A.1 set; 40 files at k = 1, 2 and 12; 3,000 files at k = 1, 3 and 5; deep paths; names with spaces, tabs, newlines and non-ASCII, executables and a rename) and requires go-libfossil's check-ins byte-identical. zit then clones, verifies and lists go-libfossil's trees, and verifies a tree go-libfossil commits on top of zit's; the §6.3 selection matches zit's analysis on five file sets. TestZitWriterBundle replays the recorded zit manifests in CI. zitRun now returns stdout only and sets FOSSIL_HOME, and zitListing reads names holding spaces, tabs or newlines. check-in: b77d8b119e user: [email protected] tags: feat/submanifest-reader, main, trunk

16:44
feat(manifest): write tree check-ins behind enable-submanifests

Checkin now honours draft-fossil-submanifests-00 §8.1: with enable-submanifests on and a pinned submanifest-k it stores the canonical tree first, marks each sub-manifest unsent, records the seen-submanifest indicator that arms the §7.2 gate, and writes a manifest of S cards with no F card and never a B card. The setting is off by default, so every commit is byte-identical to before. A stored k outside 1..12 falls back to a flat manifest and logs it (§6.3); unchanged sub-manifests are shared by content addressing.

EnableSubmanifests is §8.1's local request: a valid pin wins and a different explicit k is refused as a re-tune; with no valid pin an explicit k is pinned, or §6.3's selection runs over the current check-in; a request that cannot be met changes neither setting. RetuneSubmanifests is the manual §6.4 re-tune. After a tree commit the §6.4 gate re-pins k while submanifest-auto-retune is on, and only advises otherwise.

A sync run reports avoid-submanifests in SyncResult, and CheckinOpts.AvoidSubmanifests keeps a commit made in that sync flat (§7.2). The settings' names and parsers move to internal/manifest, which sync now uses. Repo.EnableSubmanifests and Repo.RetuneSubmanifests expose the two requests, the first returning the operator warning. A test shows fossil 2.28 never receives a tree this writer made. check-in: 14f17e675e user: [email protected] tags: feat/submanifest-reader, main, trunk

16:44
feat(manifest): build canonical sub-manifest trees

BuildTree is the construction of draft-fossil-submanifests-00 §6.2 over a check-in's sorted file entries at partition parameter k: the §6.1 predicate (leading zero bits of SHA3-256 of the name), the one-pass scan with its MIN and MAX limits and final-group merge at every height, the non-reducing and single-group stops, and the height-16 cap. Sub-manifests come back in storage order, children before parents, named by a caller function so the commit path can store as it names.

partitionCosts is §6.3's C(k), computed with zit's arithmetic so the two writers select alike; SelectPartitionK picks the least cost and the lowest k on a tie, and retuneTarget is §6.4's gate with zit's 15% margin. Tests reproduce Appendix A.2, A.3 and A.4 by name and pin the scan's limits on synthetic counts. check-in: 47cb5afcd7 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:44
feat(deck): canonical encoding and sub-manifest serializers

draft-fossil-submanifests-00 §6.2 fixes minimal escaping for every filename a tree construction emits: exactly space, newline and backslash are escaped, every other byte raw. CanonicalEncode is that encoding; S cards and a sub-manifest's F cards now marshal with it, while a flat manifest keeps §4.3's, so no existing artifact changes name.

MarshalLeaf and MarshalInterior serialize the two sub-manifest shapes, and LeafCard gives one leaf F card's bytes for the §6.3 cost. A test reproduces the Appendix A.2 leaf byte for byte. check-in: e9ec7390c7 user: [email protected] tags: feat/submanifest-reader, main, trunk

2026-10-17
23:26
docs(submanifest): point the version-gate and config deviations at zit

Fossil has no plans to read sub-manifests, so the 22900 threshold is not only fossil 2.29's announcement: every later release clears it too. The fix is a version no fossil announces, agreed with zit and the draft; SM-7.2-1 now cites danmestas/zit#239, and SM-6.3-3's draft wording is tracked in danmestas/zit#240. check-in: 7408c02304 user: [email protected] tags: feat/submanifest-reader, main, trunk

21:54
fix(sync): close the review's sub-manifest transfer findings

- A clone to a client the 7.2 gate refuses could name a withheld tree as a delta source: walkCloneChain never asked the gate, and a refused artifact was still recorded as sent. A flat check-in stored as a delta of a tree manifest -- the normal flat-to-tree transition -- then reached a 2.28 client as a cfile it could never apply. A withheld source is now unforwardable, so its dependent goes out whole, and a refused artifact is not recorded as sent. Gimme answers and pushes already send full content, and igot only announces, so the clone was the one path. - The gate armed only once a crosslink set the seen-submanifest indicator, so a tree a sync had stored but not yet linked went out ungated. Every receive path now sets it as the artifact is stored -- checkDelta hands back the content it rebuilt, so a verified delta costs no second expansion -- and a repository an earlier version swept, which may hold trees that version filed as unparseable, arms the gate until the next sweep re-examines them. - The three sub-manifest settings are written inside one savepoint, so a database error partway leaves none of them applied. - The 22900 threshold is zit's and is also what an unreleased fossil 2.29 announces without reading trees. The constant says so, and the conformance suite records SM-7.2-1 as a deviation with a case that shows such a peer being sent a tree. check-in: 4deb9d870f user: [email protected] tags: feat/submanifest-reader, main, trunk

21:29
perf(manifest): parse a delta's baseline once per sweep for the tree check

A sweep asked every delta manifest whether its baseline is a tree by expanding and parsing the baseline again, after missingBaseline had just done so. Record tree-ness where the baseline is first verified, and walk only a tree baseline. check-in: edefbcd26f user: [email protected] tags: feat/submanifest-reader, main, trunk

21:13
test(testutil): exempt the zit interop test from the fossil-home check

It runs zit, not fossil, each time under a HOME of its own. check-in: 6b694f0419 user: [email protected] tags: feat/submanifest-reader, main, trunk

21:10
docs(changelog): sub-manifest reader, transfer gate and version announcement check-in: 3232f3b158 user: [email protected] tags: feat/submanifest-reader, main, trunk
21:10
test(conformance): read zit's sub-manifest trees end to end

TestZitInterop, with zit on PATH or in ZIT_BIN, builds a flat history in go-libfossil (edits, a delete, a rename, a merge), serves it for zit to re-mint as sub-manifest trees at k = 1, which gives interior sub-manifests, and clones zit's trees over HTTP. Every check-in's file set must equal zit's own `ls --long`, the clone must verify and every R card check. go-libfossil then serves the clone to a zit clone, which verifies and lists the tip the same; pulls a tree check-in zit commits to its own server; and, as a server, receives and links a tree check-in zit pushes to it. REQUIRE_ZIT_BIN=1 makes a missing zit a failure.

CI has no zit, so TestZitTreeBundle reads a recorded copy of what zit wrote (testdata/zit-trees: 81 artifacts and the per-check-in listings, rewritten by ZIT_REGENERATE=1), storing every manifest before any sub-manifest so the deferral and retry run on real zit output. check-in: 3e7b98e18d user: [email protected] tags: feat/submanifest-reader, main, trunk

21:05
test(conformance): add the submanifest suite (draft-fossil-submanifests-00)

Pin draft-fossil-submanifests-00 from zit check-in 3f86ff4261 and write its reader-scope requirements table, 55 rows, from the draft text. The writer sections, the informative 5.6, the operator rule of 8.2 and the 7.4 SHOULD are listed as skipped, with reasons, in the table's header.

84 cases: card-level cases through deck.Parse; tree cases that store artifacts and read them back through ListFiles, LookupFile, Crosslink, VerifyRCard, Verify, Rebuild, Checkin and the sync handler; and the Appendix A vectors, as zit's byte vectors (vendored under testdata/submanifest), checked for their recorded names, walked enumerations and R cards, the A.5 delta-over-tree vector included.

Result: 50 COVERED, 4 PARTIAL, 1 DEVIATION. SM-6.3-3 deviates by following zit: the configuration request is reqconfig /config, the scalar marker zit's server answers, sent once in the clone's cleanup round and never on pull. The PARTIAL rows: a reference cycle cannot be built from content-addressed artifacts (SM-5.1-5); go-libfossil writes no tree, so the emission rules hold vacuously (SM-7.2-3, SM-8.1-1); and the visit bound is not reached by a case (SM-10-1). check-in: 6ccebaeab5 user: [email protected] tags: feat/submanifest-reader, main, trunk

20:57
feat(sync): carry sub-manifest trees safely (draft-fossil-submanifests-00)

Gate, announce and configure sub-manifest transfer the way zit does, so the two implementations interoperate and no fossil release is handed a tree it cannot read.

- 7.2 gate: once the repository has stored an S-bearing artifact, a tree manifest or sub-manifest is withheld from a peer announcing a release below 2.29.0, zit's threshold, with one explicit error card per reply and never by omission. The server applies it to gimme answers and clone batches; the client to gimmes on push, recording each withheld artifact in the result. - This library reads trees, so it announces 22900 in client-version and server-version; a peer holding trees refuses them to anything lower. No fossil release gates any behaviour between 22800 and 22900. - A ci-lock reply carries pragma avoid-submanifests, where avoid-delta-manifests goes, while enable-submanifests is off. - 6.3: the three settings are /project members on the server, which also answers zit's reqconfig /config with them. Clone asks for them as zit's clone does, once, in its cleanup round with zit's scalar marker; fossil answers that with nothing. A reply's settings are staged and applied all or none.

The examined-blob table's old revision is dropped only when present, so a sweep pays no extra write. check-in: 707ffd05a2 user: [email protected] tags: feat/submanifest-reader, main, trunk

20:50
fix(verify): report a tree waiting on a sub-manifest as a missing reference

A tree check-in whose sub-manifest has not arrived is deferred with no derived rows (draft-fossil-submanifests-00 5.2, 7.3). Verify reported that as a missing event row and missing plink rows, which reads as damage. Report it once as a missing reference naming the sub-manifest, and count it in MissingRefs. check-in: ffdf538656 user: [email protected] tags: feat/submanifest-reader, main, trunk

20:49
feat(manifest): read sub-manifest trees (draft-fossil-submanifests-00)

Parse the S card and the SUBMANIFEST artifact type, and read tree manifests through one enumeration seam.

deck: the S card (exact arity, decoded filename, raw 40/64-hex hash, strictly ascending run between R and T), type Submanifest reached only by reassigning a D-less manifest, its F S Z legality row, and the 4.4 checks: no B with S, no F with S in a manifest, F xor S in a sub-manifest.

manifest: checkinFileCards, ownFileCards and lookupFileCard are the one place a check-in's file set is assembled (9.3). A tree manifest is walked depth first with an explicit stack, a depth bound of 16, the repeated-artifact check and the least-filename and ordering checks of 5.1; lookup binary-searches one sub-manifest per level (5.3); a delta may name a tree as its baseline (5.4). A tree that cannot be walked is an error, never a partial file set (5.2). mlink derivation, R-card verification and the delta-manifest builder read through the seam, and a commit on a tree parent is written flat.

Crosslink defers a check-in whose tree cannot be walked, recording the blocking sub-manifest in orphan so the phantom-fill cascade links it on arrival (7.3, 9.4). Storing an S-bearing artifact reserves phantoms for its absent references with the referrer's privacy (7.1) and sets the seen-submanifest indicator (8.1). A sub-manifest is filed as examined so the sweep never reselects it (9.2). The examined-blob table is versioned, so verdicts a parser without S cards reached are dropped. check-in: f51becc770 user: [email protected] tags: feat/submanifest-reader, main, trunk

20:27
test(conformance): re-pin the -01 drafts with the 2026-10-14/16 errata (#436)

zit check-in 3f86ff4261 applies six more errata to the -01 drafts. Copy the three drafts and the five new -01 errata files, re-pin their SHA-256 in the requirements tables, and record the check-in in the README.

The draft text changes add six requirements and reword two:

- AF-4.7.3-2: a named delta's delete markers are filtered out of a delta-of-delta's file set and lookups. - AF-4.7.8-1: an absent and a present empty J value decode alike. - AF-6.2-7: the Z verification state is read from the bytes at n-35 and n-34, not from card lines. - RS-5.3-3: a cancel keeps its card's value on the row it writes. - RS-5.3-6: the event table and propagation treat that value as null. - RS-5.3-7: an artifact's tag cards apply in card order. - RS-6.2-4: a branch root carrying *sym-NAME and -sym-NAME keeps the tag. - SP-6.6-4: a client recognizes a refusal by its leading text.

go-libfossil already behaves this way. Each row has a case, checked against fossil 2.28 where the harness does: 421 requirements, 420 covered, 1 partial (SP-9.5-7, as before).

The repo-state §5.3 step 3 change is filed in zit as a -00 erratum and is not pinned, like the other -00 errata; the -01 draft carries its text. check-in: f61c8174a8 user: [email protected] tags: main, trunk

2026-10-13
17:32
chore(release): v0.12.0 changelog (#435) check-in: 7a25e35893 user: [email protected] tags: main, trunk, v0.12.0
16:51
fix(manifest): maintain the leaf table incrementally (RS-7.3-3) (#434)

* fix(manifest): maintain the leaf table incrementally (RS-7.3-3)

The leaf table was recomputed from scratch at the end of every crosslink batch, so a check-in lost its leaf verdict whenever a later batch linked an untagged child to it. repo-state-01 Section 7.3, as corrected, and fossil keep that verdict: a check-in and its plink parents are rechecked only when a branch tag is written to it or propagation updates or deletes its branch row (src/tag.c:195, :100-102), and the checks run when the batch ends (src/leaf.c leaf_do_pending_checks).

The rule now lives in a new internal/leaf package: Check (leaf_check), Rebuild (the explicit full rebuild) and Pending (the deferred schedule). internal/tag schedules a recheck at exactly the branch-row writes; the crosslink sweep, receive linker and repair gate apply a batch's schedule at the gate, each phantom-fill cascade transaction and each commit or tag transaction apply their own before commit, and verify.Rebuild keeps the whole-table recompute. Reparenting schedules nothing, as in fossil, so the reparent case is split into a one-batch case and a per-artifact case.

Tracked in #426.

* fix(manifest): recheck leaves a cancel reaches; drop rechecks of a failed receive tx check-in: 628f932301 user: [email protected] tags: main, trunk

16:10
test(conformance): refresh the corrected sync §9.4 erratum (#431)

zit check-in 00e1711e802e corrects the erratum's account of Fossil 2.28: forbid-delta-manifests defaults on only since 2.29, and 2.28 defaults it off. The draft text is unchanged. Re-pin the erratum copy and its SHA-256 in the sync requirements table, and record the check-in in the README. check-in: caa2797892 user: [email protected] tags: main, trunk

15:55
test(conformance): re-pin the -01 drafts with the 2026-10-13 errata (#430)

* test(conformance): re-pin the -01 drafts with the 2026-10-13 errata

Re-pin the repo-state and sync-protocol drafts to zit check-in 5a1b741b25d8, which applies five more -01 errata: repo-state §5.3, §7.3 and §8.3, sync-protocol §9.4/§9.5 and §10.1. zit now keeps each -01 text under the -00 file name; the copies keep their -01 names and conformance/rfcs/README.md records the zit path and check-in. The new errata are pinned, and the requirement tables are re-read against every changed hunk.

The §7.3 and §8.3 cases that were deviations now pass as written, and new cases cover a date tag's arrival order (§5.3), untagged leaves and a merge parent's recheck (§7.3).

The sync server answers each guard-passing ci-lock with pragma avoid-delta-manifests while forbid-delta-manifests is on, defaulting to on as the draft and fossil 2.29 do (fossil 2.28 defaults it off). The client leaves its transfer unchanged on that pragma. A file or cfile whose content does not match its hash now fails the client's round, in a pull and a clone: the reply rolls back, prefix included, and no further round starts.

The leaf table is still recomputed per crosslink batch, so a parent whose untagged child arrives in a later batch loses the verdict §7.3 keeps; that case is marked as a gap, tracked in #426.

* fix(sync): continue after a uvfile content error; renumber the avoid-delta-manifests row

A uvfile whose content does not match its HASH is a handler-reported content error: warn, commit the prefix and follow ordinary continuation (sync-protocol-01 10.1). The avoid-delta-manifests client row is SP-9.5-7 now that share-links took SP-9.5-5 and SP-9.5-6. Older unreleased changelog entries drop the pending-erratum wording. check-in: 600861484f user: [email protected] tags: main, trunk

15:33
chore: ignore .agent-config at any depth; drop a stray tool trace (#429)

.gitignore anchored .agent-config/* to the repository root, so a local tool's trace written under conformance/ was committed with the suite in #264. Remove it and ignore .agent-config directories wherever they are. check-in: fd0add2110 user: [email protected] tags: main, trunk

15:25
feat(sync): add share-links mode for pragma req-links and link (#428)

* feat(sync): add share-links mode for pragma req-links and link

sync-protocol-01 Section 9.5 makes share-links mode OPTIONAL. With SyncOpts.ShareLinks a pulling session sends pragma req-links in its first request, as fossil's --share-links does, unless SyncOpts.ProjectCode names another project. It keeps each pragma link URL ARG MTIME as config link:URL = ARG, defossilized and otherwise verbatim, replacing an existing link only with a strictly newer MTIME. A wrong token count, a URL that is not HTTP(S) or an MTIME that is not a positive decimal drops the link.

Where fossil 2.28 differs the draft is followed: fossil fails fatally on a link URL its parser rejects and reads MTIME with strtoll base 0.

New sync.9.5 client cases run against fossil sync --share-links as well. SP-9.5-4 loses its scope note and is covered; SP-9.5-5 and SP-9.5-6 are new rows for which links a client keeps and when it sends req-links.

Tracked in #425.

* test(sync): cover links in later rounds and push+pull req-links; match project code case-insensitively check-in: 5473c06942 user: [email protected] tags: main, trunk

15:15
fix(sync): answer a client clone_seqno with bad command; make Clone's phantom pull opt-in (#427)

* fix(sync): answer a client clone_seqno with bad command; make Clone's phantom pull opt-in

Two deliberate #255 deviations from sync-protocol-01 move to conformance.

SP-5.2-6: the server now answers a client-sent clone_seqno with "error bad command" and processes the cards after it, as Section 5.2 requires for a reply-only card and as fossil's page_xfer does.

SP-8.2-5: Clone now sends nothing after clone_seqno 0 and its cleanup round, so by default it leaves the phantoms a clone produces in place, as fossil(1)'s clone does. The follow-up pull that fetched them is now opt-in through CloneOpts.PullPhantoms, whose doc comment says enabling it departs from Section 8.2.

Both cases leave the sync deviations map. Tracked in #424.

* fix(sync): quote a client clone_seqno as received in bad command; keep DST coverage of the opt-in pull

Tracked in #424. check-in: d3fb5397c8 user: [email protected] tags: main, trunk

14:23
test(conformance): report deviations marked on repo-state API cases (#423)

rsAPICases copied each case's gap mark but never its deviation mark, so repostate.8.3.ckout-inclusive-whole-name, listed in repostateDeviations by #420, ran as an ordinary pass and the coverage report showed RS-8.3-4 as COVERED.

Copy the deviation mark too, and state that case the way the other deviation cases are written: it holds go-libfossil to the draft's answer (Section 8.3 retries "ckout." under ci, where ckout is a keyword, so it names the working files), checks fossil(1) against its own answer (the ckout tag's check-in), and is skipped as a deviation while go-libfossil follows fossil. RS-8.3-4 now reports DEVIATION until the 8.3 erratum is applied. check-in: e71b5c241f user: [email protected] tags: main, trunk

14:09
fix(sync): run each request, reply and clone round in one transaction (#422)

A received delta found wrong when its basis arrives later in the pass fails sync-protocol-01 Section 7.7's commit check, which now rolls back everything the pass stored, as fossil's page_xfer and client_sync transactions do, instead of undoing the delta by hand and keeping the rest. The phantom-fill cascades and the server's closing leaf and tag repair run inside that transaction. Any other card error still commits the prefix before it (Section 10.1); on the client that prefix now includes the reply's buffered igot phantoms and visibility changes.

db.DB.WithBoundTx binds a DB to one write transaction so code written against *DB, nested WithTx included, joins it as savepoints instead of contending for the write lock from a second connection.

SP-7.7-4 is covered; the new SP-10.1-6 has sync.10.1.integrity-error-prefix. Tracked in #418. check-in: c5f5838169 user: [email protected] tags: main, trunk

13:36
feat(resolve): checkout-inclusive request where ckout names the working files (#420)

* feat(resolve): checkout-inclusive request where ckout names the working files

Checkout.ResolveWorking is fossil's "ci+" request (repo-state-01 8.3): while the checkout has a version, "ckout" answers the working files, reported as WorkingVersion.Working rather than as a pseudo-RID. Every other name goes through the same resolver as Checkout.ResolveVersion, restricted to check-ins as fossil's ci+ is. The draft also makes ckout a keyword in a plain "ci" request; this follows fossil, where only ci+ has it.

New repostate.8.3 cases run the request against fossil test-name-to-id --type ci+, and RS-8.3-4 loses its scope note. Tracked in #419.

* fix(resolve): record the ckout retry as a fossil deviation; tighten tests check-in: 0889d8b777 user: [email protected] tags: main, trunk

12:56
fix(sync): verify a dangling delta when its basis arrives in the same pass (#417)

Tracked in #412. check-in: 056f36e0bc user: [email protected] tags: main, trunk

12:27
fix(sync): repair leaves and tags for check-ins the cascade links (#414) (#416)

* fix(sync): repair leaves and tags for check-ins the cascade links (#414)

A pulled check-in arrives as an announced phantom filled the next round, so the phantom-fill cascade links it. The session's closing sweep then found nothing left to link and skipped the leaf and tag-propagation repair, which gated on the sweep's own count: the check-in had an event row and no leaf row, and its parent stayed a leaf. The server handler had the same gap for a pushed artifact that fills a phantom.

AfterDephantomize and AfterDephantomizeBatch now return how many artifacts they linked. The sync client and the handler add that up over the session and close it with the new CrosslinkAfterCascades, which runs the repair once when the sweep or the cascades linked anything (repo-state-01 Section 7.3, RS-7.3-4), not once per file. The receive linker's cancellation paths gate on the same flag.

The repo-state harness gains a pull load: the first artifacts of a script are held and crosslinked locally, the rest pulled in process from a go-libfossil server holding them all. New cases repostate.7.3.pulled-child and repostate.7.6.pulled-retag, checked against fossil per artifact, each run as a .pull case too.

Tracked in #414.

* fix(sync): repair leaves of cascade links when a session fails (#414) check-in: 0035af8ac5 user: [email protected] tags: main, trunk

11:59
fix(manifest): link check-ins with absent files; refuse absent baselines (#415)

* fix(manifest): link check-ins with absent files; refuse absent baselines

The crosslink sweep held a check-in back until every file it names was stored. A root check-in, or one on a present parent, whose files never arrived got no event, plink, mlink, leaf or tag rows. fossil links it against phantom files (add_one_mlink, uuid_to_rid(..., 1)), and repo-state-01 Section 4.2 makes an unresolved reference a phantom, not an error. The sweep -- Crosslink, the close of every sync and clone, and verify.Rebuild -- now waits only on a delta manifest's baseline. The receive-time cascade and receive linker still wait for files while blobs are still arriving; the closing sweep bounds that wait.

verify.Rebuild linked a delta manifest whose baseline was absent, with no baseline id. fossil refuses it (fetch_baseline) and files it in orphan under a phantom for the baseline; artifact-format-01 Section 8.3 calls this a soft failure retried when the baseline arrives. Every path now does the same, so a sync requests the baseline and the phantom-fill cascade links the manifest when it lands. Rebuild clears and rederives orphan.

The gap case repostate.4.2.root-absent-file passes and is unmarked; new cases cover a present parent with an absent file and an absent or late baseline, each also through verify.Rebuild where fossil rebuild agrees.

Tracked in #406.

* fix(manifest): refuse a non-manifest baseline and drop the orphan row on link

Tracked in #406. check-in: 67fc6ced4b user: [email protected] tags: main, trunk

11:38
test(sync): mark dangling deltas whose basis arrives in the same pass (#413)

* test(sync): mark a dangling delta whose basis arrives in the same pass

SP-7.7-4 requires every artifact stored in a pass whose bytes are available in that pass to be checked against its HASH. A delta stored dangling is not rechecked when its basis arrives later in the same reply or request; fossil's commit hook catches it and rolls the round back. Add client, clone and server cases for it, marked as gaps.

Tracked in #412.

* test(sync): note how the server late-basis case must tighten when its gap closes check-in: 8f03734a23 user: [email protected] tags: main, trunk

11:14
fix(sync): verify a received delta against HASH before storing it (#410)

sync-protocol-01 Section 7.7 verifies a reconstructable non-clone file before storage, and SP-7.7-4 has every newly stored artifact whose bytes are available hashed before commit. The client and a clone stored a delta against a held basis as given, and the server checked only a file delta, so a delta that did not give HASH stayed stored under that name until something expanded it.

The check moves to storeReceived, the one choke point every receive path shares, so the client, the server (file and cfile) and a clone refuse a delta whose result is not HASH, or that does not apply. An unheld basis still leaves the delta stored dangling.

Tracked in #405. check-in: b3914d95b3 user: [email protected] tags: main, trunk

11:14
feat(repo): name the current trunk and a branch's open leaf (#411)

* feat(repo): name the current trunk and a branch's open leaf

Repo.CurrentTrunk resolves the main-branch setting's name the way ResolveVersion does, as fossil does for update outside a checkout and open with no version (repo-state-01 Section 9). Repo.BranchOpenLeaf names the open leaf of a branch with the greatest mtime, or reports the branch not open; fossil's branch info takes whichever open leaf its query meets first, and this follows the draft. checkout.BranchLeaves and the new checkout.BranchOpenLeaf share one open-leaf query.

RS-9-1 and RS-9-4 lose their scope notes and gain cases.

* fix(repo): resolve the current trunk as a check-in, reject an empty branch name

Tracked in the stacked work on repo-state-01 Section 9. check-in: c2795c4f48 user: [email protected] tags: main, trunk

11:05
feat(resolve): resolve checkout-relative names and branch-type requests (#409)

* feat(resolve): resolve checkout-relative names and branch-type requests

Checkout.ResolveVersion resolves current, prev/previous and next relative to the open checkout, after tip and before the other forms, as fossil's symbolic_name_to_rid does; a keyword that names nothing falls through. Repo.ResolveBranchStart is the branch-type request: it resolves a name and walks back to the first check-in of its branch. Section 8.3 walks back from every result; fossil only from tag:NAME and plain names, and this follows the draft.

The repostate.8.3 checkout and branch-type cases run against the new entry points, with new cases for a keyword shadowing a tag, prev at a root, an untyped ckout, root:current and a branch-type hash. Tracked in #386.

* fix(resolve): match check-ins only for branch-type requests, merge via checkout

A branch-type request is rewritten to a check-in request (repo-state-01 Section 8.3), so a technote tagged with the branch name cannot answer it. A checkout with no recorded version resolves the ordinary forms, and merge resolves its version through the open checkout. Tracked in #386.

* fix(resolve): wrap a long test line check-in: cba2d0c823 user: [email protected] tags: main, trunk

10:59
feat(repo): set the hash policy by name (#408)

artifact-format-01 7.1 (AF-7.1-2) matches policy value strings case-sensitively and exactly and makes an unrecognized value fatal. No surface took a value string, so the row stayed partial.

Repo.SetHashPolicy takes sha1, auto, sha3, sha3-only or shun-sha1, as fossil's hash-policy command does with fossil_strcmp, stores the ordinal, and refuses any other string without touching the stored policy. Auto in a repository that already holds a SHA3 artifact stores sha3 (7.3).

New conformance cases drive every name, wrong-case names, unknown names and the empty name through it, and the AF-7.1-2 scope note is dropped. check-in: feea6a1614 user: [email protected] tags: main, trunk

10:52
fix(manifest): link a check-in atop an absent parent despite absent files (#407)

* fix(manifest): link a check-in atop an absent parent despite absent files

The crosslink deferral held back a check-in until every F-card blob was stored. When the primary parent's manifest is itself absent, no mlink row depends on those files (fossil's add_mlink returns before reading one, and the parent's crosslink derives the transition once it arrives), so the wait could never complete for files the repository will not receive. The libfossil repository holds one such check-in, a fossil-scm commit imported without its history; the sweep lost its event and plink rows that fossil rebuild writes (RS-4.2-2).

missingCheckinRefs now skips the F-cards when the primary parent is unavailable; the baseline is still required. Adds conformance case repostate.4.2.phantom-parent-absent-file and a unit test that also checks the child's mlink is derived once the parent arrives.

Tracked in #398.

* fix(manifest): record the remaining crosslink deferral gaps

Adds gap case repostate.4.2.root-absent-file, a receive-linker ordering test for the child-before-parent path, and notes the narrowed exemption. Tracked in #398 and #406. check-in: 15762adeb6 user: [email protected] tags: main, trunk

07:32
fix(sync): stop the client reply loop at a card with no artifact name (#404)

* fix(sync): stop the client reply loop at a gimme or igot with no artifact name

fossil's client reads gimme and igot only with blob_is_hname; any other line is an unknown command that ends the reply-card loop, as sync-protocol-01 Section 5.2 requires of a malformed defined card. A server case pins the existing bad-command answer to such a gimme.

Tracked in #397.

* fix(sync): stop the client reply loop at a file or cfile with no artifact name

Tracked in #397.

* test(conformance): split the case tables this round grew past 70 lines

Tracked in #397. Each half keeps its cases in order; only the table functions move. check-in: 636e873e6a user: [email protected] tags: main, trunk