What this project has had to understand about Fossil, beyond its manual: the points that changed a design or caused a failure. It is not a guide to Fossil.
Serving
- Only the server writes the served repository. Every other process
reaches it through HTTPS sync with its own login (decision 3 in
HISTORY.md). - An extension page's output is held until the program exits. A CGI
program under
--extrootcannot stream: three events written two seconds apart arrived together at six seconds (Fossil 2.27, measured for f718b75092). A live stream needs its own server. - Run as root, the server jails itself into the repository's directory,
so an
--extrootoutside it is not found. The served host runs with--nojail(scripts/start-fossil.cr). email-urlis the repository's public address. Workspaces clone it; a launch is refused when it is unset or does not resolve (0160971667). A clone's copy of a setting is not the server's: reading it from a clone gave a stale host name (bafb3a31bc).
Hooks
- The after-receive hook is started inside a write transaction, from the
backoffice (Fossil's hook documentation).
As Fossil 2.27's
source shows (
hook_backofficein hook.c,popen2andpclose2in popen.c,backoffice_run_if_neededin backoffice.c):- Standard input is the list of artifacts received since
hook-last-rcvid, one per line, a web post included: each its hash, a space and Fossil's summary of it (describe_artifactsin name.c), which begins with its kind:check-in to BRANCH by USER on DATE,forum-post ROOTorforum-reply-to ROOT(the first 14 hex digits of the thread's root post),ticket TICKET(the first 21 of its ticket-id),file NAME,tag …,wiki "NAME",event …andattachment …; anything elseunknown, or as an artifact others name it, such ascheck-in referenced by "NAME" tagfor one not received. - Standard output is a pipe Fossil closes without reading; SIGPIPE is ignored, so a write there fails.
- Standard error, under the server, is
/dev/null: the backoffice child callssetsid(), reopens descriptors 0 to 2 on/dev/nulland closes 3 to 99 before running hooks. The exit code is ignored. Fossil keeps nothing a hook says; a hook writes its own log. - Fossil does not wait for the hook:
pclose2only reaps children that have ended. The backoffice writes the list, recordshook-last-rcvidand commits while the hook goes on, in the backoffice's own session. So the hook is already detached; it reads the repository only after waiting out that write transaction (a busy timeout), never while it holds. - A web post reaches the hook like a sync, and Fossil records its
receipt (
rcvfrom) under the logged-in login (98fc903663).
- Standard input is the list of artifacts received since
- The backoffice starts only after a page that answers 200 (
cgi_replyin cgi.c). A form's POST answers 302, so a web post reaches the hook with the page shown after it. Each start takes a lease for a minute: a page within it queues a backoffice that waits for the lease to end, and withbackoffice-nodelayon gives up, leaving the receipt to a later page;backoffice-disablestarts none, so hooks run only underfossil backoffice. Tried with Fossil 2.27 on a scratch server, 2026-10-06: a post made a second after a page reached the hook 61 seconds later. - Run as root without
--nojail, the server's backoffice ran no hook: the server jails itself into the repository's directory, and a page's backoffice child left no lease and ran nothing (Fossil 2.27, the same scratch server). The served host runs--nojail, and so does the hook's spec. A backoffice leaves its server's process group (setsid), so one waiting out a lease outlives its server by up to a minute. - Hooks are a JSON list in the
hookssetting,{"cmd", "type", "seq"}, named by their place in it:fossil hook deleteremoves the places given one at a time, each moving those after it, so the last goes first.%Rbecomes the repository's path, unquoted. fossil settings NAMEshows the value Fossil reads: the repository's (local), or else the account's global one (global).- A hook is a hint, not a queue. Work is found again by querying the record after startup and after each sync.
- A pull runs no hook: on a clone,
fossil pullbrought a ticket change and ran no after-receive hook;fossil backofficethen ran it, listing the change (Fossil 2.27, a scratch server, 2026-10-12).
Checkouts and reads
Tried with Fossil 2.27, 2026-10-07:
- A checkout is its checkout file,
.fslckout(or the older_FOSSIL_), an SQLite database whosevvartable holds therepositoryit opens and itscheckout-hash; Fossil finds it from the working directory up, and OCX reads it so too (theuse-fslckoutbranch's example, 5909a744f5). fossil infooutside a checkout exits 0 with norepository:line; in a checkout whose repository is gone it exits 1.fossil whatisof a name that names nothing exits 0, sayingunknown:; outside any repository it exits 1.fossil sqlprints a NULL asNULL, unquoted, so a read that may find none coalesces it to nothing.fossil remoteprints the URL with its login but not its password.
Clones and logins
A tag pushed by a login without Check-In (
i) is refused: the push printsError: not authorized to write, sends nothing, and still endsPush donewith status 0, as a sync endsSync done(Fossil 2.27, tried on a scratch server, 2026-10-06).ocx2 auditreads that line, and from a clone reads its verdict back by a second push that must send nothing.Autosync pulls before each commit and pushes after it; with the server away, the commit is refused. Workspace clones of an HTTPS remote run with it on (0160971667).
A clone of a repository file copies every login's password hash, so OCX deletes the other logins from such a clone (393e0757a7).
A remembered remote URL holds the password, obscured, not encrypted:
fossil test-obscureturns the stored value back into the password (tried with Fossil 2.27 on a scratch repository, 2026-10-03). Whoever reads a clone holds its login, so each holder gets a login of its own.fossil clone --save-http-password URLremembers the password the URL carries; without it, and with no terminal to ask at, the clone forgets it and its first pull is refused, andfossil remote URLdoes not save it either (Fossil 2.27, 2026-10-06).fossil --args -reads more arguments from standard input, one per line, where they stand among the others. A URL holding a password given that way never appears in the process's command line, which any account on the host may read in /proc while it runs (Fossil 2.27, 2026-10-13).A pull, push or sync can fail and exit 0. When the login no longer works, fossil syncs as nobody and prints
Error: login failed; when the remote cannot be reached, it makes no round trip (Round-trips: 0) and says so only on standard error (Fossil 2.27, 2026-10-06; OCX'sFossil.unsynced).What a clone says, fossil does: its
ssh-commandsetting runs when a pull reaches anssh://remote (Fossil 2.27, 2026-10-06). A session can write its clone, so OCX syncs, commits and reads a unit's clone only in the unit's sandbox, never on the host (spec/ocx/workspace_spec.cr).The global
--user LOGINmakes a command's artifacts that login's, and Fossil records their receipt (rcvfrom) under it; a change pushed from a clone is recorded under the login that pushed it.A repository has no
forumposttable until its first forum post.A login with
pchanges its own password on the/loginpage, with the page's CSRF token and a same-originReferer; the old password is refused at once.fossil user passwordchanges only a local user table.Capabilities cannot limit a login to one branch, nor to audit tags only;
iadmits any write. OCX narrows what an agent may do with its own grants (Fossil's capability reference).
Forum posts
- No fossil command writes a forum post; only the forum's web page does.
ocx2 postcomposes the post's artifact (D, G, I, N, U, W and Z cards; a card's argument writes a space as\s) and puts it into a clone withfossil test-content-put, which works only in an open checkout and does not index the post there;fossil test-crosslinkindexes it, given its hash, though its usage says a record id. Pushed, the served repository indexes it as a post received from the pushing login (Fossil 2.27, tried on a scratch server, 2026-10-06). - Any push needs Check-In (
i), a forum post's too: a login with the forum's capabilities alone getsError: not authorized to write. Pulling needs Read (o) alone: a clone made by a login holding onlyoholds the tickets. A write on the repository itself needs no capability at all: a login holding none wrote a ticket change with--user, and the receipt names it, with no address (Fossil 2.27, a scratch server, 2026-10-12). - A clone records everything it pulls as received from its own login,
so who wrote a post there is only its U card; on the served repository,
rcvfromnames the login that sent it. The clone's login holds setup (s) in the clone, and a pull's receipt carries the server's address, as a push's carries the client's; a write on the repository itself has none (Fossil 2.27, a scratch server and clone, 2026-10-12). - A repository has no
forumposttable until its first post.
Check-ins, tags and comments
fossil commitrefuses a comment containing<…>as possible HTML, such as an e-mail address in a trailer, unless given--no-verify-comment(d26240540f).fossil amend --commentreplaces the comment shown and read; the original stays in the check-in's manifest and can be recovered from it (877097e6b9).fossil commit --branch NAMErefuses a name an open branch has (an open branch named "NAME" already exists - use --force to override), so a unit's first commit starts its branch only while no one else has;fossil branch currentsays which branch a tree is on (Fossil 2.27, 2026-10-06).- A tag added again replaces the earlier value, so a corrected
auditedverdict supersedes the first while history keeps both (6912141fbe). fossil tag add --raw TAG CHECK-IN VALUEnames the tagTAGitself, notsym-TAG, so it never becomes a name for the check-in, and on that check-in alone (itstagxrefrow's type 1), unless--propagate; the check-in comes before the value. Written on the repository itself with--usernaming a login that holds no capability, it is received from that login with no address, and its change is aneventof typegnaming it (Fossil 2.27, a scratch repository, 2026-10-12).
Git export and import
Tried with Fossil 2.27 and a scratch git repository, 2026-10-07 (f3bb7b2032):
fossil git export DIRwrites a git repository inDIR/.gitand what its next export needs inDIR/.mirror_state/db:mconfig(the main branch, the autopush) andmmark, each check-in's hash beside the git commit it became. It recordsDIRin the repository'slast-git-export-reposetting, and runs again into it given noDIR;--if-mirroreddoes nothing for a repository with none. It pushes only where--autopushsaid, a setting the export keeps.fossil git statussays where the export is, when Fossil last exported, its autopush, without any password its URL holds, and main branch, andN check-ins awaiting exportorup-to-date.git fast-export --allnames a commit by the first ref that reaches it, a tag before a branch: it wrotemain's history underrefs/tags/v1, sofossil import --gitput those check-ins on no branch, with only the newest onmain, and tagged every check-intrunk. An import names branches first.- A plain
fossil import --gitthenfossil git exportrewrites every commit, keeping each tree: authors become their e-mail, times move a second, zones are dropped, each message gains aFossilOrigin-Name:line, the check-ins on no branch come back on a branchunknown, and an annotated tag comes back lightweight. - Seeded, the export keeps the upstream's commits. Export marks from
both sides (
git fast-export --export-marks,fossil import --git --export-marks), joined on the mark intommark, and the git repository holding the upstream's objects, make the first export add nothing. A Fossil check-in then exports as a commit whose parent is the upstream's own, and a clone of the upstream fetches it as a fast-forward. Itsmconfigkeystart, the newest exported check-in's Julian day, is whatfossil git statuscounts awaiting check-ins from. git fast-export --brancheswith no tags gives every check-in its branch, andfossil git exportleaves refs it does not know as they are, so an upstream's annotated tags stay as they were.fossil import --gitkeeps git's branch names,masteramong them, and a repository with notrunkopens a checkout on its newest check-in, whatever its branch.--refspec refs/heads/main:refs/heads/trunkon the fast-export names the upstream's own branchtrunkin Fossil, and the export, its main branchmain, names it back.
SQL
Each node's
fossilbrings its own SQLite, bundled or the system's: this host's Fossil 2.27 runs SQLite 3.51, and the BeagleY-SSD node's refused an aggregate's ownORDER BY(json_group_array(... ORDER BY ...)), which SQLite takes only from 3.44 (the maintainer, 2026-10-07). OCX orders rows in Crystal instead, by keys each row carries, and the gate's ledger prints the SQLite itsfossilruns on.fossil sqlreports an SQL error on standard error and exits 0 (Fossil 2.27):ocx2counts anyError:there as a failure.
Markdown
- Headings get no anchors. A link to a section needs an explicit
<a id="…"></a>in the heading, which survives safe rendering (5bbde981d9).