Skip to content
go-libfossil Timeline
Login

Timeline

50 ancestors of c36a69e36678d69d

2026-10-13
07:12
fix(sync): end card processing when a pushed file fails its handler (#402)

* fix(sync): end card processing when a pushed file fails its handler

sync-protocol-01 Section 5.2 has a file or cfile handler failure reset the reply, send the error and stop parsing, as fossil's xfer_accept_file path does. A wrong hash, a bad HASH or DELTASRC and a storage failure now break the card loop; a file delta with a held basis is verified before it is stored (Section 7.5).

Tracked in #390.

* fix(sync): model a bad file name in the clone-fatal probe

Tracked in #390. check-in: c36a69e366 user: [email protected] tags: main, trunk

06:42
fix(sync): send the clone reply and cluster igots in place (#401)

page_xfer writes the clone batch, its catalog, clone_seqno and push card at the clone card and the req-clusters igots at the pragma, so any later reset discards them (sync-protocol-01 Section 10.1). They were sent after the card loop and survived a bad command or refused push. The clone snapshot cookie is now read ahead of the loop.

Tracked in #389. check-in: 65783dbd3e user: [email protected] tags: main, trunk

06:34
fix(sync): apply ci-lock and ci-unlock for any requester (#400)

sync-protocol-01 Section 9.4 applies both pragmas with no Write test, as fossil's page_xfer does. The leaf sweep before each lock write bounds the lock table. nobody's lock row stores a null login; ci-lock-fail fossilizes LOGIN and names a login-less holder nobody. lock-timeout is read as db_get_int reads it.

Tracked in #392. check-in: e44a484403 user: [email protected] tags: main, trunk

06:14
fix(checkout): pick the no-target update target as fossil does; read untagged check-ins as the default branch (#399) check-in: 425dd36e13 user: [email protected] tags: main, trunk
05:59
fix(manifest,hash): verify the R card at commit; reject an unrecognized hash-policy (#396)

* fix(manifest,hash): verify the R card at commit; reject an unrecognized hash-policy

A check-in reads its manifest back and requires the R card to equal the digest recomputed over the resolved tree, byte for byte (artifact-format-01 6.3.4, AF-6.3-5). A hash-policy setting outside the ordinals 0 through 4 is now an error instead of a fall back to the default (7.1, AF-7.1-2).

Tracked in #387.

* fix(manifest,repo): drive the commit R check from conformance; refuse a bad hash-policy on write check-in: c42a2b90c6 user: [email protected] tags: main, trunk

05:32
test(conformance): cover the repo-state GAP and PARTIAL rows (#395) check-in: 3c3b12fdf2 user: [email protected] tags: main, trunk
05:26
fix(sync): cover the sync-protocol-01 coverage rows (#394)

Check the remaining partial and uncovered sync rows, and fix the client private-gimme answer, the unversioned-file NAME encoding, the check-in lock table, the uv-hash name form and the uvgimme round limit they exposed. Tracked in #392. check-in: 161d6ab68f user: [email protected] tags: main, trunk

05:17
fix(sync): handle server cards in request order, phantom gimme after push, clone bad cards (#393)

Tracked in #385.

The server verified the login card first and handled every control card before any data card. A file card ahead of its push card was stored, and cards ahead of a successful login got the login's capabilities. Cards are now handled in one in-order loop, as fossil's page_xfer reads them. A request holding an invalid clone sequence is first walked without side effects, which stands in for fossil's whole-request rollback.

A refused push without an earlier pull now resets and answers "not authorized to write"; a refused pull resets, answers "not authorized to read" and ends card processing (sync-protocol-01 Section 6.6).

An accepted push now asks for the server's phantoms after the card loop, at most 500, skipping what the client asked for, shunned names and private ones outside a private sync, as request_phantoms does. A pushed igot makes a phantom instead of drawing a gimme on the spot.

A clone reply card the client cannot read ends the reply-card loop instead of aborting: the session goes on, the second clone round is still made, and the clone then fails naming the card.

The conformance matchers that accepted "push denied" and "pull denied" are narrowed to the draft's text, and new cases put output ahead of a refusal and cards ahead of the push or login they depend on. check-in: 3beb6eeb15 user: [email protected] tags: main, trunk

04:51
test(conformance): cover artifact K, delta-lookup and delta-size requirements (#388)

* test(conformance): cover artifact K, delta-lookup and delta-size requirements

Single-name lookup in a delta manifest now follows artifact-format-01 8.4 (manifest.LookupFile), Marshal writes the K ticket ID lowercase (4.7.9), and new cases cover 3.1 size limits, 3.4 checksum mismatches and date-only timestamps. AF-6.3-5 and AF-7.1-2 stay uncovered with a reason in their rows.

* test(conformance): cover artifact K, delta-lookup and delta-size requirements (review fixes) check-in: 22b3a0f06d user: [email protected] tags: main, trunk

04:13
fix(sync): withhold SHA3 unversioned files from a pre-SHA3 server (#303) (#384)

sync-protocol-01 Section 7.6 (SP-7.6-1): a sender replaces a uvfile whose HASH is a SHA3 name with the error "Fossil version 2.0 or later required." when the peer's recorded VERSION is below 20000. The client gated artifacts this way already but sent unversioned files to such a server regardless.

buildUVFileCards now sends the refusal in place of the card, with or without content, and drops the file from the send list, as fossil's send_unversioned_file does. A deletion tombstone names no hash and is still sent. check-in: 6bde4f3729 user: [email protected] tags: main, trunk

04:08
fix(sync): answer a private gimme with igot HASH 1 for a recent peer (#375) (#383)

sync-protocol-01 Section 8.6: outside a private sync, a gimme for content held as private is answered with igot HASH 1 when the peer's date is 2020-04-13 or later, and with nothing for an earlier or unknown date. fossil's send_file decides it on remoteDate, which page_xfer reads from a client-version pragma carrying DATE and TIME.

The handler now records the peer's date from client-version and sends the private igot in place of the skipped file. check-in: 13e5e9d229 user: [email protected] tags: main, trunk

03:56
fix(sync): end card processing on refused writes and logins, count errors as fossil (#374) (#382)

Three server refusal paths now follow sync-protocol-01:

- Section 6.6 (erratum s6.6-file-card-after-refused-push): a file or cfile card without an accepted push resets the reply, sends "not authorized to write" and ends card processing, as page_xfer does. The pull's igot inventory still follows. - Section 6.4: a refused login ends card processing at its own position rather than ending the request. Cards before it keep their effect, the end-of-request inventory runs, and the reply carries the closing timestamp comment. - Section 5.3 (erratum s5.3-errors-count): errors N counts errors raised, including those a reset discarded, and skips the bad-command error, the private-content refusal and the SHA3 refusal. check-in: 5f4424320b user: [email protected] tags: main, trunk

03:51
fix(xfer): treat a payload cut short as its card's handler failure (#373) (#381)

sync-protocol-01 Section 5.1 (erratum s5.1-truncated-payload-receiver) makes a file, cfile or uvfile payload that the body ends inside a validation failure of the card's handler, never a stored artifact. The decoder failed the whole message instead, so a server answered HTTP 400 and a client dropped every card of the reply.

The decoder now yields such a card as a BadFileCard carrying the handler's malformed-line text and stops, as it already does for a line it cannot frame. The server answers 200 with that error and keeps the cards before it; the client keeps the prefix it processed. fossil 2.28 stores the bytes present; -01 is followed here. check-in: 648f5d4479 user: [email protected] tags: main, trunk

03:36
fix(tag): derive the check-in event time from direct date tags only (#372) (#380)

* fix(tag): derive the check-in event time from direct date tags only (#372)

Section 4.2, step 6 of repo-state-01 makes a date tag applied directly to a check-in set its event time, whatever order the tag and the check-in arrive in. Crosslinking read the date tagxref row whatever its source, so re-crosslinking after a propagating date tag retimed the descendants that inherited it, and a cancel, an unparsable value or 0 left no time or a time of 0, as fossil 2.28 does.

tag.RefreshEventTime now holds the rule, and both tag application and crosslinking call it: a direct, non-cancel date row whose value parses to 0 < r < 5373484.5 sets mtime; anything else restores the D-card time kept in omtime. The date-tag cancel, unparsable and zero cases are no longer deviations, and new before-checkin cases cover the other arrival order.

Tracked in #372.

* fix(tag): word the date-tag parse rule as step 6 does (#372)

* docs(changelog): tidy the entry spacing (#372) check-in: 30b9d65021 user: [email protected] tags: main, trunk

03:23
fix(manifest): resolve a delta manifest's baseline one level (artifact-format-01 4.7.3) (#379)

* fix(manifest): resolve a delta manifest's baseline one level (#371)

A B card may name another delta manifest (artifact-format-01 4.7.3). Drop the #275 refusal in loadBaseline, which ListFiles and crosslink share, and use the named manifest's own F cards.

Tracked in #371.

* fix(manifest): drop baseline delete markers and never write delta-of-delta (#371)

* docs(changelog): note delete markers and the writer rule for delta baselines (#371) check-in: 62396a79e5 user: [email protected] tags: main, trunk

03:13
fix(deck): accept the full Julian-day grammar of artifact-format-01 4.6.4 (#378)

* fix(deck): accept every Julian-day spelling of artifact-format-01 4.6.4 (#370)

Sign, exponent, leading and trailing point are now read, hex/inf/nan stay rejected, and the bound is checked after millisecond rounding as fossil does.

Tracked in #370.

* fix(deck): test the millisecond rounding bound and drop the stale gap marker (#370) check-in: 288594db14 user: [email protected] tags: main, trunk

02:28
test(conformance): check the -01 drafts of artifact, repo-state and sync (#377)

Re-pin the conformance suite to draft-fossil-artifact-format-01, draft-fossil-repo-state-01 and draft-fossil-sync-protocol-01, copied byte for byte from zit check-in 100351e26c. The delta format has no -01 and stays on -00. The -00 errata are folded into -01 and are no longer pinned; the six -01 errata are pinned for the fossil 2.28 behavior they record. conformance/rfcs/README.md records where each copy came from.

The requirement tables are re-read against -01, and new cases cover the date tag's event time (repo-state §4.2 step 6), the Julian-day grammar, a delta manifest naming a delta, dropped reserved F cards, the trailing-punctuation retry, truncated payloads, errors N, a file card after a refused push, the end of a request after a failed login, and the answer to a gimme for private content.

No library behavior changes. Where -01 departs from fossil 2.28 and go-libfossil follows fossil (a cancelled, unparsable or 0 date tag), the cases are marked as deviations. The non-conformances the re-pin exposes are marked as gaps, tracked in #370, #371, #372, #373, #374 and #375. check-in: e49527edd2 user: [email protected] tags: main, trunk

01:52
fix(sync): gate xtable-hash on a direction card (#351) (#369) check-in: b2d5659397 user: [email protected] tags: main, trunk
01:46
fix(sync): keep the client's gimme limit as fossil's mxPhantomReq (#350) (#368) check-in: 46fa73b523 user: [email protected] tags: main, trunk
01:40
fix(sync): server igot handling skips shunned hashes (#349) (#367) check-in: 1724e7ec32 user: [email protected] tags: main, trunk
01:10
fix(testutil): isolate fossil's global config in tests (#284) (#366)

* test: isolate fossil's global config in tests that run the fossil binary (#284)

Tracked in #284.

* test(testutil): document the file-level scope of the fossil home guard (#284)

* test(testutil): allow StartServer in the fossil home guard (#284) check-in: 8d3f18265e user: [email protected] tags: main, trunk

00:48
fix(test): kill fossil server's process group before cleanup (#352) (#365)

Start fossil server in its own process group through a shared testutil.StartServer helper and SIGKILL the whole group before Wait, so forked per-connection children cannot write into t.TempDir() during cleanup. Tracked in #352. check-in: bb864e6c58 user: [email protected] tags: main, trunk

00:38
fix(tag): apply bgcolor, user and comment tags to event as fossil does (#343) (#364)

* fix(tag): apply bgcolor, user and comment tags to event as fossil does (#343)

tag.ApplyTagWithTx now sets event.bgcolor, euser and ecomment from the bgcolor, user and comment tags for every tag type, NULL on a cancel, as tag_insert does. Crosslinking a check-in reads the three values back from tagxref, as manifest_crosslink does, so a tag that arrived while the check-in was a phantom takes effect.

Tracked in #343.

* fix(tag): store empty event override values as NULL (#343) check-in: 66cd641642 user: [email protected] tags: main, trunk

00:17
fix(sync): let Clone imply Read at the server's read gates (#312) (#363)

fossil's trunk page_xfer sets g.perm.Read for a requester with Clone, and the section 7.3 erratum states it for uvgimme. The handler treated only o, i and Admin as Read, so a Clone-only requester got no unversioned files, no xtable-hash inventory and no read-gated configuration.

This is an access change. handler.canRead, CanRead or a Clone held before any login (as page_xfer derives Read ahead of its card loop), now owns every read gate: the unversioned catalog for uv-hash and for a clone, uvigot and uvgimme answers, xtable-hash, and reqconfig. auth.CanRead is unchanged, and pull still needs o. The reqconfig interop test compares a Clone-only requester with fossil 2.28 given Read too, since the 2.28 binary predates the trunk change.

Pull still needs o although fossil trunk's derived Read lets a Clone-only nobody pull; that difference is deliberate. check-in: a67fe49556 user: [email protected] tags: main, trunk

00:12
fix(sync): log in through the x-f-l-c cookie and accept cleartext passwords (#304) (#362)

fossil's client moves its login card from the body into the x-f-l-c cookie once the server's server-version passes xfer_xflc_check (2.27, 2025-07-27 11:05 or later), and fossil's check_login, when a stored password is not 40 characters, tries the signature again against the shared secret that cleartext password derives (sections 6.4, 6.5). The client always logged in in the body, and the server checked only the stored value.

A request marks itself LoginCookieOK once the gate passes; Message.SplitLoginCookie moves its login card out of the body, and the HTTP transports (sync.HTTPTransport and the public NewHTTPTransport through the adapter) send it as the cookie, on every redirect too. The nonce is unchanged: it already covers what follows the login line, which in cookie form is the whole body. Transports that cannot set a cookie send the login in the body as before. The server side of the cookie landed with the server-version exchange.

HandleOpts gains LoginCookie, so an embedder serving HandleSyncWithOpts over its own HTTP handler can pass the cookie through; XferHandler and ServeHTTP read it themselves. check-in: 9d35c01fb3 user: [email protected] tags: main, trunk

2026-10-12
23:24
fix(sync): apply received unversioned files as fossil does (#308) (#361)

fossil's xfer_accept_unversioned_file stores a received file under the HASH its card gave, takes only the mtime when the content is the same and newer (status 2), and stores an omitted payload as NULL with size 0; send_unversioned_file sends nothing for a name it does not hold; and the client seeds uv_tosend from rows whose hash is not NULL (sections 7.3, 8.5.1). Both sides stored a received file under a hash computed by local policy and dropped status 2 and omitted-content cards; the server answered an absent name with a tombstone; the client queued its tombstones.

uv.Receive now owns the acceptance rule, which the server and the client share instead of each re-deriving it from uv.Read, uv.Status and uv.Write. check-in: d33a37fffd user: [email protected] tags: main, trunk

23:20
fix(sync): send clone replies as fossil's page_xfer does (#306) (#360)

fossil's send_compressed_file sends a cfile as the blob table stores it and pads a payload that does not end in LF; page_xfer sends a version 3 clone's reply as application/x-fossil-uncompressed, sends the unversioned catalog with a clone when uv-sync is on, and refuses a clone with a reset, the push card and "error not authorized to clone". The server recompressed each cfile, never padded, compressed the reply, sent no catalog, appended its refusal to the reply, and restarted a clone whose SEQNO was negative.

CFileCard's StoredBlob, set by the clone path from the blob table, is now what the encoder sends; xfer.Message.Uncompressed tells the HTTP server to send a reply as card text. The catalog goes to a requester with Read, as for uv-hash: fossil lets Clone imply Read there, which this server does not. A negative SEQNO, which section 8.1 exempts from its fatal, now ends the clone with clone_seqno 0, and the unit test that expected a restart expects that. check-in: 6c69fc1231 user: [email protected] tags: main, trunk

23:14
fix(sync): answer pushed config, ci-unlock and req-clusters (#305) (#359)

fossil's page_xfer refuses a pushed config card from a requester without Admin with a reset, "error not authorized to push configuration" and a break (section 9.3); drops a client's check-in locks on pragma ci-unlock (section 9.4); and answers pragma req-clusters with send_all_clusters, the cluster igots between "# sending-clusters" and "# end-of-clusters" (section 5.3). The server ignored the first two and recorded the third without answering it.

An Admin's config push is refused too, naming that this server does not apply pushed configuration, rather than dropped while the push appears to succeed. ci-unlock is honored for writers only, as ci-lock already is, and the cluster igots go to any requester, as in fossil: they name public artifacts and carry no content. check-in: 85c48f38eb user: [email protected] tags: main, trunk

23:10
fix(sync): exchange server-version and gate SHA3 content by peer release (#303) (#358)

fossil's page_xfer answers a client-version that carries DATE and TIME with pragma server-version, and both sides record the peer's release; send_file, send_compressed_file and send_unversioned_file then append "error Fossil version 2.0 or later required." in place of SHA3-named content for a peer older than 2.0, with a server treating a client that announced no release as one (sections 7.6, 9.1). The server sent no server-version and neither side kept or gated on the peer's release.

The server gates gimme, clone and uvgimme; the client gates its answer to a gimme. Because a server-version from a current release turns fossil's client to logging in through the x-f-l-c cookie (section 6.5), the HTTP server now reads that cookie: xfer.DecodeRequest puts its login card first with the nonce bound to the whole body, as page_xfer does. Without it a real fossil client's later rounds arrived as nobody. That moves sync.6.5.server-cookie-login, marked for #304, here. check-in: 6963b0a9dc user: [email protected] tags: main, trunk

23:06
fix(sync): end client sessions by section 3.7 and keep igot phantoms (#301) (#357)

fossil's client_sync continues a round when stored content or a new phantom leaves the phantom table non-empty, or when files are queued for the next request (section 3.7). The client instead continued on any received file and on any phantom until it had aged three rounds, kept a gimme it could not answer queued forever, and kept an igot's phantom only in the session's map. After a clone's first round it aborted on a server error, where the draft ends the reply-card loop and goes on.

converged now applies 3.7 (a) and (b). A gimme is queued only for content the client holds and may send. An igot's phantom is written to the phantom table, batched once per reply as content_new would write it, and the after-dephantomize cascades of a reply run as one batch (manifest.AfterDephantomizeBatch), so a pull no longer begins a crosslink transaction per artifact. A clone records a later round's server error, stops reading that reply, continues by 3.7, and fails at the end naming the error. Four unit tests that encoded the old continuation now expect fossil's. check-in: 62683d1d0f user: [email protected] tags: main, trunk

23:01
fix(sync): send pragma send-private first and keep a private basis private (#307) (#356)

fossil's client_sync emits pragma send-private ahead of client-version in its first request and right after it in later ones (section 3.2); the client put it after its igot, file and gimme cards. And fossil's content_new makes the missing basis of a private card's delta a private phantom (section 7.4), where the client made it public.

buildRequest is split into header, content and extra card builders so the header can carry send-private in fossil's place; the cards it sends are otherwise unchanged. check-in: 9d3644134d user: [email protected] tags: main, trunk

22:57
fix(sync): drop shunned artifacts and never ask for a shunned phantom (#302) (#355)

* fix(sync): drop shunned artifacts and never ask for a shunned phantom (#302)

fossil's xfer_accept_file and xfer_accept_compressed_file drop a received artifact whose hash is on the shun list, and its request_phantoms and content_new leave shunned names out of what a client asks for (sections 3.3 and 7.7). The server, the sync client and clone stored a shunned artifact, and the client asked for shunned phantoms.

storeReceivedFile, which every receive path shares, now drops a shunned artifact; the server checks first so a dropped file is not reported as missing after store. The client's phantom scan and its igot handling skip shunned names, so a delta whose basis is shunned is kept as a delta and the basis is never requested.

* refactor(sync): split the client's reply handling into per-card helpers

processResponse had grown to 232 lines: one switch over every reply card followed by the convergence check. It is now a loop over handleReplyCard, which hands each card to a focused helper, and converged, which keeps the termination rules apart from card handling. Behavior is unchanged. check-in: aea957939f user: [email protected] tags: main, trunk

22:52
fix(sync): take a uvfile on y alone, without a push card (#296) (#354)

fossil's page_xfer accepts a uvfile on the unversioned-write capability (WrUnver, y) alone; `fossil uv sync` sends uvfile cards with no push card. The handler refused each with "rejected: no push card", so pushing unversioned files from fossil to a go-libfossil server stored nothing.

This is an access change: a requester with y and no Write may now store unversioned files, as in fossil. A refusal (no y, or a HASH that does not match the content) now resets the reply, answers the error and breaks out of the card loop, as xfer_accept_unversioned_file's caller does. Tests pin both sides of the gate, and an interop test pushes with fossil uv sync. The real-fossil test helper now points HOME and XDG_CONFIG_HOME, not only FOSSIL_HOME, at the test's directory. check-in: b45ccf4351 user: [email protected] tags: main, trunk

22:48
fix(sync): refuse push and pull cards naming another project (#309) (#353)

fossil's page_xfer compares a push or pull card's PROJECTCODE with the repository's project-code before any capability check and, on a mismatch, resets the reply, answers "error wrong project" and breaks out of the card loop (section 8.5). The handler accepted either card whatever its project code.

The refusal goes through a new breakLoop, which the undecodable file card path now shares: the reply is reset to the one error, no later card is processed, and the post-loop work and timestamp still follow. Tests that synced repositories created with different project codes now create them with a shared one, and the DST simulator gives its leaves the upstream's. check-in: 821cf115ce user: [email protected] tags: main, trunk

20:17
fix(checkout): make WouldFork fossil's commit-time fork check (#347) (#348)

Since #291 WouldFork was fossil's leaf_ambiguity, which answers no at once for a version that is not a leaf. That is the case a commit forks in: when a pull brings in a child of the checked-out version, the version stops being a leaf, WouldFork said false, and the commit became a second child. EdgeSync's autosync relies on WouldFork to refuse that commit.

WouldFork is now fossil's commit refusal (src/checkin.c "Would fork", taken when !is_a_leaf(vid)): true when the version has a child on its own branch, read from plink as is_a_leaf does, merge children included. Another open leaf elsewhere on the branch no longer counts; fossil commits there, extending the checkout's leaf, and only warns afterwards.

The repo-state draft's Section 7.5 ambiguity test moves to checkout.LeafAmbiguous, and the repostate.7.5.ambiguity-* conformance cases call it. The superseded #291 changelog text and the WouldFork API doc are corrected. check-in: cd4a11828f user: [email protected] tags: main, trunk

19:37
fix(branch): check the open branch name in the check-in transaction (#316) (#346)

branch.Create refused a name an open leaf carries (fossil's branch_is_open), but checked before manifest.Checkin opened its transaction, and Checkin applied the branch tags, parent propagation and leaf state in later transactions. Concurrent creates of one name could all pass the check and all commit.

Checkin now derives its tags and leaf state inside its own transaction and runs an optional Precheck first in it. Create checks the name there, so check and write are one transaction: with BEGIN IMMEDIATE the second writer sees the first's open leaf and is refused. Create's redundant second tag pass after the check-in goes away. check-in: 9c071c7c22 user: [email protected] tags: main, trunk

19:21
fix(verify): rebuild derived state in one transaction (#335) (#345)

fossil's rebuild_db empties and re-derives the repository's tables in a single transaction. Since #285 Rebuild committed the emptied derived tables before the crosslink sweep, because the sweep found candidates outside any transaction and opened its own; a sweep that failed partway left event, plink, mlink, tagxref and leaf empty.

manifest.CrosslinkRebuild now runs inside the caller's transaction: candidate discovery, linking and the leaf/tag repair gate all on tx. Rebuild empties, re-derives and refills the bookkeeping tables in one WithTx, so a failure rolls back to the state it found. The incremental sweep keeps its batched commits; only the repair gate is factored out so both paths share it. check-in: 2832099f56 user: [email protected] tags: main, trunk

19:16
fix(manifest): set event.mtime from the newest date tag (#334) (#344)

fossil's check-in time override (`fossil amend --date`) is a one-shot date tag. manifest_crosslink takes a check-in's event.mtime from that tag's value when tagxref already holds one, and tag_insert moves event.mtime (keeping the original in omtime) whenever a date tag wins its tagxref row. Crosslink always used the D-card time, so every date-amended check-in sat at its original time.

Crosslinking a check-in now reads its date tag back, and tag.ApplyTagWithTx applies the override. tag.ApplyTag and tag.AddTag now derive through ApplyTagWithTx rather than repeating it, so a tag added directly moves event.mtime and event.bgcolor as crosslink would.

New fossil-referenced repository-state cases cover the tag after the check-in, before it (on a phantom), and newest-wins. check-in: 81b0b585e5 user: [email protected] tags: main, trunk

19:01
fix(sync): frame HTTP requests and replies as fossil's http.c does (#310) (#342)

The server refused application/x-fossil-debug with HTTP 415, where fossil dispatches it to sync like the other x-fossil types. The client read any 2xx as a sync reply and decoded a reply as compressed only when typed exactly application/x-fossil, so a reply with no Content-Type was read as plain cards and a proxy's HTML page was parsed as cards.

Replies now go through xfer.DecodeReply, which applies section 4's reply rule by case-insensitive prefix: no type is the compressed container, an x-fossil-debug or x-fossil-uncompressed prefix is card text, another x-fossil prefix is compressed, and anything else is refused. Only status 200 is a reply. check-in: 47dc6363c0 user: [email protected] tags: main, trunk

18:07
fix(sync): handle unknown, error and pull-only cards per the draft (#300) (#341)

The server ignored reply-only cards a client should never send and the client read on past cards that end a reply.

On the server, a client-sent error or message card -- reply-only cards fossil's page_xfer has no branch for -- now answers "bad command" and resets the reply, not silence. A push card from a requester who may read but not write answers "message pull only - not authorized to push", as fossil does, rather than a refusal, so the pull proceeds. To reach that branch the client now sends its pull card before its push card, fossil's order: the server's read-vs-pull-only decision turns on having seen the pull card first.

On the client, an error card ends the reply-card loop (§10.1): the cards before it stand, those after it are not read. A "message pull only ..." (§10.2) stops the client pushing -- it keeps pulling but answers no further gimme and drops its push card one request later, matching fossil clearing SYNC_PUSH after it has already emitted the next request's header.

Behavior change worth a look: a first --private pull against a server that denies private content (no 'x') now receives no content that session, because the denial error ends the reply-card loop before the server's inventory, as fossil's client does. The private blob still never reaches an unauthorized client. TestSyncPrivateEndToEnd/no_x_capability is updated to this. check-in: c9436f82cc user: [email protected] tags: main, trunk

18:01
fix(xfer): follow the draft's card grammar, not whole-message failure (#299) (#340)

The card decoder refused a card by failing the entire message: the server answered HTTP 400 where §5.2 has it emit "bad command" and parse on, and the client dropped a whole reply including its valid prefix.

The decoder now yields a bad-command card for a line no rule accepts (an unknown operator, a bare pragma, a gimme with a surplus token, a config with a signed size) and a bad-file card for a file/cfile/uvfile line it cannot frame. The server resets its reply, emits the matching error (bad command, or the handler's "malformed file line" and the like) and either parses on (bad command) or stops (bad file), as fossil's page_xfer does; the client ends the reply-card loop, keeping the cards decoded before the bad one; a clone treats it as a non-retryable protocol error.

Tokens are split on ASCII whitespace only, within fossil's six-token window (blob_tokenize over aToken[6]): a no-break space no longer separates tokens, and a seventh token is ignored rather than rejected, so igot and private tolerate a surplus token. A full cfile's USIZE is carried through as metadata, not checked against the inflated size (the hash binds the content instead), and the byte after a config payload is consumed whatever it is, both matching fossil's xfer_accept_compressed_file and page_xfer.

This also resolves the bad-command cases tracked in #300 (sync.5.2.bad-command, .bad-command-resets, .unknown-prefix, sync.10.1.bad-command-keeps-mutations), whose marks are removed here. check-in: ddc4b5396d user: [email protected] tags: main, trunk

17:52
fix(deck): enforce card legality, required cards and per-type semantics (#273) (#339)

deck.Parse determined an artifact's type but then checked nothing against it, so a card a type disallows, a required card left out, or a tag that breaks a per-type semantic rule all parsed.

Add deck/legality.go with fossil's manifestCardTypes table (allowed and required cards per type) and the §5.4 semantic checks: a control artifact's tags may not target itself, a technote's tags are all self-referential singletons, and a forum post carries at most one of an H or I card and at most one parent. Parse now determines the type (inferType gains the B, J and Q type-setters it was missing and falls back to a control artifact, not an empty check-in, when nothing sets a type and there is no comment or parent), then applies legality and the semantic checks. A missing Z card is now caught by the universal required-card rule.

Several internal/manifest and internal/deck tests built incomplete artifacts fossil also rejects -- a cluster with a D card, a forum post with both an H and an I card, control-shaped D/U/Z artifacts with no T card, and single-card fixtures missing their type's required cards. Frame each as a valid artifact of its type; the behavior each test pins is unchanged.

This also makes two repostate conformance cases conform: the technote with a non-self tag is now rejected at parse (not by a later isArtifact gate), and the opaque-control rebuild case. Their marks are removed in conformance/repostate_gaps_test.go. check-in: 7b8f655da6 user: [email protected] tags: main, trunk

17:44
fix(deck): re-encode present-but-empty values and keep timestamp precision (#272) (#338)

Marshal dropped a present-but-empty C comment, N mimetype or T value, a bare P card and an empty W body, and wrote D to the millisecond and E to the second, so an artifact carrying any of those did not round-trip through Parse and Marshal.

Carry the presence of a C, N or P card, and of a T value, in unexported Deck/TagCard fields that Parse sets and Marshal reads, and re-emit the W card whenever its body is non-nil (a parsed empty body is a non-nil empty slice). The public API is unchanged: a caller that sets a non-empty value still has it marshalled. Write the D and E timestamps at their writer precision -- millisecond and second -- extended to keep whatever finer fraction the parser read, so a sub-second timestamp survives the round trip. Remove the three FuzzArtifactRoundTrip knownLosses these fixes retire. check-in: 0902561005 user: [email protected] tags: main, trunk

17:39
fix(deck): parse D and E timestamps with the full 4.6.4 grammar (#271) (#337)

The parser tried only two time.Parse layouts and rewrote a lowercase t to T, so it rejected the HH:MM, timezone, hour-24, Feb-31 and negative-year forms fossil accepts via SQLite's julianday(), and it silently accepted a lowercase t separator the draft rejects.

Add deck/timestamp.go: a byte cursor that parses the date-time form (optional leading minus, YYYY-MM-DD, one or more whitespace-or-T separators, HH:MM with optional seconds and fraction, an optional Z/z or +HH:MM/-HH:MM zone), the date-only form, and a bare Julian-day number, with the exact-digit field ranges the 4.6.4 erratum scopes to those forms and the "strictly after -4713-11-24" lower bound. A lowercase t separator is rejected. time.Date normalizes the hour-24 and unchecked-day forms the way julianday() does. check-in: 127685f0f5 user: [email protected] tags: main, trunk

17:34
fix(verify): derive Rebuild state through the crosslink sweep (#285) (#336)

verify.Rebuild kept its own partial derivation of repository state: it wrote event/plink/mlink/tagxref rows only for check-ins and control artifacts, applied tags from artifacts the crosslink would refuse, skipped absent tag targets and parents instead of reserving phantoms, wrote no plink baseid, and computed leaves as "no child at all" regardless of branch. Against fossil rebuild it missed hundreds of event, plink, tagxref and leaf rows on real repositories.

Make Rebuild empty the crosslink-derived tables and run the same sweep a clone runs, so derived state has a single owner. The three partial derivation files (rebuild_tags.go, rebuild_manifest.go, rebuild_leaves.go) are deleted; Rebuild keeps only what the sweep does not do -- blob hash verification and the unclustered/unsent bookkeeping.

A rebuild is terminal: nothing more will arrive, so a check-in whose file or baseline content is absent must be linked with phantoms rather than deferred for a blob that never comes, as fossil's add_one_mlink does via uuid_to_rid(...,1). Add manifest.CrosslinkRebuild, a sweep that does not defer; on a complete repository it is identical to Crosslink. Rebuild's leaf rule now follows fossil's branch-aware sweep, so an untagged parentless check-in is no longer a leaf -- the same Section 7.3 deviation the sweep and per-artifact paths already carry. check-in: 33959a6e3e user: [email protected] tags: main, trunk

17:29
fix(deck): enforce the pathname, wiki-name and reserved-name rules (#269) (#333)

The parser stored F-card filenames and old-names, A-card filenames and targets, and L-card titles without the §4.6 name predicates, and never dropped a reserved repository filename.

Add deck/pathname.go with the three predicates ported from fossil: the non-strict simple-pathname check (file_is_simple_pathname_nonstrict), the well-formed wiki-name check (wiki_name_is_wellformed), and the reserved-name test (file_is_reserved_name). Apply the simple-pathname predicate to the decoded F filename and old-name and the A filename; the wiki-name predicate to the decoded L title and to an A target that is not an artifact hash; and drop a reserved F-card filename silently after the sort-order check, as fossil does. check-in: 119ef76ae9 user: [email protected] tags: main, trunk

17:24
fix(deck): validate every artifact-hash and checksum position (#268) (#332)

The parser stored hash-bearing tokens without checking them, so an F, B, E, G, I, K, M, P, Q, A-source or T-target token of the wrong length or alphabet passed, and the A target and N mimetype were kept raw rather than decoded.

Validate each position against the §4.7.1 map: 40 or 64 case-insensitive hex for an artifact hash, 40 for the K-card ticket id, 32 for the R checksum. A baseline manifest now requires a hash on every F card; only a delta manifest (one with a B card) may carry a hashless deletion card. Decode the A target and the N mimetype per §4.3.1 and re-encode them on marshal, keep the other hash tokens raw, and keep only the final path component of the A filename. Reject a Q sign other than + or -, and a tag name that is entirely hex in either case.

Four internal/manifest crosslink tests built manifests fossil itself rejects -- two R cards 40 hex long (R is 32), an attachment source "abc123", and a baseline manifest with a hashless deletion card. Make those fixtures valid artifacts (32-hex R, a real source hash, the merge expressed as a delta off its primary parent so the deletion card is legal); the mlink and crosslink outcomes they assert are unchanged. check-in: 1a15366e29 user: [email protected] tags: main, trunk

01:42
fix(deck): tokenize cards as fossil does and enforce per-card arity (#266, #267) (#331)

The parser split card arguments with strings.Fields and strings.TrimSpace, which collapsed a repeated space, split on tab and trimmed a trailing CR, and its line reader skipped blank lines. It also gave no card a maximum arity, so an extra token was absorbed.

Replace the line reader with a scanner that mirrors fossil's next_token/next_card: tokens are separated by a single space, a card ends at LF, and tab and CR are ordinary token bytes. A blank line, an empty token between two others, a bare card where a value is required, and residue after the last card are rejected; a line ending in a space keeps a present-but-empty last value. The W card's size is parsed as leading decimal digits with an overflow guard and the body must be newline terminated. The clearsign signature trailer is matched on 29 bytes, as fossil's remove_pgp_signature does, so a mangled final dash no longer hides it.

A faithful tokenizer enforces arity intrinsically -- fossil rejects an extra token as residue once the card's fields are read -- so #267 is resolved by the same change: each card is held to its exact Table 2 (§4.2.1) token count. The card parsers now take the scanner's tokens rather than one argument string; update the J-card and U-card unit tests to the new shape and to fossil's actual verdict on a multi-space U-card (rejected, not "anonymous"). check-in: 1af61c2713 user: [email protected] tags: main, trunk

01:37
fix(deck): judge artifact framing by the rule it breaks, not the Z checksum (#265) (#330)

deck.VerifyZ required a Z card at the last 35 bytes, read the final byte as the Z card's LF, and rejected a too-short input, so framing failures (a missing final LF, residue after the Z card, a short or wrapper-only blob) were all reported as checksum errors. Rewrite verifyZ to mirror fossil's verify_z_card: locate the Z card, never read byte n-1, treat an absent or unparsable Z card as "continue", and error only on a present-but-wrong checksum.

Restructure Parse into visible stages: a raw framing check (non-empty, ends in LF) on the bytes before the signature wrapper is stripped; the first-card line gate (upper-case letter, space); the Z self-checksum; then the card loop, which now scans the whole content and parses the Z card as an ordinary card rather than trimming a fixed 35 bytes. A missing Z card is judged by the universal required-card rule after the loop.

Update TestVerifyZTooShort, which encoded the old reject-on-short behavior, to expect fossil's: a too-short input has the Z card absent. check-in: 7aab5bfd8e user: [email protected] tags: main, trunk

00:31
fix(libfossil): sym- prefix and reserved-prefix refusal in Repo.Tag (#293) (#329)

fossil tag add places a check-in tag in the symbolic namespace with a sym- prefix, so it resolves as a branch/tag name in update and merge, and refuses the reserved wiki-, tkt- and event- prefixes. Repo.Tag stored the name verbatim. It now refuses the reserved prefixes and, when the target is a check-in (fossil's whatis_rid_type==CFTYPE_MANIFEST), prefixes sym-; other artifact types keep the name bare. The raw tag.AddTag primitive is unchanged. Callers pass the bare name; fossil prefixes the raw name, so sym-x becomes sym-sym-x on a check-in. check-in: b5c515e10f user: [email protected] tags: main, trunk

00:24
fix(tag): keep a propagated-tag cancel from deleting newer inherited rows (#288) (#328)

* fix(tag): keep a propagated-tag cancel from deleting newer inherited rows (#288)

The cancel branch of the propagation walk used COALESCE(srcid=0, 0), deleting any inherited row whatever its mtime. fossil's tag_propagate guards both the propagate and cancel cases with srcid=0 AND tagxref.mtime < :mtime: an inherited row newer than the tag is left in place and the walk stops there. Unify the two queries on that guard, parameterizing only the default for a child with no row.

* test(tag): cancel keeps newer inherited rows; bind the propagate default (#288)

Bind the per-type doit default as a query parameter instead of Sprintf-ing it into the SQL, and drop the redundant var rows/var err. Add unit coverage: a cancel skips an inherited row newer than itself and halts the walk there, and re-propagation at equal mtime keeps the first value. check-in: 58aabe243a user: [email protected] tags: main, trunk