48 ancestors of 9776c25462dce8dd
|
2026-07-19
| ||
| 18:17 |
Remove unused CloneOpts.ProjectCode and ServerCode fields (#77)
Both were accepted by the public CloneOpts but never forwarded to the internal clone path, so setting either silently did nothing. The internal clone options struct has no matching fields at all, so wiring them would mean designing semantics first — is a caller- supplied code a validation assertion or an identity override? — and no caller has asked for either. Delete them rather than guess. CloneResult.ProjectCode and CloneResult.ServerCode are unrelated and unaffected: they are populated from the internal clone's return value (the remote, via protocol negotiation), not from caller options, and continue to work exactly as before. Consistent with the same call made on UpdateOpts.Force. check-in: 9776c25462 user: [email protected] tags: trunk | |
| 17:40 |
Remove unused UpdateOpts.Force field (#69)
Force was accepted by the public UpdateOpts but never forwarded to the internal update path, so setting it silently did nothing. Delete it rather than guess at forcing semantics; a real implementation can be designed and added later as a deliberate, wired field if a caller needs it. Rides along with the UpdateResult signature change already landing on Update in this cycle. check-in: cd674f058b user: [email protected] tags: trunk | |
| 16:50 |
fix: populate mlink parent columns on the Crosslink check-in path (#63)
* fix: populate mlink parent columns on the Crosslink check-in path insertCheckinMlinks (the xfer/Crosslink ingestion path) wrote mlink rows with only (mid, fid, fnid) set, leaving pmid/pid NULL. Fossil's compute_fileage filters mlink.fid != mlink.pid, and in SQLite <int> != NULL evaluates to NULL rather than true, so every row written by this path was silently dropped from ancestry-walk queries - `fossil ls -R <repo> -r <rev>` returned empty for any commit that arrived over the wire, even though `fossil open` worked fine (it reads the manifest blob directly and never consults mlink). Root cause: the two check-in paths that write mlink both needed to implement the same three-case pid convention (src/manifest.c:1668-1679 in canonical Fossil - carried-over/added/added-by-merge) and had drifted. insertMlinks (the direct check-in path) only handled two of the three cases and ignored merge parents entirely; insertCheckinMlinks didn't populate the parent columns at all. Factor the convention into one place (internal/manifest/mlink.go): resolveMlinkParent implements the pid/pmid rule, insertMlinkRow wraps it and also fills pfnid (rename tracking) and mperm (permission encoding), which neither path wrote before. Both insertMlinks and insertCheckinMlinks now call insertMlinkRow, so the two write paths produce indistinguishable rows and cannot drift apart again. insertCheckinMlinks also stopped skipping F-cards with an empty UUID (the delta-manifest deletion marker) - it now writes a row with fid=0, matching canonical's "deleted files have mlink.fid=0" and letting deletions participate in ancestry queries the same way Fossil's own writer does. Un-gates ls_branch_tip_repro_test.go (previously untracked, guarded behind LIBFOSSIL_LS_BRANCH_TIP_REPRO) as a permanent regression test, and adds explicit coverage in internal/manifest/mlink_test.go for all three pid cases (normal add, merge add, delete) on both write paths. * test: pin the deleted-file pid to the parent's exact file rid Strengthens the deleted_file_gets_fid_zero subtest added for #29: it was only checking pid != 0, which would pass even if resolveMlinkParent picked the wrong nonzero value. Compare against the primary parent's actual mlink.fid for the same filename instead. * fix: review findings on #29 — substring perm match, test isolation Blocking: permToMperm switched on the whole perm string, matching only exact "x"/"l". Canonical manifest_file_mperm (src/manifest.c:1482-1492) does a substring test (strstr), and F-card perm fields can carry more than one character — internal/deck/parse.go:194 assigns the field verbatim from remote xfer input, and Fossil itself emits multi-char perm strings (the " w" rename placeholder, see #51). An exact match on e.g. "wx" would silently drop the executable bit, the exact invariant PR #48 landed to protect. Switch to strings.Contains, x checked before l to match canonical's order (the same idiom already used at internal/checkout/vfile.go:52). Added TestPermToMperm_SubstringMatch, including a "wx" case pinned to mperm=1. Test isolation: rowFor in both mlink_test.go table-driven tests closed over the parent *testing.T, so a Fatalf in one subtest's row lookup aborted every subtest after it via runtime.Goexit unwinding the shared parent goroutine. Pass each subtest's own t into rowFor instead. Proven on the reviewer's revert run: after the deletion subtest's Fatalf, the merge-add and normal-add subtests never even started. Tightened the carried-over-file assertion in TestInsertMlinks_MergeParentsGetPidNegativeOne from "pid != 0" (would pass for -1 too) to pin the exact parent fid, matching how the crosslink-path twin test already asserts it. Documented (comment only, no behavior change) a known divergence from canonical in resolveMlinkParent: a file present in a merge parent with different content (a conflict resolved during the merge) should stay pid=0 in canonical Fossil, but this function returns pid=-1 for any file present in a merge parent regardless of content match. Low blast radius — every mlink consumer in this package filters pid != fid, which holds either way — but worth flagging so it isn't rediscovered as a surprise. check-in: 24c35dad87 user: [email protected] tags: trunk | |
| 16:31 |
checkout: Update returns UpdateResult so conflicts are visible (#61)
* checkout: Update returns UpdateResult so conflicts are visible The internal 3-way merge already tracked which files were written, removed, and left with conflict markers, but Checkout.Update discarded all of it and returned a bare error. That collapsed a clean update and one that silently wrote <<<<<<< markers into working-tree files into the same outcome, with no way for a caller to tell them apart. Update now returns (UpdateResult, error). UpdateResult.Conflicted lists paths that were merged but not cleanly -- this is still a successful update (err == nil), not an error, since the working tree is usable and the caller just needs to know which files to show a user. A genuine failure still returns a non-nil error paired with a zero-value UpdateResult. FilesWritten/FilesRemoved are populated the same way, as paths rather than counts. Threading the paths up required changing the internal update's processFileUpdates from returning bare int counts to returning the actual []string paths, since the count was all that existed before. Checkout.Extract is untouched -- it has no conflict concept. Breaking change, clean break at v0.7.0 (pre-1.0). * checkout: sort Update result paths, document FilesWritten/Conflicted overlap Update's result slices were built by ranging over maps.allNames, a map[string]bool, so their order was whatever Go's randomized map iteration happened to produce that run. A caller diffing result.FilesWritten against a golden file, or asserting on it with reflect.DeepEqual, would see it fail intermittently for no reason related to their code. Sort all three slices (sort.Strings) right before Update returns. Added a test that touches four files -- two conflicting, two clean additions, named so map iteration order is very unlikely to hand them back sorted by luck -- and asserts the exact sorted slice on both FilesWritten and Conflicted. Verified it fails reliably (5/5 runs) with the sort calls temporarily removed, then restored the fix. Also stated outright, in both the internal and public UpdateResult doc comments, what "clean or not" was only implying: every path in Conflicted also appears in FilesWritten, since the conflict markers are what got written there. check-in: 75f3521e54 user: [email protected] tags: trunk | |
| 16:30 |
fix: make Timeline pagination cursor an exact total order (#62)
* fix: make Timeline pagination cursor an exact total order #54 (merged as 95cbbb7) split Repo.Timeline into a repository-wide enumeration and Repo.Ancestry, the original first-parent walk under an honest name. Its pagination cursor predicate used a tolerance window around the caller-supplied Before/After pair to work around a lossy julian<->time.Time round trip: mtime < (C - eps) OR (mtime <= (C + eps) AND rid < After) This reintroduces exactly the defect the composite cursor existed to prevent, and makes it worse: rows within eps of the boundary that are not real ties get filtered by rid instead of tie-broken by it, which can skip a row, duplicate another, or fail to advance the cursor at all — unlike canonical fossil's own bare-timestamp slop, which repeats rows but still advances. A pagination test using only exact-tie and one-hour-apart fixtures could not exercise the 0 < delta < eps range where the defect lives, so it stayed green. Fix: make the predicate an exact total order, with rid as a true tie-break only at exact mtime equality: mtime < C OR (mtime = C AND rid < After) and stop deriving C from a time.Time altogether. TimelineOpts.After is now an opaque Cursor (internal/fsltype.Cursor, re-exported as libfossil.Cursor): obtained only from a LogEntry the library already produced, carrying the exact float64 mtime read off that row rather than one reconstructed from a rounded timestamp. A cursor built from parts can silently drift from the row it claims to follow; a cursor that can only ever be handed back cannot. That exactness depends on the row's scanned mtime being faithful to begin with, which surfaced a second bug: under the ncruces/WASM sqlite driver, event.mtime scans as a time.Time via the driver's own internal REAL conversion, carrying ~13us of noise that db.ScanJulianDay's time.Time branch then truncated (via UnixMilli()) into a full missed millisecond — independent of and in addition to the JulianToTime truncation bug #54 already fixed on the read-back side. Fixed db.timeToJulian to round to the nearest millisecond, the same class of fix, at the point where the driver-specific noise actually enters. Also tightened the regression coverage: - Pagination tests now use a fixed loop bound of exactly ceil(n/pageSize) pages rather than an open "while non-empty" loop, so a non-advancing cursor fails the test immediately instead of hanging it. - Added a sub-millisecond-boundary fixture (events 3ms apart, well inside the old epsilon window) with rid order deliberately decoupled from mtime order — a monotonic fixture cannot expose this class of bug, since rid and mtime need not move together in general (backdated commits, imports). - TestJulianRoundTrip now asserts exact equality for values originating from TimeToJulian instead of a *1ms tolerance, which would have passed under both the old truncating JulianToTime and the current rounding one and so was not a regression guard for that fix. No change to Ancestry, event-type filtering, NULL tolerance, or either loop bound; all were already correct. * fix: assert pagination termination; mark Cursor change breaking Both pagination tests (the pre-existing ones and the sub-millisecond regression) ran exactly ceil(n/pageSize) pages and asserted each event appeared once, but neither ever called Timeline one more time to confirm the next page comes back empty. The headline symptom of the epsilon-cursor defect these tests exist to catch was non-termination — a caller paginating until empty never stops — and that specific failure mode was going unguarded: a cursor that enumerates every row correctly but never signals exhaustion would have passed all four tests as they stood. Added one more Timeline call after the last expected page in all four tests, asserting it returns zero entries. Verified this is not vacuous: reverted the tie-break to <= (non-strict) and confirmed the existing per-event count checks catch it; separately, made Timeline phantom-return one row only on an already-exhausted call (isolated from the real per-page counts, which stayed correct in that experiment) and confirmed only the new final-page assertion fails — proving it exercises a real, non-redundant path, not one already covered by the count checks. Both experiments reverted before commit. Also corrected the CHANGELOG bullet describing the Cursor redesign: it described the new Cursor type but never stated that TimelineOpts.Before (time.Time) and TimelineOpts.After (FslID) were deleted outright, not deprecated. A reader diffing releases would have read it as an addition rather than a second compile-breaking change on top of #54. check-in: 168e20fc91 user: [email protected] tags: trunk | |
| 16:00 |
fix: split repository enumeration from ancestry walking (#45) (#54)
Repo.Timeline was an ancestry walk wearing an enumeration's name: it required a start rid, emitted that check-in, followed plink where isprim=1 to the primary parent, and repeated until the chain ended. It never touched the event table and never visited a second parent or a sibling branch head, so it silently omitted any check-in that wasn't a first-parent ancestor of the given start rid — on libfossil's own repository that was 577 of 3,761 check-ins, with no error and no warning. The limit option never bound because the (wrong) walk exhausted first. Split into two operations under honest names: - Repo.Timeline(TimelineOpts) enumerates the event table newest first, every kind by default or a single kind via Type. This matches canonical `fossil timeline`'s -t/--type default (a filter only applies when given and not 'a'). - Repo.Ancestry(LogOpts) keeps the original first-parent walk, unchanged in behavior, under a name that says what it does. Breaking change, no deprecated shim (pre-1.0, clean break): callers of the old Timeline(LogOpts) need to move to Ancestry(LogOpts) if they want the walk, or to Timeline(TimelineOpts) if they want the enumeration fossil's own timeline command performs. The only in-tree callers were the `repo timeline` CLI command and the two examples; all three are migrated to Timeline(TimelineOpts). See CHANGELOG.md. LogEntry gains a Kind field identifying which of event.type's six kinds (ci, e, f, g, t, w) an entry is; Parents is only populated for check-ins, since plink only relates check-in artifacts. Timeline's pagination cursor orders by (mtime DESC, rid DESC) instead of canonical fossil's bare mtime DESC with no tie-break — a deliberate improvement, not an oversight, since fossil's web cursor combines that missing tie-break with a one-second timestamp slop and can repeat or skip rows at a page boundary. Getting the composite cursor to actually round-trip losslessly required fixing a real bug along the way: JulianToTime truncated instead of rounded when converting a julian day back to milliseconds, so TimeToJulian(JulianToTime(m)) was not always equal to m even for exact-millisecond inputs — enough to misplace a row at a page boundary under exactly the same-mtime conditions this issue's acceptance criteria test for. Fixed to round to nearest. Because the same class of drift also appears in the ncruces/WASM sqlite driver's own internal REAL-to-time.Time conversion (independent of this library's code), the cursor comparison also carries a small explicit tolerance rather than relying on bit-exact equality across drivers. Both the ancestry walk's loop and the enumeration's row loop now carry an explicit, testable bound so a corrupt or cyclic plink table can't hang the process. check-in: 9a2697c556 user: [email protected] tags: trunk | |
| 16:00 |
fix: separate blob existence from blob availability (#43) (#52)
* fix: separate blob existence from blob availability (#43) A phantom is a real blob row with size = -1 and NULL content, so blob.Exists — "SELECT rid FROM blob WHERE uuid=?" — answers true for it. Every guard built on Exists therefore admitted artifacts whose content cannot be read, and content.Expand aborted with "rid N is a phantom". Canonical Fossil keeps two predicates and never substitutes one for the other: rid_from_uuid (existence, xfer.c:70) and content_is_available (availability, content.c:163). The latter is transitive — it follows delta.srcid up the chain, so a delta whose base is a phantom is unavailable even though every row in the chain exists. Add content.IsAvailable / content.AvailableByUUID, ported from content_is_available including the chain walk, with an explicit depth bound so a cyclic delta table terminates instead of hanging. The walk lives in exactly one place; no caller touches the delta table. blob.Exists keeps its behavior and gains a doc comment stating that it answers existence only, returns true for phantoms, and naming the availability predicate as the alternative. manifest.blobPresent is deleted. It was a local patch at one call site that tested size >= 0 without the chain walk, so it still reported a delta-on-phantom as present — the same class of bug it was written to work around. Every blob.Exists call site was classified as existence or availability and converted where it guards a content read. Sites that want rid identity — plink/mlink/tagxref population, cluster phantom creation, private-status bookkeeping, the post-store sanity check in handleFile, and phantom filling in storeReceivedFile — keep existence, since delta-before-base is a normal steady state during clone. Verified against a loopback fossil 2.28 server: the 23-artifact repro that previously failed roughly half the time now clones 6 of 6. * fix: IsAvailable must fail closed when the delta lookup errors Only sql.ErrNoRows means the chain is grounded. Any other failure — cancelled context, closed connection, NULL srcid — says nothing about groundedness, so returning true there claims readable content that was never verified. That is the same class of bug this predicate exists to prevent, and it runs on data arriving over the wire during clone. The blob-size lookup above already fails closed on the same pattern; this restores the symmetry. check-in: 670ca0e75f user: [email protected] tags: trunk | |
| 15:31 |
fix: capture executable bit at Add/Manage time and propagate to commit (#48)
Manage() inserted new vfile rows with isexe always defaulting to 0, regardless of the file's on-disk permissions. buildCommitFiles() also never consulted vfile.isexe when constructing the manifest F-card for changed/added files — it only carried forward the parent manifest's Perm string, which is empty for a brand new file. Together this meant an executable file added via Manage always committed without an "x" marker, and a later Extract wrote it back as 0644. Fix: - Manage() now stats the file and records isexe using the same predicate as Fossil's file_perm() (src/file.c:316): owner-execute on a regular file (S_ISREG && S_IXUSR). Group/other-only execute (e.g. 0645) does not count, matching canonical behavior. The detection lives in one place (modeIsExecutable, next to the existing isexe-to-mode helper vfilePerm) rather than being duplicated at call sites. - buildCommitFiles() now derives the F-card Perm for changed/added files from vfile.isexe instead of only inheriting the parent's Perm, without disturbing an existing symlink marker ("l"). Extract already read isexe correctly when writing files back to disk, so no change was needed there. Out of scope, left untouched: symlink (islink) handling, and executable-bit CHANGE detection on an already-tracked file whose mode changes without going through Manage again (Fossil's belt-and-braces re-stat at commit time / VFileIsExec path) — that path appears to still be missing here and should be tracked separately if it matters. check-in: 76dd5241c2 user: [email protected] tags: trunk | |
| 15:25 |
fix: tolerate NULL event.user/comment in manifest.Log (#47)
Fossil's check-in manifests require only the D and Z cards; the U-card (user) and C-card (comment) are optional, and canonical fossil binds a missing U-card as SQL NULL on the event row. manifest.Log scanned both columns into plain strings, so any repository containing such a check-in failed the entire walk with "converting NULL to string is unsupported" and returned zero rows. Scan event.user and event.comment through sql.NullString and use its zero-value String field, so a NULL column becomes "" without changing LogEntry's public shape - no nullable type leaks out, and no caller has to know NULL was ever possible. Non-NULL columns are unaffected. repo timeline now renders an empty user as "?", matching canonical fossil's coalesce(euser,user,'?') TTY output; the substitution stays in the CLI's presentation layer, not the library. Fixes #44 check-in: 9564ccad55 user: [email protected] tags: trunk | |
| 15:24 |
Provision ticket schema at repo creation (#46)
Newly created repositories were missing the ticket and ticketchng tables and the ticketchng_idx1 index that fossil new provisions. Stock Fossil's web UI queries the ticket table unconditionally (e.g. info.c's artifact-detail lookup: SELECT 1 FROM ticket WHERE tkt_uuid GLOB ...), so opening a libfossil-created repository with the stock fossil binary and visiting /info/<uuid> crashed with "no such table: ticket". Add the ticket and ticketchng table definitions and the ticketchng_idx1 index to schemaRepo2, taken verbatim from zRepositorySchema2 in fossil's src/schema.c, in the same position canonical fossil places them (after attachment_idx2, before cherrypick). Verified against a real fossil new repository: sqlite_master table/ index diff and per-table .schema diff for ticket/ticketchng now match exactly. check-in: 099ddb1b7a user: [email protected] tags: trunk | |
| 11:08 |
chore(deps): bump the go_modules group across 2 directories with 1 update (#42)
Bumps the go_modules group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto). Bumps the go_modules group with 1 update in the /observer/otel directory: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.49.0 to 0.52.0 - [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.52.0) Updates `golang.org/x/crypto` from 0.49.0 to 0.52.0 - [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.52.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.52.0 dependency-type: direct:production dependency-group: go_modules - dependency-name: golang.org/x/crypto dependency-version: 0.52.0 dependency-type: indirect dependency-group: go_modules ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> check-in: c5e189600f user: [email protected] tags: trunk | |
|
2026-06-30
| ||
| 20:38 |
Rebrand to go-libfossil + add acknowledgements (#40)
* docs: rebrand project to go-libfossil; add acknowledgements Rename the project/repo identity to go-libfossil while keeping the Go module path (github.com/danmestas/libfossil) and the libfossil CLI/package names unchanged — imports and installs are unaffected, GitHub redirects cover the renamed repo URLs. - README: title, Tests badge repo URL, and a note clarifying the repo-name vs import-path split. - Add an Acknowledgements section crediting Stephan Beal (libfossil), Dr. D. Richard Hipp / Fossil SCM, TigerBeetle TigerStyle, John Ousterhout (A Philosophy of Software Design), and Antithesis (DST), with links. - Docs site: hugo.yaml title + GitHub repo links, landing page identity surfaces. baseURL, module paths, and CLI/API names left intact. * docs: add go-libfossil logo to README check-in: 97ae7eb00d user: [email protected] tags: trunk | |
|
2026-06-09
| ||
| 06:11 | fix checkout portability under ncruces (#39) check-in: 6abfdc7c25 user: [email protected] tags: trunk | |
|
2026-05-13
| ||
| 19:09 |
fix: DSN _txlock=immediate to serialize writers at BEGIN (#33) (#34)
Two concurrent libfossil.Repo handles on the same SQLite file fail their writes with SQLITE_BUSY immediately, well under the configured busy_timeout=5000 window. Reproduces ~50% of the time on contended manifest.Checkin paths. Root cause: db.WithTx opens BEGIN DEFERRED. Under WAL, a deferred transaction acquires SHARED on first read, then upgrades SHARED→RESERVED on first write. If two connections race that upgrade, SQLite's deadlock-avoidance returns SQLITE_BUSY *immediately*, bypassing busy_timeout entirely (documented behavior, sqlite.org/c3ref/busy_timeout.html). Fix: prepend _txlock=immediate to the DSN in both modernc and ncruces drivers. Every BEGIN now acquires RESERVED upfront, so the second writer blocks at BEGIN — where busy_timeout *does* apply — and the two transactions serialize cleanly. Regression tests added to both driver packages: - TestBuildDSN_TxlockImmediate covers the DSN shape across empty/non-empty pragma maps. - TestConcurrentWritersDoNotRace reproduces the issue-#33 contention pattern (two conns, SELECT then INSERT under DEFERRED) over 25 iterations and asserts zero SQLITE_BUSY. Without the fix, this test fails 25/25; with the fix, 0/25. Closes #33. check-in: 202576c58c user: [email protected] tags: trunk, v0.6.3, db/driver/ncruces/v0.2.0, db/driver/modernc/v0.2.0 | |
|
2026-05-12
| ||
| 23:48 |
fix: Repo.Commit preserves parent's tracked files (#30) (#32)
Repo.Commit was a thin pass-through to manifest.Checkin: callers who supplied a subset of the parent's tracked files got a partial manifest, so files tracked at the parent silently disappeared from the new commit. A checkout at the child's rev saw only the supplied subset — bones, EdgeSync, and sesh all hit this as silent data loss across slot commits. Default behavior now matches `fossil ci`: when ParentID > 0 the parent's F-cards are merged into the commit's file set (caller wins on name collision; untouched parent files are loaded by UUID and carried forward). For the rare case where a caller intentionally wants omission to mean deletion, CommitOpts.PartialManifest opts out of the merge. The internal diff/deletion tests that previously relied on the partial behavior now set this flag explicitly. A first-class deletion API may replace the escape hatch later. Adds TestCommitPreservesParentFiles as the regression: parent [a,b,c], child supplies only [b'], child's ListFiles must return [a,b',c] with a and c keeping the parent's blob UUIDs. check-in: 862dcb20c3 user: [email protected] tags: trunk, v0.6.2 | |
| 20:01 |
feat: CreateOpts.ProjectCode — accept explicit project-code on Create (#31)
* feat: CreateOpts.ProjectCode — accept explicit project-code on Create Downstream EdgeSync coordinates a shared project-code across sibling Fossil repos created on different machines so its NATS sync subject ("<prefix>.<project-code>.sync") matches across leaves. Today every Create generates a fresh project-code, so commits never propagate. Add CreateOpts.ProjectCode: empty preserves current behavior; non-empty must be 40-char lowercase hex (^[0-9a-f]{40}$) — the same format generated project-codes use. Validation runs at the top of libfossil.Create before any disk I/O, so invalid input cannot leave a half-written file behind. Server-code stays auto-generated. Threaded through repo.Create / repo.CreateWithEnv / db.SeedConfig. SeedConfig validates as defense-in-depth (public package, multiple callers possible). Clone accepting ProjectCode is out of scope; will follow separately if EdgeSync needs it. * test: round-trip explicit ProjectCode through fossil rebuild Asserts that vanilla `fossil` (2.28) accepts a Go-side repo created with an explicit ProjectCode, and that `fossil sql` reads back the same value we set. Closes the gap where the existing TestCreate_FossilValidation only covered the generated-project-code path. check-in: 6b66ea6b2d user: [email protected] tags: trunk, v0.6.1 | |
|
2026-05-05
| ||
| 15:11 |
fix: checkpoint WAL on Close so vanilla fossil can read repo (#28)
Closes #27. libfossil opens repos in WAL journal mode but the Close chain never ran wal_checkpoint, so closed repo files were a 4 KiB stub plus a fat -wal sidecar. Vanilla fossil's validity probe stats the main file before consulting the WAL → repo rejected as "not a valid repository". - Adds a typed CheckpointMode (Passive / Full / Restart / Truncate) and a Checkpoint(mode) method on the public Repo (mirrored through internal repo and db.DB). CheckpointMode and its constants are re-exported from the package root so callers don't have to import db. - db.DB.Close now runs PRAGMA wal_checkpoint(TRUNCATE) before closing the underlying *sql.DB. Checkpoint errors are joined with the close error via errors.Join so the connection is always closed regardless of checkpoint outcome. - Skips the close-time checkpoint on the WASM/WASI build path where WAL isn't used. check-in: faf57ad9c7 user: [email protected] tags: trunk, v0.6.0 | |
|
2026-05-04
| ||
| 18:59 |
feat: support whole-checkin diff via empty filePath (#26)
Repo.Diff has a multi-file return shape ([]DiffEntry) but previously rejected filePath == "" with "filePath is required". Consumers that wanted a whole- checkin diff (the equivalent of `git diff revA..revB` or `fossil diff --from revA --to revB`) had to enumerate changed files themselves and call Diff per file; EdgeSync's Agent.Diff is the in-tree example of that workaround. This relaxes the guard: when filePath == "", Diff now returns one DiffEntry per file that changed between ridA and ridB, sorted by Name for deterministic ordering. The non-empty filePath path is unchanged. Identical checkins return ([]DiffEntry{}, nil). Whole-checkin enumeration is name-keyed (union of files across both manifests where the content UUID differs or the file exists on only one side). Proper rename detection via mlink (so a pure rename produces a single entry rather than an add+delete pair) and permission-only change reporting are deferred to a follow-up; the limitation is documented on Diff's godoc and called out in the PR description. The single-file path is unaffected. The previous TestDiff_EmptyFilePath (which asserted the rejection error) is replaced with positive coverage for identical checkins, single-file change, multi-file change, addition-only, and deletion-only. closes #24 check-in: 764a82ecdb user: [email protected] tags: trunk, v0.5.0 | |
| 18:59 |
feat: add Repo.UUIDFromRID for public rid->uuid lookup (#25)
Consumers (bones, EdgeSync) currently bypass the methods-only Repo API to convert RIDs to UUIDs by reaching into r.DB() and running raw SQL. The lookup is well-defined and recurs throughout the libfossil internals; expose it as a first-class method. Add Repo.UUIDFromRID(rid int64) (string, error) backed by a small internal helper Repo.UUIDByRID on internal/repo so the SELECT lives in one place. Missing rids surface ErrArtifactNotFound (wraps sql.ErrNoRows behind a typed sentinel) with the rid in the message. closes #23 check-in: 61aa4dd022 user: [email protected] tags: trunk | |
|
2026-04-30
| ||
| 14:44 |
docs: cross-repo release automation spec + implementation plan (#22)
* docs: add cross-repo release automation design spec Designs an automated release pipeline across libfossil → edgesync → bones: - repository_dispatch chain replaces manual go.mod bumps - linear-only graph (libfossil does not dispatch directly to bones) - per-repo `make ci` mirrors GitHub CI verbatim; `make ci-fast` for pre-push - `make release VERSION=...` helper with annotated tag template - GoReleaser auto-changelog (edgesync, bones); `gh release --generate-notes` for libfossil - 6-PR rollout: 3 hygiene PRs first, then 3 dispatch wiring PRs See docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md * docs: add cross-repo release automation implementation plan Breaks the design spec into 6 sequential PRs across libfossil, edgesync, and bones — 3 hygiene PRs (Makefile + pre-push hook + release helper) followed by 3 dispatch-wiring PRs. Plan includes: - Phase 0 prerequisites (DISPATCH_PAT setup per upstream repo) - Per-PR branch creation, file-level steps, expected commands/outputs, commit messages, and PR-creation gh commands - Post-merge smoke tests for each receiver/sender combo - Final end-to-end cascade validation via v0.4.6-rc1 tag See docs/superpowers/plans/2026-04-30-cross-repo-release-automation.md * docs: correct bones make ci scope to include make check bones' .github/workflows/ci.yml runs make check (fmt-check + vet + lint + race + todo-check) before the otel-tagged lanes. The earlier scope captured only the otel lines, missing the discipline suite that is the primary CI gate. Update both spec and plan to match the actual workflow. The cross-repo leaf-binary integration step (sibling EdgeSync + make leaf) remains intentionally CI-only — it requires a sibling working tree that doesn't generalize to a single-repo make target. * docs: capture libfossil deviation (no pre-push, no ci-fast) PR #3 (#19, merged 2026-04-30) shipped a tighter scope than PR #1/#2 because libfossil's existing pre-commit is already comprehensive. Document the option (A) decision in both spec and plan, and add a multi-module note for the future case if pre-push is ever added. * docs: add go get retry + actor field to receiver workflows PR #4 (bones #90) added a follow-up commit per code review with these two improvements; capture them in spec/plan so PR #5's edgesync receiver picks them up automatically: - 3-attempt retry with 15s backoff covers GOPROXY propagation race (dispatcher fires immediately on tag push, receiver may outrun the module proxy). workflow_dispatch fallback still exists for manual recovery. - Actor field plumbed payload→input→github.actor with triple fallback and rendered as 'Triggered by: @actor' in bump PR body for audit. check-in: 339643c721 user: [email protected] tags: trunk | |
| 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 | |
| 13:50 |
chore: add make ci targets and release helper (#19)
* feat(make): add ci targets mirroring test.yml verbatim ci aggregates 3 sub-targets matching the 3 GitHub Actions jobs: ci-default — modernc driver tests + DST + vet + cmd build ci-ncruces — ncruces driver tests with -tags test_ncruces ci-otel-target — observer/otel sub-module tests All use GOWORK=off (matching CI). Existing test/test-drivers/test-otel targets are left untouched — they don't use GOWORK=off and have drifted from CI; ci-* are the single source of truth going forward. No ci-fast / pre-push hook in this PR — libfossil's existing pre-commit hook (modernc + ncruces + vet + otel + build + SDK drift, ~45s) already exceeds what ci-fast would provide. Adding a pre-push would just double push cost without catching new bugs. * feat(make): add release VERSION=... helper Validates clean tree, runs make ci, opens \$EDITOR with auto-populated tag annotation template, creates annotated tag locally. Push is intentionally manual. * feat: add release annotation template Used by 'make release VERSION=...' as the body skeleton for annotated tags. check-in: 5fd8907820 user: [email protected] tags: trunk | |
| 10:13 |
release: v0.4.5
Since v0.4.4: - sync: converge clone against writing hub (#17, #18) check-in: a22373f305 user: [email protected] tags: trunk, v0.4.5 | |
| 10:10 |
fix(sync): converge clone against writing hub (#17) (#18)
A clone against a hub being written to during the session reliably hit "exceeded 100 rounds" once the production scenario exercised the multi-round path. Three compounding root causes: 1. Server-side: emitCloneBatch issued an open-ended `WHERE rid > cursor` query with no upper bound, so blobs committed between rounds extended the queue indefinitely and CloneSeqNoCard{SeqNo:0} never fired. Fixed by capturing max(rid) at the start of the clone session and round- tripping it via CookieCard so the otherwise stateless handler can scope the query: `rid > cursor AND rid <= cap`. 2. Client-side: the libfossil clone client sent CloneCard (clone-mode signal) without CloneSeqNoCard (the actual pagination cursor, per TestHandleClonePagination). Server's cloneSeq stayed 0 every round, masked by every existing clone-client test fitting in DefaultClone- BatchSize=200. Fixed by emitting CloneSeqNoCard on continuation rounds (cycle > 0). 3. Cookie carrier: clone client previously ignored CookieCards on the wire. Now stores and echoes them, mirroring the existing sync client pattern. Adds three regression tests: - TestCloneAgainstWritingHub (direct issue #17 repro) - TestCloneMultiRoundCursor (catches the cursor bug in isolation) - TestCloneSnapshotBoundExcludesPostSnapshotWrites (snapshot semantic) The fix is libfossil-internal: protocol carrier is the existing CookieCard with an opaque `lf-clone-cap:<rid>` value, so non-snapshot-aware peers fall back to pre-fix behavior with no wire-format break. check-in: 25e235b387 user: [email protected] tags: trunk | |
|
2026-04-27
| ||
| 11:13 |
release: v0.4.4
Since v0.4.3: - sync: prevent rid=0 panic on partial xfer (#14, #16) check-in: 5d6bd072ef user: [email protected] tags: trunk, v0.4.4 | |
| 11:10 | fix(sync): prevent rid=0 panic on partial xfer (#14) (#16) check-in: 30bfac2f91 user: [email protected] tags: trunk | |
|
2026-04-26
| ||
| 15:21 |
release: v0.4.3
Auto-version landing now reads this tag at CF build time. Since v0.4.2: - diff: emit "\\ No newline at end of file" for byte-fidelity with fossil (#10) - repo: expose ResolveVersion + ReadFileAt for symbolic versions (#11) - xfer: always emit project code on push/pull cards (#12) - landing: inject version from latest git tag at build time (#13) check-in: d5dd3dbc3e user: [email protected] tags: trunk, v0.4.3 | |
| 15:20 |
feat(landing): inject version from latest git tag at build time (#13)
Replace the three hardcoded "0.4.2" strings in the landing layout with {{ .Site.Params.version }} template directives. Add scripts/sync-version.sh that reads the latest git tag via 'git describe --tags --abbrev=0' and rewrites hugo.yaml's params.version line. Wire it into wrangler.jsonc's build command so Cloudflare's git-integrated build picks up new tags automatically — push a commit to main with a fresh tag, CF rebuilds, the landing shows the new version with no PR required to bump strings. Footer year switched to {{ now.Year }} so 2027 doesn't surprise anyone. Local fallback: when no tags are reachable (very early dev), the script exits cleanly and the layout's 'default' filter falls back to the value already in hugo.yaml. check-in: ab1e500bc4 user: [email protected] tags: trunk | |
| 15:00 |
fix(xfer): always emit project code on push/pull cards (#12)
The encoder emitted bare 'push\n' / 'pull\n' cards in code paths where the project code wasn't passed explicitly. The upstream Fossil C server's xfer parser requires at least the project code on push and both project + server codes on pull, rejecting 0-arg cards. This broke libfossil <-> fossil interop on the first sync round. Make the project code a required parameter on the encoder and update call sites to thread it through. The libfossil server-side decoder is tightened to match — it previously accepted 0-arg cards, which masked the encoder bug during Go-to-Go testing. check-in: 4082bffaa5 user: [email protected] tags: trunk | |
| 15:00 |
feat(repo): expose ResolveVersion + ReadFileAt for symbolic versions (#11)
* feat(repo): expose ResolveVersion + ReadFileAt for symbolic versions Repo.ReadFile took a raw int64 RID, leaving every library caller to re-implement symbolic-version resolution (tip, trunk, branch name, UUID prefix) — work the CLI's resolveRID already does inside the cli package. Move the resolver onto Repo as ResolveVersion, refactor cli/shared.go to call it, and add Repo.ReadFileAt(version, path) so callers can read by name without first calling ResolveVersion themselves. Adds ErrVersionNotFound for the not-found case and an ambiguity error for prefix collisions. * chore(cli): drop unused openInternalRepo helper + repo import Now-dead code: resolveRID was its only caller, and resolveRID is now a one-line wrapper around Repo.ResolveVersion that doesn't need the inner handle. Also removes the now-unused internal/repo import that the helper brought in, and refreshes the cli SDK page accordingly. * chore(go.work.sum): pin transitive deps from docs toolchain gomarkdoc pulls these via go install during make docs-gen-sdk; commit the lockfile entries so contributors see no go.work.sum drift after a docs build. check-in: 7eb46a9635 user: [email protected] tags: trunk | |
| 14:59 |
fix(diff): emit "\ No newline at end of file" for byte-fidelity with fossil (#10)
Upstream fossil's diff output marks any side whose content does not end with a newline using the standard POSIX marker. internal/diff/diff.go was stripping trailing empty lines via splitLines and never emitting the marker, producing diffs that decoded cleanly Go-side but diverged from the upstream tool — recipients reconstructing file content from the diff would silently introduce a newline that was not in the source. Track per-side ends-with-newline status and emit the marker after the final hunk line of each side that lacks the terminator. check-in: 2a11bf319a user: [email protected] tags: trunk | |
| 14:14 |
docs: usability improvements from DX audit (#9)
Implements all seven recommended changes from the docs audit: 1. SDK pages now show package names in nav. Add scripts/prepend-sdk-frontmatter.sh that prepends '--- title: <pkg> ---' to each gomarkdoc-generated api.md; wired into 'make docs-gen-sdk'. Sidebar previously showed five undifferentiated 'API' entries; now shows cli, db, dst, libfossil, observer/otel. 2. /examples/ populated with paragraphs + GitHub deep-links for embed-repo-api and wasm. No longer a placeholder pointing back at the repo. 3. Architecture page gets an inline SVG mirroring the landing-page diagram. Self-contained CSS (uses currentColor + ember accent) so it tracks Hextra's light/dark theme without depending on the landing's CSS scope. 4. Cross-links added: quickstart -> production + extension-points, migration -> architecture, architecture -> extension-points. Extension Points and Production patterns are now reachable via prose, not just the sidebar. 5. /docs/ landing expanded from 3 sentences to a 7-line subsection map calling out each page's purpose. 6. Version indicator in navbar (libfossil v0.4.2). Override layouts/_partials/navbar-title.html mimics Hextra's default and appends a small mono version tag from .Site.Params.version. 7. New /docs/production/ page covering error handling, context cancellation, telemetry wiring, repo-handle lifetime, and driver selection — the patterns the quickstart's log.Fatal shortcuts gloss over. check-in: f30849a466 user: [email protected] tags: trunk | |
| 08:23 |
docs: wire live docs URL into README + hugo baseURL + SDK refresh (#8)
- README: replace placeholder URL with the live CF Workers URL, add a Docs badge to the badge cluster so the link is prominent on the GitHub repo page. - hugo.yaml: set baseURL so Hugo emits absolute canonical <link> tags and a real sitemap.xml instead of root-relative paths. - SDK pages: refresh gomarkdoc output (drifted from current Go doc comments since the original generation). Same regen that PR #7 contains; either-order merge is a no-op for the second. The GitHub repo's About sidebar now also links to the docs site (set via 'gh repo edit --homepage'). check-in: 0ca5c54ee6 user: [email protected] tags: trunk | |
| 08:12 |
site(landing): SVG architecture diagram + SDK refresh (#7)
* site(landing): SVG architecture diagram + SDK regen - Replace §04 ASCII diagram with an SVG mirroring the dagnats style: three columns (callers / core / plugins), ember flow arrows with CALLS / DELEGATE / EVENTS / PERSIST labels, bottom box for .fossil (SQLite). Picks up the layout's existing .diagram CSS with no new styles. - Refresh gomarkdoc-generated SDK pages — they had drifted from current Go doc comments since the original generation. * docs(site): drop redundant body H1s from frontmatter pages Each of the four moved docs (architecture, extension-points, migration-from-fossil, testing) and the two hand-written driver _index pages had a body '# Title' that duplicated their frontmatter 'title:'. Hextra renders both, producing the visible doubled headline. Removing the body H1 leaves Hextra's frontmatter-driven page heading as the single source. SDK api.md pages from gomarkdoc are unaffected — they have no frontmatter, so Hextra promotes their body H1 to the page title without duplication. * site(landing): bump version refs to v0.4.2 check-in: d1016bd2ef user: [email protected] tags: trunk | |
| 07:52 |
docs site + landing + Cloudflare auto-deploy (#6)
* design: docs site + landing page + Cloudflare auto-deploy Mirror dagnats's docs/landing infrastructure for libfossil: a single-page landing at site/index.html (forked CSS/HTML, content rewritten), a Hugo + Hextra docs site at docs/site/, gomarkdoc-generated SDK reference under docs/site/content/docs/reference/sdk/, llms.txt artifacts, and a wrangler.jsonc that lets Cloudflare's git integration auto-deploy on push to main. Existing docs/*.md files move under docs/site/content/docs/. * design: correct landing-page path to Hugo layout override dagnats's working landing page lives at docs/site/layouts/index.html (a Hugo home-page layout override), not at site/index.html. The top-level site/index.html in dagnats is an older draft that the build pipeline ignores. Match the working setup. * design: align visual system + section list with live dagnats landing The earlier description came from dagnats's stale top-level site/index.html (JetBrains Mono + Instrument Serif italic + ember/teal accents, 8 sections). The live Hugo layout that actually serves at / uses Geist Mono only, prefers-color-scheme dual-mode (warm cream / warm dark), a single ember accent, and 7 sections (Install, What it is, Why you might want it, How it fits together, Compared to neighbors, What it is not, Get going). Update the spec to match what we are actually mirroring. * design: drop SDK regen from CF build to match dagnats exactly Earlier draft had CF install Go + gomarkdoc and re-run docs-gen-sdk on every deploy. dagnats does not do that; it relies on the pre-commit hook to keep committed SDK pages in sync, and ships only Hugo + llms-gen + hugo --minify in the CF build. Match that: smaller, faster CF build, no Go-runtime assumption about Workers Builds, and a true 1:1 mirror. * plan: docs site + landing + CF auto-deploy implementation steps 11 tasks covering Hugo+Hextra bootstrap, content moves, gomarkdoc SDK generation with pre-commit drift check, llms.txt artifacts, Makefile targets, landing page fork from dagnats's live Hugo home layout, wrangler.jsonc for CF Workers Static Assets, README update, and PR creation. * docs(site): bootstrap Hugo + Hextra skeleton * docs(site): move existing docs into Hugo content tree * docs(site): fix links + H1s broken by docs-tree move - Drop .md extensions on Hugo cross-doc links - Switch source-code references to absolute github.com/danmestas/libfossil URLs (relative ../ paths broke when files moved 3 dirs deeper) - Align H1 of architecture.md and migration-from-fossil.md with their frontmatter titles * docs(site): add quickstart, examples, and SDK section stubs * docs(site): tighten quickstart snippets + reference index - Make the Sync example self-contained (open + sync, full main, real signatures) - Bridge from Clone to Open with a one-line prose link - Add a sentence to reference/_index.md so it does not render blank - Use the project's canonical "pure-Go, CGo-free" phrasing * docs(site): add docs-gen-sdk Makefile target + commit generated SDK pages * docs(site): pin gomarkdoc version + replace empty driver stubs with prose - Add GOMARKDOC_VERSION Makefile variable so contributors don't drift on output format when a new gomarkdoc ships - Drop the two driver packages (modernc, ncruces) from gomarkdoc and hand-write _index.md pages explaining the register-on-import contract; the stubs were byte-stable but rendered as visibly broken documentation * docs(site): add gen-llms-txt.sh + docs-gen-llms target * docs(site): switch llms-full frontmatter strip from sed to awk The sed range-negation pattern '/^---$/,/^---$/!p' silently re-opens a suppression range on any bare '---' in body content, eating Markdown horizontal rules and embedded YAML in fenced code blocks. The awk pattern strips the frontmatter only when it begins on line 1, leaving body content intact regardless of how many '---' lines it contains. * hooks: pre-commit fails when SDK pages drift from code * hooks: pre-commit re-installs gomarkdoc if it's the wrong version The drift check's existing gate just looks for gomarkdoc on PATH. A contributor with an older gomarkdoc installed locally would have the gate pass and then run regeneration with their stale binary, producing either a silent false-negative or a confusing OUT-OF-DATE error with no version hint. Compare the installed version against GOMARKDOC_VERSION from Makefile and re-install on mismatch. Best-effort; offline contributors can still commit. * docs(site): add docs-serve, docs-build, docs aggregate targets * docs(site): make docs aggregate parallel-safe via explicit deps The docs aggregate previously listed gen-sdk, gen-llms, and build as side-by-side prereqs. Make doesn't guarantee left-to-right ordering under -j, and gen-llms reads what gen-sdk writes, while build reads both. Encode the chain explicitly so 'make -j docs' is correct. Side benefit: 'make docs-build' standalone now produces a complete deployable site, matching contributor expectation. * site(landing): fork dagnats home layout; rebrand chrome * site(landing): rewrite §01–§07 content for libfossil * site(landing): use .ascii class for diagram + fix §03 phrasing * deploy: add wrangler.jsonc for CF Workers Static Assets * docs: point README at the new docs site check-in: e2a2f28861 user: [email protected] tags: trunk | |
| 06:51 |
v0.4.2: defer manifest crosslink until referenced blobs arrive (#5)
* sync: defer manifest crosslink until referenced blobs arrive Repo.HandleSync (added crosslink in v0.4.1 PR #4 fix 3) crosslinked each manifest as soon as its blob arrived, even when the manifest referenced blobs that hadn't yet been received in the same session. A receiver that Pulled during a multi-blob delivery saw the manifest in event/leaf/mlink while one or more file blobs were still in flight, and a downstream Checkout.Update walking those manifests via manifest.ListFiles hit 'expandUUID: blob not found for uuid <hex>'. Surfaced by agent-infra's hub-and-leaf scaling trial under 16-way concurrent fork+merge cycles (agent-infra trial #10, see docs/trials/2026-04-25/trial-report.md). The 3-leaf friendly case in agent-infra's hub-leaf-e2e doesn't exhibit it because the broadcast cadence is slower than the multi-blob delivery window. Fix: manifest.Crosslink now defers a Checkin candidate when any referenced blob (F-card files or B-card baseline for delta manifests) is not present locally. The deferral is implicit: the candidate query selects rids without an event/tagxref/forumpost/attachment row, so a deferred manifest re-appears in the next sweep automatically. Since HandleSync runs Crosslink whenever filesRecvd > 0, the next sync round that delivers the missing blob also re-runs the sweep and links the manifest fully. Divergence from fossil-scm/c: fossil's reference handles this via 'rcvfrom' tracking and ipctest-style deferred crosslink flushes; the Go port reuses the existing whole-repo sweep semantics by checking blob presence at sweep time. Documented inline in manifest.Crosslink and missingCheckinRefs. Test coverage: - TestCrosslink_DefersCheckinWithMissingFileBlob — manifest with missing F-card blob is deferred (no event/leaf/mlink rows), then fully linked once the file blob arrives. - TestCrosslink_DefersDeltaCheckinWithMissingBaseline — delta manifest with missing B-card baseline is deferred; baseline + delta both link in the next sweep once the baseline blob arrives. - TestCrosslink_LinksWhenAllBlobsPresent — confirms the unchanged synchronous path (all blobs present before Crosslink runs). All three tests fail without the fix (verified by reverting the crosslink.go change locally) and pass with it. * manifest: assert non-nil repo and non-empty uuid in blobPresent Tiger Style: panic at the boundary on programmer errors. Calling blobPresent with a nil receiver or empty uuid is always a bug — empty uuid would silently return false (no row matches the empty-string predicate), masking the upstream caller that lost track of which uuid it intended to check. The peer helpers Crosslink and missingCheckinRefs already panic on nil inputs; blobPresent was the gap. * manifest: order Crosslink candidate sweep by rid Without an explicit ORDER BY, SQLite is free to return candidate rows in any order convenient to its query planner. The deferred-crosslink sweep re-discovers the same rids across multiple sync rounds, so any implicit ordering would let blob-arrival timing leak into observable behavior: per-defer slog streams, pending-item processing order, and any future caller that consumes Crosslink's side effects in sequence. ORDER BY b.rid pins the sweep to insertion order — the same order blobs were stored — which is the natural arrival order for a single sync and a stable canonical order across replays. * manifest: emit deferred rids and missing-blob list in Crosslink rollup The session-end Info log previously reported only counts. When a sync ends with deferred manifests, an operator needs to know which manifests are stuck and which UUIDs they are waiting on — otherwise a permanently-stuck deferral (blob never arrives) is invisible until someone diagnoses it from the per-defer Debug stream. Add deferred_rids and missing_blobs fields. Sort missing_blobs so the rollup is byte-identical across runs that defer the same set, regardless of map iteration order. * manifest: cover P-card non-deferral and shared-blob defer cases The original three defer tests covered F-card and B-card defer plus the all-present happy path. Three gaps remained: * Multi-manifest sharing a missing blob: prove both manifests defer together and both link when the shared blob lands. Without this, a regression that links one but not the other would slip past. * Missing-parent (P-card) intentionally does NOT defer: the deferral check is scoped to F-cards and B-cards because P-card targets do not break Checkout.Update. Deferring on missing parents would deadlock partial clones with pruned ancestors. Pin the contract with a regression test. * F-card duplicate-UUID dedup: the seen-map in missingCheckinRefs already deduplicates, but the behaviour was untested. Two F-cards with different filenames pointing at the same blob must defer once and link to two mlink rows on arrival.check-in: b6b6c40218 user: [email protected] tags: trunk, v0.4.2 | |
| 02:57 |
v0.4.1: xfer encoder, multi-round sync, manifest crosslink (#4)
* xfer: handle empty ServerCode/ProjectCode without arg-count error Repo.Sync(ctx, transport, SyncOpts{Push: true, Pull: true}) with both codes empty previously wrote "push \n" to the wire. The decoder's strings.Fields collapsed adjacent spaces and failed with "xfer: pull requires 1-2 args, got 0". Encoder now skips trailing empty args. Decoder accepts the verb with 0 args as "no specific code requested" — the same semantic as the existing 1- and 2-arg forms after fossil's normal arg interpretation. Also fixes session.cardSummary's slice-out-of-range panic on empty codes by clamping the prefix length. Adds a roundtrip test for the empty-codes path. * sync: continue round-loop while either side has pending work Sync's round-loop previously exited when the user passed Pull=false even if the peer was still requesting gimmes. The user's Pull flag controls whether we *request* new content; it should not gate whether we *service* the peer's gimmes. Symptom: agent-infra's Manager.Push had to pass Pull=true as a workaround to drive multi-round transfers. Root cause: HandleSync.handleIGot gated gimme generation on pullOK alone. Push-only clients send igot cards from sendUnclustered every round (they always announce what they have), but the server would silently drop them because pullOK was false — leaving the server holding only what the client pushed proactively. Continue the loop while any of: pending ungots to push, pending pulls to request, gimmes to service. Exit only when a round completes with no work in either direction. Concretely the gate is now (pushOK || pullOK), restoring parity with fossil-scm's client_sync continuation logic. Adds an integration test (TestPushOnly_DrainsServerGimmes) that exercises the push-only path end-to-end and a unit test that asserts gimme emission under push-only handleIGot. * sync: crosslink incoming manifests so hub-side state stays consistent Repo.HandleSync previously stored manifest blobs in 'blob' but did not populate the relational tables (event, leaf, plink, mlink) that the timeline, fork detection, and clone protocol depend on. As a result: - Leaves pulling from a hub got blobs but stale 'leaf' state, so WouldFork reported phantom siblings. - Verifier-clone of a hub repo failed with "sync.Clone: exceeded 100 rounds" because 'mlink' was empty and the clone protocol couldn't traverse manifest parents. - Hub timelines were always empty even after successful Push. HandleSync now invokes manifest.Crosslink (the same scanner Clone and Sync run after convergence) when at least one file landed in the round. The scanner walks blobs that have no event/tagxref/ forumpost/attachment row yet, parses them, and populates the relational tables. Local commits already produced this state via manifest.Checkin's existing crosslink path; incoming syncs now match. Coverage spans every artifact type the existing scanner already supports (ci, wiki, ticket, event, attachment, cluster, forum, control). The agent-infra trial that surfaced this only exercises 'ci' artifacts, so the divergence note in the prompt described the narrowest possible repair; reusing the existing scanner avoids the divergence and matches the C reference's full coverage. Adds two tests: TestHandleSync_CrosslinksReceivedManifest verifies event/leaf/mlink rows after pushing a single checkin; the leaf- transition test verifies the parent leaves the leaf table when the child lands and that plink connects them. check-in: 3f290b159f user: [email protected] tags: trunk, v0.4.1 | |
|
2026-04-25
| ||
| 19:09 | feat: Repo.Pull public wrapper + Checkout.Update coverage check-in: c9c7dbe3d3 user: [email protected] tags: trunk, v0.4.0 | |
| 18:54 | libfossil: Tiger Style coverage for Checkout.Update check-in: 5ba9d7556c user: [email protected] tags: trunk | |
| 18:51 | libfossil: drop unsubstantiated threading claim from Repo.Pull doc check-in: 3a3060d725 user: [email protected] tags: trunk | |
| 18:47 | libfossil: Repo.Pull public wrapper (HTTP transport, hostile-input asserts) check-in: 5502b364b5 user: [email protected] tags: trunk | |
| 17:30 | libfossil: failing tests for Repo.Pull (Tiger Style: hostile inputs) check-in: e853fb95fc user: [email protected] tags: trunk | |
|
2026-04-21
| ||
| 11:13 |
feat: implement Repo.ReadFile (#3)
Expose a public wrapper around the internal blobAt helper so callers can read a file's content at a specific checkin rev without going through a checkout. Returns ErrFileNotFound (wrapped) when the path is not tracked in the given checkin. Diff already used blobAt internally; this just surfaces the primitive. check-in: d8b2b1bca4 user: [email protected] tags: trunk, v0.3.0 | |
| 03:03 |
feat: implement Repo.Merge (branch-to-branch 3-way) (#2)
Adds Repo.Merge(srcBranch, dstBranch, message, user) which resolves each branch tip, finds the common ancestor, runs ThreeWayText.Merge against the union of source and destination filesets, and — if every file merges cleanly — records a merge commit whose primary parent is the dst tip and whose secondary parent is the src tip (plink isprim=1 / 0). Any unresolved conflict aborts the whole operation with a *MergeConflictError listing the affected files; callers can check for it via errors.Is(err, ErrMergeConflict). Agreed deletions are honored, and one-sided modify/delete conflicts are reported as conflicts rather than silently dropped. Supporting changes: - CheckinOpts.MergeParents plumbed through manifest.Checkin so the P-card and plink rows carry every parent (previously the fresh-commit path only tracked one parent, unlike the sync path in crosslink.go). - manifest.Checkin now calls tag.PropagateAll(primaryParent) after inline T-card processing, mirroring crosslink.applyInlineTags. Without this, a child commit on an existing branch would not inherit its parent's branch tag, so BranchTip would return stale tips for any non-initial commit. - Repo.BranchTip(name) resolves a branch to its latest checkin via tagxref; secondary ORDER BY rid keeps the lookup deterministic when multiple commits share a julian-day bucket. - CommitOpts.MergeParents exposed on the public API so arbitrary callers can author merge commits without going through Repo.Merge. Tests cover clean merges of divergent edits, conflicting edits, one-sided additions, missing branches, same-branch input, multi-parent plink verification, and BranchTip resolution on both trunk and named branches. Green on both the modernc and ncruces drivers plus the OTel observer. check-in: 461f168708 user: [email protected] tags: trunk, v0.2.0 | |
| 02:36 |
feat: implement Repo.Diff (#1)
Fills in the TODO stub in repo_history.go so Repo.Diff returns a real unified diff for a file between two checkins instead of a "not yet implemented" error. The implementation piggybacks on internal/diff.Unified, which already produces Myers-algorithm output. A small blobAt helper walks the manifest at each checkin, resolves the file's UUID to a blob RID, and expands the content. An absent file on a side is treated as empty bytes so pure additions and deletions render as clean insert/delete hunks. Identical content on both sides returns an empty slice rather than a DiffEntry with an empty Unified string, which keeps callers from having to distinguish "no diff" from "missing output". Adds repo_history_test.go covering modification, identical content, addition, deletion, absent-both-sides, and the empty-filePath error. check-in: a3fbae135e user: [email protected] tags: trunk | |
|
2026-04-20
| ||
| 10:42 | docs: date v0.1.0 release, drop private-migration notes, ignore shm files check-in: a4fbc79e3a user: [email protected] tags: trunk | |
| 10:15 | chore: regenerate go.sum entries for v0.1.0 dependencies check-in: f368d871af user: [email protected] tags: trunk | |
|
2026-04-19
| ||
| 16:46 |
chore: remove replace directives for go install compatibility
Replace blocks in published go.mod files are ignored by consumers but cause `go install @v0.1.0` to fail module resolution. Local development continues to work via go.work, which substitutes workspace modules without requiring go.mod replace entries. Also add observer/otel to the workspace for consistent local builds. check-in: 71751cf461 user: [email protected] tags: trunk, db/driver/modernc/v0.1.0 | |
| 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 | |