10 most recent check-ins occurring on or after 2026-07-24 17:23:36.
More ↑
|
2026-07-26
| ||
| 13:15 |
ci: pin canonical fossil to 2.28 instead of the distro package (#173)
Both test jobs installed the canonical fossil binary with an unpinned `apt-get install -y fossil`. On the Ubuntu runner that resolves to 2.23, while all local real-fossil parity work is validated against 2.28 -- so CI was checking our output against a different reference implementation than development does. The distro package is not stable even at a fixed version string: it tracks different upstream check-ins over time, so two runs at nominally the same version can behave differently. Install the official upstream release tarball instead, pinned by version and by SHA-256 so it is a fixed artifact rather than a moving target. The version lives in one workflow-level env value both jobs reference, and a verification step asserts the installed binary actually reports it, so an upstream change fails loudly instead of drifting silently. This also explains the intermittent `table global_config already exists` failure recorded in #162. `global_config` is not part of the repository schema at all -- it is the only table in `~/.fossil`, fossil's per-user global config database, created lazily on first use. Under concurrent fossil invocations against a HOME that has no `~/.fossil` yet (a fresh CI runner, never a dev machine) 2.23 races on that CREATE TABLE; 2.28 does not. Reproduced at 24-way concurrency on a fresh HOME: 2.23 hit the error in 30/30 trials, 2.28 in 0/30, and 0/60 at 48-way. Our schema creation is not implicated. 1500 iterations of TestCreateRepoSchema_FossilValidation against 2.28 (500 in-process plus 200 separate processes per driver variant) and 100 full `./db/...` runs passed with zero failures, and 40 independently created schema artifacts across both drivers produced byte-identical DDL. Closes #162 check-in: 9ff6e07d74 user: [email protected] tags: trunk | |
| 00:03 |
perf: cache prepared statements in db.Querier to stop per-call SQL re-parse (#171)
The hot per-artifact queries in the phantom-fill crosslink cascade — the content/blob-by-rid lookup and the per-node orphan/delta-child lookups in afterDephantomize — re-tokenized, re-parsed, and re-compiled their SQL text on every call because db.Querier delegated straight to the raw driver each time with no statement reuse. On a large repository (67k+ artifacts) the SQLite parser/compiler dominated CPU during the slow clone stretch. Add a statement cache to both Querier implementations, keyed by exact query text, with lifetimes matched to each backing type: - *DB caches *sql.Stmt prepared from its long-lived *sql.DB pool (safe for concurrent use, manages its own connections). Statements are prepared lazily on first use, reused thereafter, and closed in DB.Close. - *Tx caches *sql.Stmt scoped to a single *sql.Tx; the cache is torn down (all statements closed) when the transaction commits or rolls back, so no statement outlives its transaction or leaks across a clone/sync session. Exec/QueryRow/Query transparently get-or-prepare the cached statement — no signature or caller-visible behavior change. QueryRow preserves database/sql's deferred-error contract by falling back to the raw path on prepare failure so the error still surfaces from Scan. Multi-statement strings (DDL setup batches) bypass the cache and run through the raw driver path, since a prepared statement compiles only one statement. Benchmark (Apple M4 Pro, modernc driver): the blob-by-rid lookup drops from 4779 ns/op to 2289 ns/op (~2.1x). Cache reuse, transaction-boundary cleanup, and multi-statement bypass are covered by new tests in db/stmtcache_test.go. check-in: 218ebaec74 user: [email protected] tags: trunk | |
|
2026-07-25
| ||
| 15:31 |
fix: thread sync context into the general push/pull phantom-fill cascade (#167) (#169)
storeReceivedFile's mid-round AfterDephantomize crosslink cascade became context-aware in #166, and the clone and server-push receive paths already pass their real deadline context into it. The third caller -- the general push/pull path -- did not: (s *session) handleFileCard hardcoded context.Background(), so an ordinary Sync could not interrupt a long-running crosslink cascade via its caller's deadline the way a Clone now can. Thread the sync loop's own context the one remaining hop: - processResponse gains a ctx parameter and passes it to handleFileCard. - handleFileCard gains a ctx parameter and hands it to storeReceivedFile instead of context.Background(). - Sync's round loop passes its ctx into processResponse. Existing processResponse callers in tests pass context.Background(), the convention already used elsewhere. A new regression test drives the general sync path (processResponse) with a context cancelled mid-cascade against a fanout large enough to cross crosslinkCancelCheckStride and asserts the cascade stops early rather than crosslinking every child. check-in: 5ef8deb561 user: [email protected] tags: trunk | |
| 15:07 |
fix: make the mid-round phantom-fill crosslink cascade honor the clone deadline (#168)
AfterDephantomize crosslinks a just-filled phantom and walks the whole orphan/delta graph reachable from it. It runs synchronously mid-round as a clone stores received files (sync.storeReceivedFile), before the round returns and long before the post-round CrosslinkContext sweep that already observes the deadline. That walk took no context and was bounded only by a 1,000,000- iteration safety cap, so on a large graph a clone could run for many multiples of its configured deadline with the process busy inside this exact call chain and no cancellation ever reaching it (#166). Thread context.Context through AfterDephantomize, afterDephantomize, and crosslinkSingle, and poll ctx.Done() inside the work-stack loop every crosslinkCancelCheckStride pops -- the same stride-batched check linkBatch already uses in the whole-repository sweep. On cancellation the walk returns what it has crosslinked so far, matching how the batch sweep abandons its remaining candidates. The clone path (cloneSession.handleFile) and server push handler (handler.handleFile) pass their real request context through; the general sync loop, whose deadline is not threaded to handleFileCard, passes context.Background() to keep its behavior unchanged. This is an additive, disjoint cancellation path: the post-round batch sweep and its existing cancellation tests are untouched. Regression test TestAfterDephantomizeObservesCancellationMidWalk builds a root-plus-fanout delta graph larger than the stride and drives the walk with a context that goes live -> cancelled mid-walk, asserting the cascade stops early instead of crosslinking every node. check-in: a633f335c9 user: [email protected] tags: trunk | |
| 02:15 |
fix: decode sync replies by the wire's real Content-Type, not a hardcoded assumption (#165)
The public byte-oriented Transport discarded the reply's Content-Type, and the adapter bridging it to the internal xfer decoder hardcoded the §4.1 compressed container framing unconditionally. That assumption holds only when both peers are this implementation (Message.Encode always emits the compressed container). Against a peer that legitimately replies uncompressed -- a real Fossil clone-v3 reply uses plain card text -- the adapter read the first four bytes of the card text (`prag` from `pragma server-version`) as a big-endian length prefix and failed on exchange round 0 with "declared container length ... exceeds ... bytes". Add an optional FramedTransport interface a Transport may also implement to report the framing signal (e.g. an HTTP Content-Type) it observed on a reply, via a per-call RoundTripFramed return value -- no shared mutable state, so a reused transport stays safe. httpTransport implements it by returning the HTTP response Content-Type. transportAdapter.Exchange decodes by that signal when the transport reports one and otherwise falls back to the compressed framing, so MockTransport / TransportFunc and any fixed-framing peer transport keep working unchanged. The low-level xfer.Decode framing selection is unchanged. No trial-and-error framing detection is reintroduced. check-in: 21a447e620 user: [email protected] tags: trunk | |
| 01:19 |
chore: drop go.mod replace directives, unblock release (#161)
* chore: bump driver submodule requires, drop root replace directives The db/driver submodules now have real tags under the corrected go-libfossil module path (v0.3.0 for modernc and ncruces). Root no longer needs to resolve them from local directories via replace. go mod tidy also cleared the stale old-path (danmestas/libfossil) lines the earlier replace bridge left behind in go.sum. * chore: drop submodule replace directives, pin root require to a pseudo-version The three submodules required the root module (which they import directly or transitively) but no root tag has ever existed under the corrected go-libfossil path -- every prior tag (v0.1.0 through v0.6.3) still declares the old danmestas/libfossil path in its own go.mod, so pinning to any of them would reproduce the exact declares-as-X-required-as-Y failure the module rename was fixing. Pinned each submodule's require on root to a pseudo-version of the just-pushed go.mod cleanup commit (48111c1) instead of a tag -- the standard resolution for this bootstrapping case. This is a stopgap: once the next root release tag exists, these can be bumped to point at it directly instead of the pseudo-version, but there's no functional difference in the meantime. go mod tidy on each submodule also dropped now-unneeded indirect requires (modernc/ncruces don't actually import each other; the requires existed only to satisfy the local replace directives) and cleared the stale old-path go.sum lines. All four go.mod files are now free of replace directives. check-in: 2de18e7747 user: [email protected] tags: trunk | |
| 00:33 |
manifest: emit delete rows for omitted files and fix rename pid (#157) (#163)
insertCheckinMlinks walked only the child's F-cards, so two cases diverged from canonical Fossil's add_mlink: 1. A file present in the primary parent but dropped by OMISSION from a full (non-delta) child manifest got no mlink row at all. Canonical emits a delete row (fid=0) for it. A new second pass, insertOmittedDeletions, compares the primary parent's file set against the child's effective tree and emits a fid=0 delete row for every parent file the child no longer carries and named in no F-card. 2. A rename resolved its pid against the NEW path, which is absent from the parent, so pid defaulted to 0 as if the file were a fresh add -- losing pre-rename ancestry and defeating content_deltify. The F-card walk now resolves the parent lookup against the OLD path (OldName) for a rename, so pid is the parent's pre-rename blob rid. Verified byte-for-byte against the fossil 2.28 binary (TestFossilBinaryMlinkParity_DeletionAndRename): canonical emits BOTH a rename row keyed on the new name (pfnid = old name) AND a delete row for the vacated old name, both carrying pid = the old-path blob rid. The omission pass reproduces that pair, and applyFileCard now vacates a rename's prior name from the effective tree so the old name is correctly seen as dropped in delta manifests too. check-in: 9fed05730c user: [email protected] tags: trunk | |
|
2026-07-24
| ||
| 22:17 |
deck: strip PGP/SSH clearsign framing before VerifyZ (#156) (#160)
VerifyZ validated a control artifact by hashing everything up to the last 35 bytes and comparing against the Z card it expected to find there. When a manifest arrived PGP-clearsigned, the raw blob carried a trailing "-----BEGIN PGP SIGNATURE-----" ... "-----END PGP SIGNATURE-----" block after the Z card, so the last-35-byte window landed inside the signature framing instead of on the Z card, and verification failed. deck.Parse propagated that failure, so ~95 of Fossil's own 2007-era clearsigned check-ins could not be parsed -- and any child whose primary parent was one of them lost its parent's file tree in loadPrimaryParentFiles, cascading into all-added mlink data (~98 affected children on the real corpus). Mirror fossil's src/manifest.c remove_pgp_signature: strip the armor header (up to the blank line that ends it) and the trailing signature block, isolating the inner content the Z card is actually computed over, then verify and parse that. The stripping is an identity on non-clearsigned input (the common case) and idempotent. The artifact's content-addressed hash is taken over the raw blob elsewhere and is untouched by this change: a clearsigned manifest still resolves to the same UUID it always has. Verified against the real Fossil check-in 61829b07 (aku, 2007-11-29), whose raw-blob SHA-1 still equals its UUID and whose inner Z-card MD5 now validates; cross-checked byte-for-byte against fossil 2.28. check-in: c1cbd26795 user: [email protected] tags: trunk, observer/otel/v0.2.0, db/driver/ncruces/v0.3.0, db/driver/modernc/v0.3.0 | |
| 17:51 |
manifest: run leaf/tag repair on interrupted crosslink sweep (#159)
* manifest: run leaf/tag repair on interrupted crosslink sweep crosslinkSweep skipped the repairLeafTable/repairTagPropagation pass whenever linkCandidatesInOrder returned an error, including a context cancellation. After #89's batched sweep, batches commit as they go, so a sweep cancelled after committing at least one batch left the plink edges and tagxref origins those batches wrote without their leaf/tag consequences -- stale until a later sweep linked something, and a later sweep whose remaining candidates all link zero artifacts skips the same gate and leaves the window open longer than one sweep. Gate the repair on linked > 0 rather than on sweep success and return the sweep's own error after repairing, so an interrupted-but-committed sweep still recovers derived leaf/tag state on the way out. The repair runs in its own context-free transaction, so a cancelled ctx interrupts the candidate sweep but not the recovery, preserving the batched commit/rollback consistency guarantees. Closes #143. * manifest: join repair error with sweep error on interrupted sweep When both the sweep is interrupted and the deferred leaf/tag repair pass fails, the repair error was dropped entirely -- only the sweep error was returned. A real repair failure (disk full, corruption, constraint violation) during a cancelled sweep would then persist the exact stale leaf/tag window this fix closes with no signal. Join both with errors.Join so the repair failure is surfaced while errors.Is(err, context.Canceled) still holds for the three callers (clone.go, session.go, handler.go), all of which wrap with %w and none of which use exact equality. check-in: 7bc832f339 user: [email protected] tags: trunk | |
| 17:23 |
manifest: fix crosslink's WAL/availability/mlink working-set problems (#158)
* manifest: batch crosslink, memoize availability, preload mlink parents (#89) Three throughput fixes for the whole-repository crosslink sweep, none of which change its observable output. Profiling the sweep on the Fossil SCM corpus (67,503 blobs, 27,084 events) after the delta-chain-order work (#126) found the earlier bottlenecks had moved on: - Per-candidate transactions made every SQL statement open its own WAL read-transaction, whose setup cost climbs as the uncheckpointed WAL grows -- throughput collapsed from ~200 events/sec to ~7 within a few seconds of a large sweep. The sweep now commits in batches of crosslinkBatchSize candidates so SQLite checkpoints and truncates the WAL between batches, holding read-transaction cost flat. Batch-granular durability is correct for derived data: an interrupted sweep leaves committed batches and the candidate query re-selects only what is still unlinked. CrosslinkContext is split into crosslinkSweep / collectCrosslinkCandidates / linkCandidatesInOrder / linkBatch, with the per-artifact dispatch pulled into linkArtifact and the deferral rollup into deferCheckin / logDeferredCheckins. - missingCheckinRefs walked a blob's whole delta chain (content.IsAvailable) once per F-card of every check-in to decide deferral -- ~40% of the sweep, re-walking the same file chains over and over. content.AvailabilityCache memoizes the verdict per rid and stops a walk at the first already-decided ancestor, bounding total walk work by the number of distinct blobs rather than F-card references. - resolveMlinkParent looked up each F-card's parent version with SELECT ... FROM mlink WHERE mid=? AND fnid=?, which SQLite answers via the fnid index -- returning a file's entire crosslinked history, so the lookup grew as the mlink table did. loadMlinkParents now reads each parent's file set once with a single mid-indexed scan and resolves every F-card from the in-memory map. It reads exactly the same rows, so the mlink output is unchanged. The crosslink helpers now take the batch's *db.Tx directly (replacing r *repo.Repo + per-helper r.WithTx), matching the single-transaction shape of internal/verify.Rebuild and letting a whole batch commit atomically; tag application routes through tag.ApplyTagWithTx. updateAttachmentComments materializes its cursor before writing so a single connection never runs two statements at once. crosslinkSingle (dephantomize) wraps its one artifact in its own transaction. Whole suite green (1235 tests, 35 packages, incl. dst and the fossil-binary comparison). This does not by itself land #89's "minutes not hours" target; the dominant remaining cost is a separate, pre-existing mlink over-insertion documented on the issue. * manifest: diff crosslink mlink against the parent manifest (#89) insertCheckinMlinks emitted one mlink row per F-card and read pmid/pid from the parent's mlink rows. On the Fossil SCM repository that is ~180x canonical's row count (a full sweep would write ~19M rows against fossil's own 104,880), and because delta-chain order (#102/#126) routinely crosslinks a child before its parent, the parent's mlink rows were usually absent, so ~97% of rows defaulted to pid=0 -- wrong parent-column data independent of the volume. Rewrite it to diff each check-in's file list against its primary parent's expanded manifest -- the same diff canonical Fossil computes in add_mlink (src/manifest.c) and the rebuild path already does in internal/verify.rebuildMlinks -- and emit a row only for a file that was added, modified, deleted, or renamed. pmid/pid now come from the parent MANIFEST, which is always present once a check-in is eligible to link, so pid is correct regardless of crosslink order. The parent expansion runs through the sweep's shared content cache, so each parent's delta chain is walked at most once across all its children. Measured on the Fossil SCM corpus: 127,775 mlink rows (canonical: 104,878) with pid populated on 74% of rows (was ~3%); full-repo crosslink completes in ~15 minutes rather than the prior ~hour. The residual gap is 95 PGP-clearsigned parent manifests deck.Parse cannot yet strip, whose ~98 children fall back to the old all-added behavior -- a pre-existing deck limitation tracked separately. check-in: 81fbc0be5f user: [email protected] tags: trunk | |