All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
libfossil versionprints a single, stable, machine-parseable build identifier -- module version (or a-ldflags -X github.com/danmestas/go-libfossil/cli.buildVersion=...override for release builds), Go toolchain version, and platform -- and exits 0. Previouslyversionwas not a recognized command at all and fell through to the unrecognized-argument path. There is no global--versionflag:repo extractalready owns--versionas a command-scoped flag (the source version to extract), and kong's global flags are visible in every subcommand's context, so a root-level--versionwould collide with it.libfossil repo serveCLI command wires the already-publicRepo.ServeHTTPxfer HTTP server into the command surface. A repository created or cloned with this tool can now be served to a peer -- including a stockfossil clone-- without writing Go. Binds to127.0.0.1:8080by default; override with--addr host:port. Process interrupt (Ctrl-C/SIGTERM) cancels the same contextServeHTTPalready shuts down on; no new shutdown mechanism was introduced.
Fixed
- A clone from a libfossil server could fail with a card-syntax error
(
decode card 16303: empty line after split) naming a card the response never contained. The message decoder tried three body framings in order and treated any decompression failure as "not this framing", so a well-formed compressed body that exceeded the decoder's own size bound fell through to the uncompressed branch and the still-compressed bytes were parsed as card text — thousands of nonsense cards until one split into no fields. Worse than the misleading error, an oversize or truncated compressed body was accepted: the garbage cards decoded without error and the artifacts they carried failed their hash check downstream. A body recognizable as zlib is now decompressed or reported, never reparsed. The bound is also raised to 64 MiB, since it sat below what this implementation's own server emits in a single clone round — two libfossil peers could not reliably clone from each other even though each could clone from fossil. Two compile-time guards constrain the bound: it must clear the server's clone batch budget twice over, and it must stay below the size at which a length prefix this implementation writes could itself be read as a zlib header. Neither guard certifies a whole round — the budget bounds what precedes an artifact, not the artifact, so a large artifact that does not lead its round can still overrun the bound (#109). (#104)
Changed
- Breaking:
StatusOpts,MergeOpts, andCheckoutOptshave been removed. No function anywhere accepted any of the three, and nothing constructed one:Checkout.Status()takes zero arguments,Repo.Mergetakes positional string arguments, andCheckoutOptshad no construction site at all. A public type with no call site documents a capability that does not exist — a consumer readingMergeOpts{Strategy: ...}in the docs could reasonably conclude a strategy-selecting merge API exists; it never did.Repo.MergeandCheckout.Statuskeep their current signatures — an options-struct refactor forMergewas considered and declined.CheckoutOpts.Forcewas also a third same-namedForcefield in this package, alongside the already-removedUpdateOpts.Forceand the real, fully-wiredExtractOpts.Force; removing it also removes that readability hazard.ExtractOpts.Forceis unrelated and unaffected. - Breaking:
CloneOpts.ProjectCodeandCloneOpts.ServerCodehave been removed. Neither was ever wired to anything —Cloneaccepted both fields but never forwarded them to the internal clone path, so setting either had no effect and gave no error. 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 that.CloneResult.ProjectCodeandCloneResult.ServerCodeare unrelated and unaffected — they continue to report both, populated from the remote via the clone protocol negotiation, which is the only place they meaningfully originate. - Breaking:
UpdateOpts.Forcehas been removed. It was never wired to anything —Checkout.Updateaccepted the field but never forwarded it to the internal update path, so setting it had no effect and gave no error. Deleting it is honest about what the API actually does; real forcing semantics forUpdatecan be designed and added later as a new, deliberately-wired field if a caller needs them.ExtractOpts.Forceis unrelated and unaffected — it is fully wired and unchanged. - Breaking:
Checkout.Updatenow returns(UpdateResult, error)instead of a bareerror. The internal 3-way merge already tracked which files were written, removed, and left with conflict markers; the old signature discarded all of it, so a caller could not tell a clean update from one that silently wrote<<<<<<<conflict markers into working-tree files.UpdateResult.Conflictedlists the paths that were merged but not cleanly — this is a successful update (err == nil), not an error; a genuine failure still returns a non-nilerrorwith a zero-valueUpdateResult.Checkout.Extractis unchanged. - Breaking:
Repo.Timelinenow enumerates the repository'seventtable newest-first (every event kind by default, or a single kind viaTimelineOpts.Type), matching canonicalfossil timeline. The previous behavior — a first-parent walk from a required start rid — is preserved under a new, honestly-named method,Repo.Ancestry(LogOpts). The oldRepo.Timeline(LogOpts)was actually an ancestry walk masquerading as an enumeration: it 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. There is no deprecated shim; callers of the oldTimelineshould switch toAncestryif they want the walk, or adopt the newTimeline(TimelineOpts)if they want a full enumeration. LogEntrygains aKindfield (EventKind) identifying which ofevent.type's six kinds (ci,e,f,g,t,w) an entry is.Parentsis only populated forKind == EventKindCheckin.- Breaking:
Repo.Timelineorders by(mtime DESC, rid DESC), a total order with rid as a true tie-break at exact mtime equality — a deliberate improvement over canonical fossil's baremtime DESCwith no tie-break, which can repeat or skip rows at a page boundary. Pagination uses a new opaqueCursortype: take one from a returnedLogEntry.Cursorand pass it back asTimelineOpts.Afterto resume immediately after that entry.TimelineOpts.Before time.TimeandTimelineOpts.After FslIDare deleted, not deprecated — callers constructing either field will fail to compile.Cursor's representation is intentionally hidden — it can only be obtained from aLogEntry, never built from a timestamp and a rid by hand, because a hand-built cursor derived from a roundedtime.Timeis not guaranteed to match its row exactly, which is what reintroduces skipped or duplicated rows at a page boundary in the first place. - Breaking:
LogEntryno longer has aCursorfield, andRepo.Timelinenow returns[]TimelineEntryinstead of[]LogEntry.LogEntryisRepo.Ancestry's result type; its cursor was always the zero value, which is structurally identical to a legitimate Timeline first-page call — so feeding anAncestryentry's cursor intoTimelineOpts.Aftersilently paginated from page one forever with no error, and documenting the field as invalid onAncestryentries did nothing to stop it.TimelineEntry(which embedsLogEntryand addsCursor Cursor) is nowRepo.Timeline's result type, so the cursor lives only on the type that has one to give. Callers that readentry.Cursor,entry.UUID,entry.User, etc. off aRepo.Timelineresult need no source change — those fields are still reachable throughTimelineEntry's embeddedLogEntry. Callers that stored aRepo.Timelineresult as[]LogEntry(rather than letting:=infer the type, or using[]TimelineEntry) will fail to compile.
[0.1.0] - 2026-04-20
Initial open-source release of libfossil, a pure-Go implementation of the
Fossil SCM that reads and writes the same .fossil SQLite repository format.
Added
- Repository lifecycle: create new repos and clone from existing ones.
- Working-tree operations: checkout and checkin.
- Timeline traversal over commits and events.
- Merge and rebase primitives.
- Diff and annotate (blame) over tracked content.
- Manifest parsing and content-addressed blob storage.
- Sync protocol client/server for pulling and pushing between repos.
- Observer interfaces for sync and checkout, allowing external hooks into both network sync events and working-tree state transitions.
- SQLite driver abstraction with support for both
modernc.org/sqlite(pure Go) andncruces/go-sqlite3(cgo-free, wasm-based) backends. - Deterministic simulation test harness with BUGGIFY-style fault injection for exercising concurrency and failure paths.
- OpenTelemetry observer provided as a separate submodule to keep the core dependency footprint small.
wasip1/wasmbuild target for runninglibfossilunder WASI runtimes.