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]
Fixed
Checkout.WouldForkagain reports a fork when the checkout's version already has a child on its own branch (#347), as after a pull brings in a child of the checked-out version. Since #291 it was fossil's leaf-ambiguity test, which answers no for a version that is not a leaf, so a commit there created a fork. It is now fossil's commit-time "Would fork" check (!is_a_leaf); another open leaf elsewhere on the branch no longer counts, since a commit extends the checkout's own leaf. The repo-state §7.5 ambiguity test ischeckout.LeafAmbiguous.branch.Createrefuses an open branch name inside the check-in's own transaction (#316). The check ran beforemanifest.Checkinopened its transaction, and the branch tags were applied in further transactions after it committed, so concurrent creates of one name could all pass the check and all commit.manifest.Checkinnow applies its inline tags, parent tag propagation and leaf state inside its transaction, and takes aPrecheckthat runs first in it;branch.Createchecks the name there, so exactly one of several concurrent creates commits.verify.Rebuildis atomic again (#335). Since #285 it committed the emptied derived tables before the crosslink sweep ran, so a sweep that failed partway left event, plink, mlink, tagxref, leaf and the rest empty until a later rebuild succeeded. It now empties, re-derives and refills the bookkeeping tables in one transaction, as fossil'srebuild_dbdoes, and a failed rebuild rolls back to the state it found.manifest.CrosslinkRebuildnow takes the caller's transaction and runs candidate discovery, linking and the leaf and tag repair inside it; the incrementalCrosslinkpath is unchanged.A check-in's
event.mtimenow follows its newestdatetag, the one-shot tagfossil amend --datewrites, as fossil'smanifest_crosslinkandtag_insertdo (#334). Crosslink used the manifest's D-card time and never applied the override, so every date-amended check-in sorted at its original time in the timeline,tipand branch listings. Crosslinking a check-in now reads a date tag already recorded for it, and applying a date tag later moves the check-in'sevent.mtime, keeping the original inomtime.tag.AddTagnow derives its tag through the same path crosslink uses, so it movesevent.mtimeandevent.bgcolortoo.verify.Rebuildnow derives repository state through the crosslink sweep rather than its own partial derivation (#285). It kept a second derivation that wrote rows only for check-ins and control artifacts: no cluster, wiki, ticket, technote or forum events, applied tags from artifacts the crosslink would refuse, skipped absent tag targets and parents instead of reserving phantoms, wrote no plinkbaseid, and computed leaves by "no child at all" regardless of branch. Rebuild now empties the derived tables and runs the same sweep a clone runs, so derived state has one owner and matchesfossil rebuildin event, plink, mlink, tagxref, leaf and phantom. A newmanifest.CrosslinkRebuildruns that sweep in a terminal mode that links a check-in whose content is absent with phantoms rather than deferring it for a blob that will never arrive, asfossil rebuilddoes. Its branch-aware leaf rule now follows fossil, so an untagged parentless check-in is no longer a leaf (the §7.3 erratum the sweep already observed).The sync client ends a session by §3.7's rules, as fossil's
client_syncdoes (#301). Stored content or a new phantom continues it only while the phantom table is not empty, so neither received content alone nor phantoms the server cannot supply keep it going; a phantom used to be asked for until it had aged three rounds. Agimmefor content the client does not hold, or may not send, is dropped where it stayed queued and held the session open untilMaxRounds. Anigotfor a missing artifact now records a phantom in the phantom table, so a later session still asks for it. After a clone's first round, a servererrorends that reply and the clone continues as the draft says, with a cleanup round, andClonethen fails naming the error. Phantoms a pull fills are crosslinked once per reply instead of in one transaction each.The sync client places
pragma send-privatewhere fossil'sclient_syncdoes (§3.2, #307): first in its first request and right afterclient-versionin later ones, where it trailed the content cards. A delta's missing basis received under aprivatecard becomes a private phantom, as fossil'scontent_newmakes it, where it became public.Sync honors the shun list as fossil does (§3.3, §7.7, #302). A received
fileorcfilewhose hash is shunned is dropped without an error and not counted as received, by the server and the sync client, where each stored it; a clone drops a shunnedcfilebut keeps a shunnedfile, as fossil's!cloneFlagexemption does. The client no longer asks for a shunned phantom: the phantom scan, anigotand a clone's delta basis all leave shunned names out, so a delta whose basis is shunned is kept and the basis is never requested.shun.IsShunnednow reports a failed lookup as an error instead of "not shunned".The sync server stores a
uvfilesent without apushcard, asfossil uv syncsends it (§7.3, #296). It answered each one withrejected: no push card, so pushing unversioned files from fossil failed. This is an access change: a user with only the unversioned-write capabilityycan now push unversioned files, as in fossil; Write is no longer also required. A refuseduvfile(noy, or a HASH that does not match its content) now resets the reply and ends the card loop, as fossil does.The sync server refuses a
pushorpullcard naming another project witherror wrong project, as fossil'spage_xferdoes (§8.5, #309). It accepted either whatever its project code, so a client of an unrelated repository could pull its artifacts or push into it. The refusal comes before any capability check, resets the reply and ends card processing.Sync HTTP framing now follows §4 (#310). The server dispatches a request typed
application/x-fossil-debug, which it refused with HTTP 415. The client takes only HTTP 200 as a sync reply, so a 201 or other 2xx is an error, and reads a reply's framing as fossil'shttp.cdoes: no Content-Type is the compressed container, anx-fossil-debugorx-fossil-uncompressedprefix is plain cards, any otherx-fossilprefix is compressed, and a foreign type such as a proxy's HTML page is refused rather than read as cards. A server that writesHandleSync's reply over HTTP must type itapplication/x-fossil.Unknown, wrong-direction and error cards now follow §5.2 and §10 on both sides of a sync (#300). The server answers a client-sent
errorormessagecard — reply-only cards fossil'spage_xferhas no branch for — withbad command, resetting the reply, rather than ignoring it. The client ends its reply-card loop on anerrorcard, keeping the cards before it, as fossil'sclient_syncdoes. Amessage pull only ...from the server (§10.2) now stops the client pushing: it keeps pulling but answers no further gimme and drops its push card one request later, matching fossil's clearing ofSYNC_PUSH. To make that reachable, the client now sends itspullcard before itspushcard (fossil's order), and the server answers a push from a requester who may read but not write withmessage pull only - not authorized to pushinstead of an error, so the pull still proceeds.The xfer card decoder now follows the draft's card grammar instead of failing a whole message on one bad card (#299). A line no card rule accepts (an unknown operator, a bare
pragma, agimmewith a surplus token, aconfigwith a signed size) decodes to a bad-command card: the server resets its reply, answerserror bad command: <LINE>and parses on, and the client ends the reply-card loop keeping the cards before it. Afile,cfileoruvfileline that cannot be framed decodes to a bad-file card: the receiver emits the handler's message (malformed file lineand the like) and stops, as fossil'spage_xferbreaks. Tokens are now split on ASCII whitespace only, within fossil's six-token window, so a no-break space no longer separates tokens and a seventh token is ignored rather than rejected;igotandprivatetolerate a surplus token. A fullcfile's USIZE is treated as metadata, not checked against the inflated size, and the byte after aconfigpayload is consumed whatever it is, both matching fossil.Repo.ResolveVersionnow follows fossil'ssymbolic_name_to_ridname resolution (src/name.c): it chooses a tag by the carrying check-in'sevent.mtimerather thantagxref.mtime, no longer falls back to the tip when no check-in carriessym-trunk, tries an artifact-hash prefix before a plain tag, and accepts therid:,tag:,root:,start:,merge-in:andNAME:TIMESTAMPforms plus a trailing-punctuation retry. A hex prefix now must be 4–64 characters, as fossil requires (#290).branch.Createnow refuses a name an open branch already carries, asfossil branch newdoes, and stores no artifact when it refuses.branch.Listnow reports a branch closed only when its newest check-in carries aclosedtag (fossil's branch-list rule), not when any check-in on the branch does (#292).checkout.BranchLeavesnow returns only the open leaves of a branch, skipping any leaf that carries aclosedtag, as fossil'sleaveslists them (#291).A repository with no
hash-policyrow names new content as fossil'shname_default_policydoes (#274): SHA3 when it is empty or holds a SHA3 artifact, SHA1 (policyauto) otherwise. It used to name an empty repository's first artifact with SHA1. And when anautorepository takes in its first SHA3-named artifact, the promotion is written to thehash-policysetting, as fossil'scontent_putdoes, so fossil opening the repository agrees. It used to be derived from the stored names only.ListFilesand crosslink reject a B card whose target is not a baseline manifest (#275): a delta manifest or a control artifact used to be accepted as the baseline, giving a partial or empty file set instead of an error.Checkinrefuses a filename the artifact format's writer profile forbids (#270): one containing a backslash, or that is not valid UTF-8 (surrogates and overlong forms included). It used to write a manifest naming it. fossil's ownaddrefuses such names, so fossil never creates that manifest, but its reader still accepts one that arrives from elsewhere. The draft's writer profile also bars characters above U+FFFF, private-use characters and noncharacters; those are not enforced here.delta.Createrefuses a source or target of 2^32 bytes or more withdelta.ErrTooLarge(#279), as the delta format's producer bound requires; it used to write header and copy integers of 2^32 or more.Createnow returns an error, andDeltifystores such content whole, as fossil does when a delta is not worthwhile.Applynow rejects a header naming exactly 2^32 as well (the draft leaves that limit implementation-defined), so whatCreatewrites andApplyreads agree.Storing content whose name is a phantom fills the phantom in place, as fossil's
content_putdoes (#278).blob.Storeused to return the phantom's rid and leave it empty, so a check-in of a file matching a known phantom recorded a manifest pointing at content that was never stored.Repo.Tagnow places a tag name in the symbolic namespace with asym-prefix and refuses the reservedwiki-,tkt-andevent-prefixes, asfossil tag adddoes for a check-in. It used to store the name verbatim, so the tag did not resolve as a branch/tag name. Callers pass the bare name.Cancelling a propagated tag no longer deletes an inherited row that is newer than the cancellation. The cancel walk carried no mtime guard (
COALESCE(srcid=0, 0)); it now matches fossil'stag_propagate(srcid=0 AND tagxref.mtime < ?), removing an inherited row only when it is older and halting the descent at a newer one (#288).The singleton
parent(reparent) tag is now applied: the tagged check-in's plink and mlink rows are rewritten to the parents the tag names, and the affected leaves are recomputed, as fossil'smanifest_crosslink_enddoes. An old parent left without a child becomes a leaf, matchingfossil rebuild. Both the crosslink sweep andverify.Rebuildapply it (#286).A technote whose T-card names another artifact, or propagates or cancels, is now refused at crosslink time and applies none of its tags, as fossil refuses it at parse time. A technote may only carry
+(singleton) tags on itself (#287).Crosslinking a root check-in a second time no longer duplicates its mlink rows. The root-file path inserted each row with no "already derived" guard, so a re-crosslink (as the relink path does after clearing event/tagxref) doubled them; it now skips the block when the rows are already present, as
manifest_crosslinkis idempotent (#294).The first check-in's
sym-trunktag is now propagating (*sym-trunk), matchingfossil newand repo-state §6.1, so the trunk alias descends to every check-in on the branch. It used to be written as a singleton (+sym-trunk), which stopped at the root (#289).deck.Parseapplies the §5 structural-type rules once a type is determined: the §5.2 card-legality table (each type's allowed and required cards, from fossil'smanifestCardTypes), so a card the type disallows or a required card left out is rejected, and the §5.4 semantic checks -- a control artifact's tags may not target itself, a technote's tags are all self-referential singletons, a forum post carries at most one of an H or I card and at most one parent. Type determination now falls back to a control artifact (not an empty check-in) when no card sets a type and there is no comment, file or parent, matching fossil. This also makes a missing Z card a required-card rejection rather than a special case (#273).deck.Marshalre-encodes whatdeck.Parseread, soMarshal(Parse(x)) == xfor a canonical artifact. A present-but-empty C comment, N mimetype or T value, and a bare P card, are kept distinct from an absent one and re-emitted (an empty W body already was, now via nil versus a non-nil empty slice); the distinction is carried in unexported Deck/TagCard fields, so a caller that builds a deck and sets a non-empty value still has it marshalled and the public API is unchanged. The D and E timestamps are written at their writer precision -- millisecond for D, second for E -- extended to keep any finer fraction the parser read, so a sub-second timestamp round-trips instead of being truncated (#272).deck.Parseparses D and E timestamps with the §4.6.4 grammar (as amended by the §4.6.4 erratum) instead of two fixedtime.Parselayouts. It accepts the date-time form withHH:MM,HH:MM:SSor a fractional second, aZ,z,+HH:MMor-HH:MMtimezone, one or more whitespace-or-Tseparators, the hour 24 and an unchecked day (2024-02-31rolls forward, as SQLite'sjulianday()does), and a date at or after-4713-11-25; it applies the exact-digit field ranges (year 4 digits, month 01-12, day 01-31, hour 00-24, minute and second 00-59) and rejects a lowercasetseparator rather than silently rewriting it toT(#271).deck.Parseapplies fossil's §4.6 name predicates. An F-card filename and old-name, and an A-card filename, must be simple pathnames (§4.6.1): no leading or doubled or trailing slash and no "." or ".." path element, the non-strict form fossil uses, which still admits a backslash or non-UTF-8 byte. An L-card title and an A-card target (when not a hash) must be well-formed wiki names (§4.6.2): 1 to 100 bytes, no leading, trailing or doubled space and no control byte. A reserved repository filename (_FOSSIL_,.fslckout, in either case, with an optional -wal/-shm/-journal suffix, at the root or as a path's last component) is silently dropped from an F-card list after the sort-order check, exactly as fossil does (#269).deck.Parsevalidates every artifact-hash position against the §4.7.1 map: the A source, B, the E identifier, each F hash, G, I, M, every P token, both Q tokens, and the T target must be 40 or 64 case-insensitive hexadecimal characters; the K ticket id is pinned to 40; the R checksum to 32. A hash is required on every F card of a baseline manifest (only a delta manifest may carry a hashless deletion card). The A target and the N mimetype are now escape-decoded on parse and re-encoded on marshal, matching §4.3.1, while the other hash tokens stay raw; the A filename keeps only its final path component. The Q sign must be+or-, a tag name that is entirely hexadecimal (either case) is rejected, and these checks reject malformed tokens fossil also rejects, such as a hash carrying a stray carriage return (#268).deck.Parsetokenizes artifact cards the way fossil does (itsnext_token/next_card): tokens are separated by a single space, a card ends at LF, and tab and CR are ordinary token bytes rather than whitespace to split or trim on. A blank line, an empty token between two others (two spaces in a row), a bare card where a value is required, and trailing residue after the last card are each rejected; a line that ends in a space still carries a present-but-empty last value. The W card's size is read as fossil reads it -- leading decimal digits, with an overflow guard -- and the body must be followed by a newline. The clearsign armor trailer is matched on 29 bytes, as fossil matches it, so a mangled final dash no longer hides the signature. Each card is also held to its exact §4.2.1 arity: a card with more tokens than Table 2 allows is rejected (#266, #267).deck.Parseanddeck.VerifyZjudge an artifact's framing by the rule it breaks. The Z-card check now mirrors fossil'sverify_z_card: it locates the Z card as the last 35 bytes beginningZ, never reads the final byte, treats a body too short or without a trailing Z card as "Z card absent", and reports only a present-but-wrong checksum.Parsegained the §4.1 framing gates fossil applies up front -- a non-empty blob ending in LF, checked on the raw bytes before the signature wrapper is stripped, and a first card that is an upper-case letter followed by a space -- and a missing Z card is now rejected by the required-card rule rather than by the checksum. Artifacts rejected for a missing final LF, residue after the Z card, or a bad first line are no longer blamed on the checksum (#265).
[0.11.1] - 2026-10-11
Security
- The sync server sends repository content only to a requester allowed to
read it (GHSA-mvx3-6793-8x4r). It used to answer
gimme HASHwith the artifact whether or not a pull was accepted, andpragma uv-hash,uvgimmeanduvigotwith the unversioned catalog and files, to any requester, anonymous ones included. As in fossil'spage_xfer,gimmeis now answered only in a session whosepullcard was accepted, or in a clone, and the unversioned catalog and files go only to a requester with Read (o, i, or Admin).uv-push-okis sent only to a requester who may write unversioned files (y); a reader without it getsuv-pull-only, as fossil sends. The same rules apply to table sync:xgimmeneeds an accepted pull andpragma xtable-hashneeds Read. Auvigotno longer updates an unversioned file's mtime for a requester without y, andpragma ci-locktakes or reports a check-in lock only for a requester with Write (i), so an anonymous requester can no longer learn who holds a lock. This is a deliberate departure from fossil, which takes a lock for any requester; fossil's own client asks for one only before a commit, which needs Write. - The HTTP sync transports follow redirects as fossil does rather than as
Go's
net/httpdoes (GHSA-3f8x-8fr5-mr98). They used to follow a redirect from HTTPS to plain HTTP, which resent the request, login card included, in cleartext. They now refuse it, and follow a 301, 302, 307 or 308 by re-POSTing the same body and Content-Type, with one trailing/xferstripped from the new location, failing on the twentieth redirect. A 301 or 302 used to become a GET without the body, and a chain stopped after 10. This applies toNewHTTPTransport, whateverhttp.Clientit is given.
[0.11.0] - 2026-10-10
Added
Checkout.Mergemerges another checkin into the checkout, as fossil'smergedoes, andCheckout.Conflictslists the files left in conflict (#244). The merge is recorded the way fossil records one, so fossil reads it and commits it, and the nextCheckinrecords the merged-in checkin as a merge parent. A file edited here but deleted in the merged-in version is kept rather than deleted, and a merge that would overwrite an untracked file is refused: go-libfossil has no undo copy to recover them from. A change the merged-in version made to a file's executable bit is taken. Renames are not followed yet.CheckoutCommitOpts.Filescommits only the named files, andCheckoutCommitOpts.AllowConflictcommits files that still hold conflict markers (#244).
Changed
- Merge conflicts use fossil's conflict markers, with the common ancestor
shown between the two sides, so fossil sees a conflict go-libfossil wrote,
and the other way round (#244). This applies to
Updateas well asMerge. A file is in conflict while it holds markers; there is no separate conflict state to clear. - The CLI's
mergecommand callsCheckout.Merge(#244). It merges into the checked-out version; it used to merge against the repository's newest checkin, which made merging a branch that is the newest checkin a no-op. It no longer writes.LOCAL,.BASELINEand.MERGEfiles beside a conflicted file, since the markers now include the common ancestor.mark-resolvedrefuses a file that still holds markers, andconflictslists the files that hold them. - Breaking (CLI):
repo cicommits the checkout, asfossil commitdoes (#244). It used to commit the named files straight to the repository on top of its newest checkin, ignoring the checkout, so it could not record a merge and left the checkout behind. Run it in a checkout (or pass-d); with no files named it commits every change. The--parentflag is gone, since a checkout always commits onto its own version.--allow-conflictcommits files that still hold conflict markers, and--allow-emptycommits when nothing has changed. Checkout.Checkinrefuses files that still hold merge conflict markers, as fossil's commit does, unlessAllowConflictis set (#244).- Breaking:
Checkout.Checkinrefuses a commit with nothing changed and no merge pending ("nothing has changed"), as fossil's commit does, unlessCheckoutCommitOpts.AllowEmptyis set (#244). It used to record a checkin identical to its parent. A named file the checkout does not track is refused too; a named directory selects the tracked files under it. stash.Saverefuses while a merge is pending (#244). A cleanly merged file looks like a plain edit; stashing it left the merge record naming content that was no longer in the checkout.
Fixed
A pull-only
Syncconverges when the client holds artifacts the server lacks (#277). The server used to answer the client'sigotcards withgimmefor them even though the client was not pushing, so the client never satisfied the request and the pull ran toexceeded 100 rounds. The server now asks only a pushing client, as fossil does, and a client that is not pushing ignores anygimmeit receives.Repo.Annotatecredits each line to the right check-in when edits shift line positions between versions (#253). Past the first parent it used to write each line's attribution by its position in an older version, so lines were credited to the wrong check-in, and an older version with more lines than the starting one panicked with an index out of range.Check-in, parent-link and tag rows store the values fossil stores (#258), whether written by a commit, a crosslink or
verify.Rebuild. Times match bit for bit: tag times are SQLite'sjulianday(), and event, plink, attachment and forumpost times are that value as fossil writes it into SQL text, which keeps 16 digits; they used to differ in the last digits. Fossil's own text conversion depends on the platform it was built for, and this follows the macOS and Windows builds. A check-in'somtimeis recorded (it was NULL), and a tag without a value is stored as NULL rather than an empty string. The CLI'srepo tag lsreads such tags, which it used to misprint.Clone and sync exchanges follow draft-fossil-sync-protocol-00 (#255). A clone reply sends an artifact stored as a delta as a
cfilenaming its source, as fossil does in a version-3 clone; it used to send afilecard. A clone session stops asking once the server sendsclone_seqno 0, sending at most one cleanup request with no operation; it used to switch topullto fetch missing artifacts, which the draft forbids.Clonenow fetches any artifact still missing with a separate pull session afterwards, and still fails if one cannot be had: unlike the draft, which ends a clone with phantoms left,Clonereturns only a complete repository. A clone the server stops beforeclone_seqno 0fails rather than returning part of a repository. A sync now asks once, in its first round, for the phantoms a repository already holds, as fossil's does, without letting one the server lacks hold it open. Every request ends with a random comment, not only those with a login, and every completed reply ends with fossil's timestamp comment.Repo.Annotatecredits lines the wayfossil annotatedoes (#256, #257). It now uses a port of fossil's own line diff and, like fossil, diffs each older version against the starting one. Where a line repeats (blank lines, braces), which copy counts as new depends on the diff's tie-breaking, and go-libfossil's diff used to pick differently, crediting such lines to other check-ins. Trailing blank lines are no longer dropped from the result. On the RFC, althttpd, pikchr and wapp repositories every annotated file now matchesfossil annotateline for line. A line's text no longer includes a trailing carriage return, a leading byte-order mark is ignored, and a file fossil will not diff (one with a NUL byte, or a line over 32,767 bytes) returns an error. Annotate no longer allocates a table the size of both versions at every step.Checkout.Checkinrecords pending merges as merge parents, including merges made by the fossil binary (#244). It used to drop them, so a merge committed through go-libfossil looked like an ordinary commit. Committing only some files of a merge is refused, as fossil refuses it.Checkout.Checkinof a checkout with no version and nothing added returns an error instead of panicking (#244).The CLI's
mergerecorded a conflict aschnged=5, which fossil reads as "added by integrate", and recorded no merge parent (#244).
Security
A sync card whose size is not a plain decimal is a decoding error. A negative size, as in
file HASH -1, used to panic the client or server reading the message, and a signed size such as+5was read as a size. Sizes follow draft-fossil-sync-protocol-00'sdecimalgrammar and fossil's 32-bit limit, and auvfile's FLAGS take the same rule, so negative FLAGS are rejected as the draft requires. A declared size no longer sets how much memory is allocated before the payload arrives: the buffer grows with the bytes read, so a large size over a short message fails without allocating it. Acfilepayload may inflate only to its USIZE, or to a margin above USIZE for a delta, and not past its 4-byte size prefix, and allcfilepayloads in one message may inflate to at mostMaxCFileInflateBytes(1 GiB) together; a few megabytes of zlib could otherwise inflate to tens of gigabytes. A single artifact larger than that cannot be received as acfile(GHSA-j99p-mw22-69c8).An
xrowpayload orxdeletePKData must be one JSON object whose values are scalars and whose primary-key values have the column's type; anything else is refused with an error, on the client and the server. JSONnullused to panic the process, as did a primary-key value of the wrong type or a nested value. A client refuses anxrowfor a table it has not registered (GHSA-j99p-mw22-69c8).A synced table's row can no longer rewrite the SQL that stores it. Keys of an
xrowpayload orxdeletePKData were written intoUpsertXRow'sINSERTas column names unchecked, so a peer could choose the statement's columns and values, or select from other tables into a synced one. A peer's row must now use exactly the table's declared columns, andUpsertXRowrefuses any key the table's registered schema does not declare. Every other function that writes a synced table or column name into SQL validates it first (GHSA-9fq6-h6xw-274g).The HTTP transports read at most
xfer.MaxBodyBytes(65 MiB) of a reply and returnxfer.ErrBodyTooLargepast it; a server streaming an endless reply used to be read into memory whole (GHSA-j99p-mw22-69c8).The sync server answered
reqconfig NAMEwith any row of theconfigtable, to any requester, including one with no capabilities, so it disclosedproject-code,server-codeand every local setting. It now answers as fossil does: only a/GROUPrequest, only for a requester with Read, and only with the names fossil transfers in that group. The user and subscriber tables also need Admin, which counts only for a verified login, and concealed addresses need RdAddr (e). A refused request gets no cards and no error, so the rest of the exchange goes on. Groups are sent in fossil's encoding, sofossil cloneandfossil config pullagainst this server now receive the skin, project, ticket, shun and alias settings (GHSA-p6w4-4xqc-jhq2).The sync server checks a login card's nonce against the request body, as fossil does (GHSA-2g5p-q5f3-477p). The nonce must be the SHA-1 of the decompressed bytes after the login line; it was never checked, so a login card captured from one request authenticated any other request body. A refused login now ends the exchange with fossil's
error login failedinstead of carrying on as nobody, and a second login card is refused. A successful login adds the user's capabilities to nobody's instead of replacing them. As in fossil,nobodyandanonymouslog in without a password, and accounts with no stored password,developerandreadercannot be logged into.Breaking: A new repository gives its built-in users the capabilities
fossil newgives them.nobody, whose capabilities every request without credentials gets, may now clone, read and download (gjorz); it used to be grantedcghijknorswy, which let anyone push check-ins and unversioned files, write wiki pages and tickets, and act with Setup. The repository also gets fossil'sanonymous,developerandreaderusers. The creating user, who holds Setup, can now push and pull once given a password, as Setup includes those rights in fossil. A login is refused for any account with no password stored, as fossil refuses it (GHSA-44p2-whvx-9gwq).
Migration: new repositories no longer let anonymous users push. To allow
it, grant it explicitly, for example r.SetCaps("nobody", "gijoryz"), or
give clients an account with a password. Existing repositories keep the
capabilities already stored in them.
[0.10.0] - 2026-10-03
Changed
Checkout.Revertkeeps a never-committed file on disk when it reverts the add (#234). It used to delete the file, destroying the only copy. It now un-manages it and leaves it in place, as fossil'srevertdoes.Revertalso checks the disk first, so an edit that no scan has recorded yet is reverted instead of silently skipped; restores a file that is missing from disk; and, when reverting everything, drops a pending merge completely (merged-in versions and the merge record), as fossil does.Checkout.Renamerefuses to move a file onto one that already exists on disk, asfossil mv --harddoes, before changing anything. It used to overwrite it.
Fixed
Checkout.Revertundoes a pending rename, as fossil'srevertdoes (#242). It used to leave a rename made withfossil mvin place, and restored a renamed, edited file's content under its new name. The file now goes back to its old name with its committed content, and either name can be given. When undoing a rename would land on a name another file now holds (a swap, a new file added under the old name, or an untracked file there),Revertrefuses and changes nothing rather than overwrite it: go-libfossil has no undo copy to recover it from (#248). Naming several files reverts them as one: if one is refused, none is reverted.A dry-run
ExtractorUpdateno longer changes the checkout (#236). Both used to replace the checkout's file list and move its version to the target, losing pending adds, renames and merge state, even though no file was written. Switching versions withExtractnow also ends any pending merge, as fossil'scheckoutdoes, so a forced switch can't leave a merge record behind to give the next commit a bogus merge parent; re-extracting the current version withoutForcekeeps it. A forcedExtractstarts the checkout's file list fresh, asfossil checkout --forcedoes, so a pending rename or removal no longer survives it.A database fault while checking whether content is present no longer reads as "content missing" where missing content is tolerated (#239). Since #231,
CreateCheckouton a partly synced tip leaves the checkout's file list empty instead of failing; a busy or failed database during that check was treated the same way, soCreatequietly succeeded with an empty checkout. The fault is now reported. Newcontent.CheckAvailableByUUIDreturns the error; the existing boolean checks still treat one as unavailable.The CLI's
add,rm,renameandrevertcommands call the library instead of writing the checkout database with raw SQL (#234).renamenow moves the file, likefossil mv --hard, instead of leaving it under its old name where the next commit couldn't find it.revertrestores committed content instead of only clearing change flags. Errors, including from closing the checkout, are reported instead of ignored. Without-Rthey find the repository the checkout records, including a relative path asfossil openstores it.The internal stash no longer drops files changed by a merge (#233). It selected only
chnged=1, leaving out fossil's merge states, and stashed a rename as a plain edit. It now selects changes the same wayStatusdoes, and refuses a pending merge, a permission change or a rename by name, since it can't yet stash those faithfully. The stash isn't reachable through the public API yet. Checkin and stash now read the checkout through one internal package,internal/vfile, which is the only place fossil's checkout encoding is interpreted.Statusreports tracked files that are gone from disk asmissing, as fossil'schangescommand reports MISSING (#232). They used to be left out, or shown asaddedormodified.HasChangesnow sees a pending add made by the fossil binary without a scan first. Both now share one classifier, so they can no longer disagree about what counts as a change, and Status lists files by name. A directory where a tracked file belongs is reported asmissingrather than failing the scan. A checkout written by an older go-libfossil may still holdrid=0rows for content that was missing from the repository (see #231);HasChangesnow counts those, andExtractof a complete version replaces them.Checking out a version whose content is partly missing, as a partial sync leaves it, is now refused up front with an error naming the files (#231). go-libfossil used to record such files with
rid=0, which in fossil's checkout format means "added", andExtractthen panicked trying to read them. This matches fossil, which refuses with "missing content". ACreateCheckoutwhose tip is still partly synced no longer loads the tip's files, so extracting an older, complete version still works.A non-forced
Extractnow checks before it changes anything (#230). It used to replace the checkout's file list first, so a refusal lost pending adds, renames and merge state and leftStatusseeing an empty checkout. It also compared disk with the target version, so a clean checkout could not switch to a version that changed a file. Extract now follows fossil'scheckout: it refuses while the current version has unsaved changes that the switch would lose, or when an untracked file sits where the target would write different content. A tracked file that is missing from disk no longer blocks a switch, as in fossil.Reading a stored artifact could return compressed bytes (#235).
blob.Loadtreated a row as stored uncompressed whenever its stored length equalledblob.size. For a delta row those are different things (the compressed delta and the full artifact's length) and can be equal by chance, so the delta step then failed. Fossil always inflatesblob.content, and so does go-libfossil now. Go 1.27's new compressor made one test fixture hit the case, but any repository could.Checkouts created by the fossil binary (
fossil open) now work throughOpenCheckout(#228).Statusfailed outright withconverting NULL to stringonvfile.mhash, because go-libfossil judged every file against that column, which fossil only fills while a merge is pending. Change detection now follows fossil's own scan: a file is compared with the artifactvfile.mridnames, adds and merge states move through the samechngedtransitions, and fossil adds, renames andorigname==pathnamerows are classified the way fossil classifies them.A non-forced
Extractno longer overwrites uncommitted work in a fossil-made checkout. The local-edit guard swallowed the same NULL scan error and skipped its check, and it never considered a merge that fossil had applied but not yet committed; both now refuse unlessForceis set.Addwritesvfile.islink, which fossil's checkout schema declares with no default, so a file added by go-libfossil to a fossil-made checkout reads back cleanly in both tools. New rows no longer populatemhash.
[0.9.0] - 2026-08-07
Fixed
- New artifacts are named with the algorithm the repository's
hash-policyselects, rather than always SHA1. Committing into a repository whose policy is sha3 -- thefossil initdefault since 2.10 -- wrote SHA1 artifact IDs, so every F-card in the new manifest carried a differently-derived hash from the one already in the parent manifest andfossil diffreported every tracked file as CHANGED even when one file had been edited. Repository integrity was never affected and canonical fossil could always read the result; the damage was unreadable diffs and a permanently mixed-hash repository, plus artifacts ashun-sha1peer would reject.
Content already stored under the other algorithm's name is reused rather
than stored a second time, unless the policy forbids it (sha3-only,
shun-sha1). Without that, a repository holding SHA1 artifacts under a
sha3 policy would churn every F-card on its next commit -- the same
every-file-CHANGED symptom, mirrored. Only a stored blob qualifies: a
phantom is a name the repository knows of but has no content for.
checkout.Manage and uv.Write read the same policy instead of each
guessing from the width of an existing name. uv.Write's guess read
project-code, which is 40 hex characters in every repository whatever the
policy, so it could only ever choose SHA1.
- Repositories created by this library are seeded with
hash-policy, so they name artifacts the wayfossil initdoes. Without the row, naming derived the algorithm from the repository's own artifacts, and a repository with no artifacts yet settled on SHA1 -- then stayed there permanently, because the first artifact it wrote was SHA1 too.
A clone is the exception and keeps no seeded policy: it adopts its source's,
which the absent row expresses by deriving the algorithm from the artifacts
the clone actually receives. Canonical fossil propagates the source's policy
value into a clone; the sync protocol carries no configuration, so this
reaches the same answer by inference. The two differ only for a source
explicitly set to sha1 that nonetheless holds a SHA3 artifact.
Added
db.HashPolicySHA1,db.HashPolicyAuto,db.HashPolicySHA3,db.HashPolicySHA3Onlyanddb.HashPolicyShunSHA1name Fossil'shash-policyvalues, so the seed path and the naming path read one definition instead of each carrying its own copy of the enum.
Changed
- Repositories created by this library now name their artifacts SHA3 rather
than SHA1. No API changed, but the same content committed into a newly
created repository produces a different artifact ID than it did in 0.8.1.
Existing repositories are unaffected: their artifacts keep their names, and
their own
hash-policycontinues to decide how new ones are named.
[0.8.1] - 2026-07-30
Fixed
- Crosslink reports the underlying cause when it fails, instead of replacing it with a generic message.
- Test fossil servers are reaped before their temporary directories are removed, and the unit-test timeout was raised; leftover servers holding files open made teardown flaky.
Changed
- Generated and local-only files are no longer tracked.
[0.8.0] - 2026-07-29
Fixed
mlink,plink,tagxrefandleafare derived the way canonical fossil derives them, in both the sweep and the commit path, including derivingleafby canonical's branch rule. mlink derivation, previously duplicated, is now unified in one place.- Tag propagation stops at branch boundaries instead of crossing them, and the three tag-apply paths share one propagation helper.
- Authenticated sync interoperates with canonical fossil.
- Clone retries a dropped exchange round instead of aborting.
- The project code is derived, and artifact visibility is written inside the storing transaction rather than after it.
- Ticket artifacts are crosslinked, and ordinary file blobs no longer emit a warning.
- Checkins cascade-linked while referencing unavailable blobs are deferred until those blobs arrive.
- Submodules are pinned to v0.7.0 rather than an orphaned pseudo-version.
Added
- The CLI selects its SQLite driver by build tag.
Performance
- Clone no longer re-seeks
blob.uuidper F-card, and no longer over-allocates delta output. - The post-sync crosslink sweep no longer re-examines the whole repository.
- Content received during sync is reused in crosslink instead of being read back.
[0.7.0] - 2026-07-27
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
- The xfer HTTP server now sends an explicit
Content-Lengthon every clone and sync reply. The full response is already materialised byEncodebefore the first byte is written, so its exact length is known for free; setting the header keepsnet/httpfrom silently switching toTransfer-Encoding: chunkedonce the buffered body passes its internal flush threshold. Release-tagged fossil ≤2.23 reads the reply length only fromContent-Length(src/http.cleaves itsiLengthnegative otherwise) and aborts with "server did not reply" on any chunked reply above ~256KB, so those clients could not clone a repository larger than that from a libfossil server. No buffering was introduced — the response was always fully in memory — so #88's per-clone bound (DefaultCloneBatchBytes) still caps each reply. The fix is unconditional rather than negotiated: because the server never streams, chunked framing offers no memory saving overContent-Length, andContent-Lengthis what canonical fossil's own server emits, so it is strictly the more compatible framing for every client (issue #101). - The message decoder now selects a body's framing from its §4 Content-Type
rather than by trying each framing and seeing which parses. A body sent as
application/x-fossilis decoded as the §4.1 compressed container; every other type, includingapplication/x-fossil-uncompressed(used by clone-v3 replies), is decoded as plain card text — the same rule canonical fossil applies. Trial-based dispatch made a decompression failure indistinguishable from "wrong framing"; a compressed body that now fails to inflate is reported as the fault it is and never re-fed to the card parser. The former unprefixed raw-zlib framing was removed: it was checked before the prefixed container on the belief that real fossil emits it, but a fossil 2.28 server never does — its pull replies are the prefixed container and its clone replies are plain text — so the framing was only ever right by accident. Removing it also retires the length-prefix/zlib-header aliasing guard, which existed solely to disambiguate the two compressed framings that trial-dispatch conflated.xfer.Decodenow takes the Content-Type; call sites that carry no header (the byte-oriented NATS transport, both ends libfossil) pass the compressed type explicitly, matching what they always emit. The HTTP server rejects a request body whose Content-Type is absent or neither §4 type with 415 Unsupported Media Type instead of decoding it as plain card text: bodies come from untrusted peers, and without a framing signal decoding either way is a guess. Real fossil always sets the header, so no conforming peer is affected. (#106) - A large artifact now clones regardless of its position within a clone round.
The server charged its batch budget before each artifact and sent the one
that crossed it whole, so a round carried
budget + one whole artifact— and when ordinary sub-budget filler preceded a large artifact, that sum exceeded what the client could decode (xfer.MaxDecompressedBytes), failing the clone with a spurious "declared container length exceeds" error. The size at which a clone failed depended on incidental filler: 8–15 MB of filler ahead of a 58.7 MB artifact all failed, while 17 MB passed by pushing the artifact into a round of its own.emitCloneBatchnow flushes the round and sends an over-bound artifact alone when adding it would cross the client's decode bound, so the ceiling is a property of the artifact alone. The2 * budgetcompile-time guard now certifies what it appears to: filler plus one budget-sized artifact always fits in a decodable round. (#109) - 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. A compile-time guard keeps the bound at least twice the server's clone batch budget, so filler plus one budget-sized artifact always fits in a round the client can decode. (#104) - Committing a file with zero-length content panicked in
blob.Store, which treated an empty artifact -- a normal, well-known Fossil blob -- as invalid input; the panic then triggeredmanifest.Checkin's postcondition defer, which re-panicked with an unrelated "manifestRid must be positive" message that masked the real cause.blob.Storenow accepts zero-length (and nil) content, andCheckin's defer re-panics with the original panic value instead of asserting a postcondition that was never going to hold mid-unwind (#68). - A commit no longer silently carries a tracked file's last-committed
content forward when that file has been deleted from disk without
Unmanage/Remove-- it now fails clearly, naming the file, matching fossil's own behavior for a missing file that is in scope for the commit being made. A missing file outside the commit's scope (relevant only to an explicitEnqueue) is left untouched exactly as before (#79). - Clone deadline now interrupts the mid-round phantom-fill crosslink cascade, ensuring the clone does not stall with phantoms outstanding. The same deadline is now threaded into the general push/pull phantom-fill cascade for consistency.
- Sync replies are now decoded by their wire's real
Content-Typeinstead of a hardcoded assumption. The HTTP server sends the correct type on each response; call sites receiving frames without a header (the byte-oriented NATS transport) pass the type explicitly. - PGP/SSH clearsigned manifests are now accepted: the deck module strips the clearsign framing before verification.
- Manifest now emits delete rows for files omitted by the check-in, matching fossil's own behavior. Renames now resolve their parent blob from the pre-rename path to ensure the rename-source blob exists and is reachable.
Changed
- Breaking: The module path has changed from
github.com/danmestas/libfossiltogithub.com/danmestas/go-libfossil. Allreplacedirectives ingo.modhave been dropped. Consumers must update their imports and remove anyreplacedirectives pointing to the old path. - 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. - Performance improvements throughout the sync and manifest subsystems: the
phantom-fill crosslink cascade now batches onto a shared content cache,
db.Queriercaches prepared statements to stop per-call SQL re-parse overhead, and delta-chain walks bound their hash-collision chain to prevent quadratic behavior on low-entropy input. History-walking expansions are routed through the content cache, cutting sub-64KiB delta-chain expansion allocation ~18x and boundingApplygrowth per-command to cut ~5x allocation overhead.
[0.6.3] - 2026-05-13
Fixed
- The SQLite DSN now sets
_txlock=immediate, so writers serialize atBEGINrather than discovering the conflict partway through a transaction (#33).
[0.6.2] - 2026-05-12
Fixed
Repo.Commitnow preserves the parent check-in's tracked files, instead of dropping files the commit did not itself touch (#30).
[0.6.1] - 2026-05-12
Added
CreateOpts.ProjectCodeaccepts an explicit project code when creating a repository, rather than always generating one (#31).
[0.6.0] - 2026-05-05
Fixed
- The WAL is checkpointed on
Close, so a repository written by this library is readable by canonical fossil without a recovery pass (#28).
[0.5.0] - 2026-05-04
- Added support for whole-checkin diff via empty filePath parameter.
- Added
Repo.UUIDFromRIDfor public rid-to-uuid lookup. - Added cross-repo release automation specification and implementation.
- Added release workflow with downstream dispatch for automated releases.
[0.4.5] - 2026-04-30
Fixed
- A clone against a hub that is being written to concurrently now converges, instead of looping without making progress (#17).
[0.4.4] - 2026-04-27
Fixed
- A partial xfer no longer panics with
rid=0; the incomplete transfer is handled rather than dereferenced (#14).
[0.4.3] - 2026-04-26
- Stabilized xfer encoder and multi-round sync with deferred manifest crosslink.
- Added
libfossil versioncommand to print build identifier including module version, Go toolchain version, and platform. - Wired live documentation URL into README and hugo baseURL.
- Added SVG architecture diagram and SDK refresh.
- Launched documentation site and Cloudflare auto-deploy.
[0.4.2] - 2026-04-26
- Fixed manifest crosslink deferral to handle referenced blobs arriving later.
[0.4.1] - 2026-04-25
- Implemented xfer encoder and multi-round sync foundation with manifest crosslink.
[0.4.0] - 2026-04-25
- Added
Repo.Pullpublic wrapper with HTTP transport and hostile-input assertions. - Added Tiger Style test coverage for
Checkout.Update. - Improved documentation and test coverage for
Repo.Pull.
[0.3.0] - 2026-04-21
- Added
Repo.ReadFilefor reading tracked file content from the repository at any version.
[0.2.0] - 2026-04-20
- Added
Repo.Mergefor branch-to-branch 3-way merge operations. - Added
Repo.Difffor computing diffs between tree states.
[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.