Skip to content
go-libfossil Timeline
Login

Timeline

13 check-ins that include changes to files matching '.github/workflows/*'

2026-10-03
19:00
ci(release): drop the downstream dispatch to EdgeSync (#251)

The release workflow's last step sent EdgeSync an upstream-bump event with a personal access token. It has failed with 401 on every release since v0.8.1 because the token expired, marking each release run failed after the release itself was published. EdgeSync also stopped accepting the event when the repository was renamed.

EdgeSync now tracks go-libfossil with Dependabot (danmestas/EdgeSync #199), which needs no token here, so the step is removed. A release run now succeeds once the GitHub release exists. check-in: af8e531e68 user: [email protected] tags: trunk

2026-07-29
19:19
fix(test): reap fossil servers before temp-dir cleanup; raise unit-test timeout (#220)

Three tests started a real fossil server and killed it with Process.Kill() without waiting for the exit. Kill signals but does not reap, so the server could still be writing its -wal/-shm files while t.TempDir()'s cleanup walked the directory. A file appearing mid-walk fails RemoveAll with "directory not empty", which is how TestCloneFromRealFossilUncompressedReply failed on CI while passing locally -- it needs a loaded machine to lose the race.

internal/sync/integration_test.go already did this correctly, Kill followed by Wait, with a comment naming the same failure. This applies that established pattern at the three sites that lacked it.

Separately, the unit-test step ran with -timeout=120s, a per-package budget. CI runs the package binaries concurrently, so under contention internal/manifest and internal/sync exceeded it and panicked with "test timed out" despite finishing in 19s and 34s on an unloaded runner. Raised to 300s, matching the DST step, so the margin does not depend on how busy the runner is. check-in: 2256f5d7ce user: [email protected] tags: trunk

01:46
feat(cli): select sqlite driver by tag (#218) check-in: ac4c3537d1 user: [email protected] tags: trunk, v0.8.0
2026-07-27
23:37
ci: gate the js/wasm build on every PR (#201) (#205)

test.yml had zero references to wasm or GOOS=js, so the WebAssembly build was never compiled on any leg. Browser support is the reason the ncruces driver is in the tree, and a break there would have shipped undetected -- including in db/config_wasm.go, whose js-tagged wasmPragmaOverrides() no job compiled at all.

Adds a build-only job covering 31 of the main module's 33 js-resolvable packages plus the ncruces driver submodule. The package set comes from go list rather than a hand-written list so new packages are gated by default. Two main packages are excluded, both for the same structural reason: cmd/libfossil and examples/embed-repo-api import the modernc driver directly in their own main.go, and modernc.org/libc has no js/wasm build. internal/stash and internal/undo declare //go:build !js in source and are dropped by go list itself. Building under -tags test_ncruces keeps internal/testdriver in the gate by pointing it at the driver that can actually target WASM.

Running tests under WASM needs a browser harness (wasmbrowsertest) and is deliberately out of scope; compilation catches the realistic failure mode. check-in: 10abca922c user: [email protected] tags: trunk

19:57
fix: pin submodules to v0.7.0 instead of an orphaned pseudo-version (#190) (#195)

All three submodules required the root module at v0.6.4-0.20260724230440-48111c1c984d. That pseudo-version names a commit pushed on the #118 PR branch, which was squash-merged — rewriting the work as a new commit and orphaning 48111c1. `git branch -r --contains` finds it on no branch; it survives only in proxy.golang.org's cache.

CI never noticed because it resolves through the proxy. Direct-from-git resolution fails outright:

$ GOMODCACHE=$(mktemp -d) GOWORK=off go mod download github.com/danmestas/go-libfossil go: github.com/danmestas/[email protected]: invalid version: unknown revision 48111c1c984d

v0.7.0 is tagged and reachable, so the bootstrap constraint that forced a pseudo-version is gone. Repoint all three at the tag and tidy. The root go.mod hash is unchanged between the orphaned commit and v0.7.0, so this is a pure provenance fix with no dependency movement.

Add a release guard alongside the existing `replace` guard: any in-repo module required at a pseudo-version now fails the release before the tag ships. The pattern anchors on the 14-digit timestamp and 12-hex hash, and accepts either separator before the timestamp so the vX.Y.Z-0.<ts>-<hash> form is caught as well as v0.0.0-<ts>-<hash>. Third-party pseudo-versions such as ncruces/go-sqlite3-wasm are deliberately untouched. check-in: 9d35407ecb user: [email protected] tags: trunk

13:59
ci: warm fossil's global config database once before the test steps (#177)

Fossil keeps a per-user global config database outside any repository and creates it lazily on first use. The fossil-gated tests invoke the binary with inherited env -- nothing sets HOME -- and go test runs packages concurrently, several of which shell out to fossil. On a fresh runner that database does not exist yet, so two test processes can race to create it: one wins and the others fail with "table global_config already exists".

Create it once, single-threaded, after the install and version check and before any build or test step, so no test process is ever the one to create it. fossil version does not open that database, so the existing version check does not incidentally cover this; fossil all list does, and is silent.

The warm-up command's exit status is ignored on purpose -- it exists to touch a file, not to assert anything. The file check that follows is the part that matters: a warm-up that silently stopped creating the database would leave the race live while looking like protection, so its absence fails the job loudly.

Refs #174

Co-authored-by: Dan Mestas <[email protected]> check-in: bba767af03 user: [email protected] tags: trunk

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

2026-07-24
13:59
ci: gate docs/site Hugo module with a build job (#148)

docs/site was the one module in the tree with no CI compile gate. It has its own go.mod but zero Go source of its own -- the module exists only to import the hextra theme via Hugo Modules -- so `go build ./...` there matches no packages and gates nothing (and `go vet ./...` fails on "no packages to vet"). The equivalent compile gate for a Hugo module is a Hugo build: it resolves the module import, compiles templates, and renders content, failing loud on a broken import or template.

Add a test-docs job that installs pinned Hugo Extended and runs `hugo --gc --minify` in docs/site, matching how the driver/observer submodules each get their own fail-loud step. check-in: be89e6397b user: [email protected] tags: trunk

2026-07-23
15:18
ci: build all packages so broken imports fail loud (#135)

The test step runs `go test $(go list ./... | grep -v '/dst')`. When a package fails to compile, `go list` prints an error to stderr and omits that package from stdout, so it silently drops out of the test run rather than failing CI. `go build ./cmd/libfossil/` only covered the CLI binary's dependency closure, which missed internal/deck when it broke.

Add `go build ./...` ahead of the test step on both driver-tag legs (modernc and test_ncruces) so any package that fails to compile is a loud, fail-fast build error. This subsumes the narrower cmd/libfossil build, so that step is removed rather than kept alongside. check-in: fa558f254d user: [email protected] tags: trunk

2026-07-22
23:03
fix: correct go.mod module path to go-libfossil (#76) (#117)

* fix: correct go.mod module path to go-libfossil (#76)

The module directive still declared github.com/danmestas/libfossil after the repository was renamed to go-libfossil. pkg.go.dev/godoc scraping matches the declared module path against its hosting path, so the stale path broke SDK doc generation.

Rename the module path across all four in-repo modules (root, the two db/driver submodules, observer/otel, and docs/site) and every internal import, plus the release workflow, the doc-generation script, and the docs.

No tag has been published under the go-libfossil path yet, so the cross-module requires cannot resolve from the proxy. Add replace directives pointing the in-repo requires at their local directories so GOWORK=off builds and the CI matrix resolve without a published version. These replaces are ignored by downstream consumers and can be dropped once new-path tags are cut.

* Guard releases against replace directives; regenerate SDK docs

The go-libfossil rename bridges the in-repo modules with `replace` directives. Go refuses a main module that carries them, so a tag cut while they are present breaks `go install .../cmd/libfossil@<tag>` and every `@version` fetch of the driver submodules. Add a Release workflow step that fails the job before anything is published if any tracked go.mod still contains a replace directive, so the bridge is a CI-enforced TODO rather than a silent landmine. The step comment records what unblocks a release: re-tag the submodules with the corrected module paths, bump the root requires, then drop the replace blocks.

Regenerate the SDK reference docs. The previous pass only substituted URLs over the pre-existing generated files, leaving stale signatures (Repo.Timeline was still documented as taking LogOpts rather than TimelineOpts). Note that gomarkdoc must be run outside a git worktree — inside one it silently strips every source link.

Also reword the README rename note, which read as though the import path had not changed, and re-sort the ncruces require block. check-in: 6273a0750d user: [email protected] tags: trunk

2026-07-19
20:55
Deltify predecessors on the commit path (#87)

* Deltify predecessors on the commit path

Every locally-created artifact was stored whole. The delta table was in the schema and stayed empty, so a repository grew with the number of commits rather than the amount of change. blob.StoreDelta existed and worked but had no caller on the commit path; the only delta-store in use was blob.StoreDeltaRaw, from the sync receive path, for deltas that arrive pre-encoded. The write path never made a deltify decision at all.

Port canonical Fossil's content_deltify (2.28 src/content.c:852-947) as content.Deltify. Fossil deltifies backwards: a new artifact is always stored whole and its PREDECESSOR is rewritten as a delta against it, so the tip of every chain is full content and reading the newest version of a file stays a single blob load. Chain depth becomes an artifact's age in revisions rather than something that compounds, because an artifact already stored as a delta is never re-deltified.

blob.StoreDelta turned out to be the wrong primitive to wire up: it inserts a new row as a delta, whereas the policy needs to rewrite an existing row. It is left alone. content.Deltify lives in the content package because it needs Expand, which blob cannot import.

The policy is stated once, in content.Deltify, with each rule's canonical line cited: the 50-byte floors, the 75% ratio, the never-redeltify rule that bounds depth, the private-into-public refusal, and the loop break that undeltas the source and then still declines the pairing. Two call sites pass pairs, mirroring canonical's: insertMlinkRow for file content (manifest.c:1562) and Checkin for the parent manifest (checkin.c:3234). insertMlinkRow is shared by the fresh-commit and crosslink paths, so file deltification cannot drift between them.

On a 12-revision, 4-file history: 60 artifacts, 44 delta-encoded (73%), p50 chain depth 4, max 11, and 23,994 content bytes measured as sum(length(content)) -- down from 58,973 with everything whole. A repository produced this way passes fossil 2.28 test-integrity (60/60 blobs, 0 errors) and fossil rebuild, which re-derives every table by walking the chains.

* Bound the deltify ancestor walk, require a transaction, harden tests

Review follow-ups on the deltification policy.

Bound the ancestor walk. It was a bare loop with no cycle guard: a sync peer can shape a cyclic delta graph, because blob.StoreDeltaRaw records delta(rid,srcid) links from the wire without a cycle check, and an unbounded walk would spin inside the write transaction. Guard with the seen-set idiom content.walkDeltaChain already uses, plus a step cap. In the current ordering IsAvailable runs first on both rids and follows the same links under its own depth bound, so Deltify declines before the walk begins -- but that protection is incidental to a different question, so the bound belongs to the loop that needs it. Both behaviours are now pinned: the walk reports a cycle, and Deltify declines an ungrounded source.

Move both size floors ahead of the walk. Canonical returns on a sub-50-byte target (content.c:877-885) before entering the candidate loop, so it never reaches the content_undelta on :902. Running the walk first meant a small target whose successor chained back to it would inflate the source to full content and then decline anyway -- a storage regression and exactly the divergence from canonical's layout the surrounding comment claims to avoid.

Take *db.Tx instead of db.Querier. The rewrite is two statements, and a blob holding delta bytes with no delta row expands as though it were full content: silent corruption rather than a detectable error. Canonical wraps the pair in db_begin_transaction (content.c:938-941); the signature now makes that non-negotiable at compile time. Both call sites already passed a transaction.

Three tests were named for behaviours they did not pin, and each is now verified to fail when the behaviour is reverted:

- The 75% ratio test had no assertion at all; deleting the rule left it passing. It now asserts the pair is declined and stays whole. - The loop-break test could not distinguish declining from proceeding, so the undelta-then-proceed behaviour corrected in the previous commit was untested. It now asserts the target is left whole. - The manifest test checked only "at least one", so a regression from 11-of-12 to 3-of-12 passed. It now checks each index and names the manifest that stopped being encoded.

Also cover the source-side 50-byte floor, which the tiny-artifact test could not reach because it made both artifacts tiny and returned at the target check first, and assert that Undelta restores blob.size while Deltify leaves it alone (content.c:761 vs :935).

Gate the fossil-binary test on REQUIRE_FOSSIL_BIN, set in CI. A skip is invisible without -v, so a run with no fossil binary was byte-identical to a pass -- for the one criterion our own tests cannot substitute for.

Document that blob.StoreDelta is not the commit path's primitive. It inserts a new row as a delta; deltifying rewrites an existing one. It has no non-test callers and the package doc advertised it as the delta-storing entry point.

* Document what the deltify loop-safety argument rests on

Two comments, no behaviour change.

The IsAvailable(srcRid) guard is ours, not canonical's: content.c guards only content_is_available(rid) and then calls content_get(srcid) unguarded. It is also load-bearing beyond phantoms, because it screens out the cyclic delta graphs that would otherwise reach the ancestor walk. Say both, at the guard.

And say at the walk why its seen-set never fires in practice, so that someone reordering the prologue meets the reason rather than concluding the bound is dead code. The note lived only in the test file, which is not where that reader is looking. check-in: 2368cad91a user: [email protected] tags: trunk

2026-04-30
14:37
feat(ci): add release workflow with downstream dispatch (#21)

On tag push (v*): 1. Create GitHub release with auto-generated notes from PRs/commits since previous tag. Source tarball auto-attached. 2. Dispatch repository_dispatch event to danmestas/EdgeSync to trigger a chore-bump PR there.

libfossil is a library — no binaries are published, so goreleaser is overkill. 'gh release create --generate-notes' provides equivalent auto-changelog and source archives in 5 lines.

Closes the cascade chain. Once merged, the next libfossil tag will fire PRs in EdgeSync (PR #5 receiver) and via that, in bones (PR #4 receiver). check-in: 38e4948972 user: [email protected] tags: trunk

2026-04-19
16:30
Initial open source release

libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil.

This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil.

Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ check-in: f5744359d3 user: [email protected] tags: trunk