50 descendants of a3178966834aef75
|
2026-10-12
| ||
| 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 | |
| 00:18 |
fix(manifest): apply the parent (reparent) tag (#286) (#327)
* fix(manifest): apply the parent (reparent) tag (#286) A singleton parent tag relocates a check-in's ancestry. fossil's manifest_crosslink_end rewrites the tagged check-in's plink and mlink to the hashes the tag names, before it propagates tags, so the leaf recompute sees the new edges and an old parent with no remaining child becomes a leaf. ReparentCheckins does this over a deck whose P-card is the override, reusing the normal plink/mlink builders. It runs from the crosslink repair gate and from verify.Rebuild, which had no reparent step. * test(manifest): reparent plink/mlink/leaf, idempotence, phantom target (#286) Also fold the plink baseid resolution into a shared checkinBaseid helper used by both the normal linkage path and the reparent pass, and note that the reparent repair is O(reparented) per round like fossil's manifest_crosslink_end. check-in: 088f1d8a90 user: [email protected] tags: main, trunk | |
| 00:13 |
fix(manifest): refuse a technote with a non-self or non-singleton T-card (#287) (#326)
fossil rejects a CFTYPE_EVENT artifact at parse time unless every T-card is a '+' tag on the technote itself; a technote may not tag another artifact, propagate, or cancel. go-libfossil's parser still accepts such a technote, so gate it in isArtifact: a technote whose T-cards are not all self-referential singletons is not an artifact and applies none of its tags. check-in: cf9089ec35 user: [email protected] tags: main, trunk | |
| 00:08 |
fix(manifest): make root check-in mlink derivation idempotent (#294) (#325)
* fix(manifest): make root check-in mlink derivation idempotent (#294) The root-file branch of insertCheckinMlinks inserts each file row with no already-derived guard, unlike addMlink's pmid>0 early-out, so crosslinking a root a second time -- which the relink path does after clearing event and tagxref -- inserted a second copy of every row. Skip the block when a root row (pmid=0) is already present, matching the idempotence of fossil's manifest_crosslink. * test(manifest): root check-in mlink derivation is idempotent on relink (#294) check-in: d5fdd30d12 user: [email protected] tags: main, trunk | |
| 00:03 |
fix(manifest): write the first check-in's sym-trunk as propagating (#289) (#324)
fossil new and repo-state §6.1 write *branch and *sym-trunk, both propagating, so the trunk alias descends to every check-in on the branch. The initial check-in built sym-trunk as a singleton (+), which stopped the alias at the root. check-in: 470f85d9fa user: [email protected] tags: main, trunk | |
|
2026-10-11
| ||
| 23:54 |
fix(hash): default to sha3 when empty and persist the auto promotion (#274) (#323)
With no hash-policy row, NamingFor now follows fossil's hname_default_policy: sha3 if the repo holds a SHA3 artifact or no SHA1 one, auto otherwise (7.2). hash.PromoteFor, called by blob's single row writer and by sync's received-content store before an artifact enters, writes hash-policy=2 when an auto repo takes a SHA3 name (7.4), as content_put_ex does, so fossil opening the repo sees the promotion. A row still saying auto next to SHA3 artifacts, from before the promotion was written, keeps deriving sha3. check-in: cc76fd023f user: [email protected] tags: main, trunk | |
| 23:44 |
fix(manifest): reject a non-baseline as a delta manifest's baseline (#275) (#322)
ListFiles took any parseable artifact named by a B card as the baseline, so a delta manifest or a control artifact produced a partial or empty file set. Sections 4.7.3 and 8.3 require the target to be a baseline manifest: a checkin with no B card of its own. ListFiles now returns an error otherwise. check-in: 9fbe647b7f user: [email protected] tags: main, trunk | |
| 23:39 |
fix(manifest): refuse check-in filenames outside the writer profile (#270) (#321)
Checkin wrote any F-card filename it was given. The artifact format's writer profile (4.6.1) forbids a backslash and anything but valid UTF-8, surrogates and overlong forms included; fossil rejects a manifest that names one. storeFileBlobs now refuses such a name or old name before storing anything. check-in: 357d10dac6 user: [email protected] tags: main, trunk | |
| 23:34 |
fix(delta): refuse a source or target of 2^32 bytes or more in Create (#279) (#320)
The delta format holds integers below 2^32 only, so a producer must not write a target length or copy offset at or past that. Create now returns ErrTooLarge for such inputs; Deltify then stores the content whole, as fossil does when a delta is not worthwhile, and the other callers propagate the error. Apply's header check becomes >= so it refuses exactly what Create will not write. The bound is tested on lengths, as a direct test needs a 4 GiB input. check-in: 38eddf43f1 user: [email protected] tags: main, trunk | |
| 23:28 |
fix(blob): fill a phantom in place when its content is stored (#278) (#319)
blob.Store, StoreDelta and StoreDeltaRaw now share writeRow, which fills a phantom row of the same name in place (content, size, and removal from the phantom table) as fossil's content_put does, and returns that rid. existingName no longer treats a phantom as stored content, so a check-in of a file matching a known phantom stores the file instead of recording a manifest that points at nothing. check-in: 23cfc00bfe user: [email protected] tags: main, trunk | |
| 23:18 |
fix(repo): resolve version names in fossil's order and forms (#290) (#318)
ResolveVersion chose a tag by tagxref.mtime instead of the carrying check-in's event.mtime, fell back to the tip when no check-in carried sym-trunk, tried a plain name before an artifact-hash prefix, and recognized none of fossil's rid:, tag:, root:, start:, merge-in: or NAME:TIMESTAMP forms nor the trailing-punctuation retry. It is now a port of fossil's symbolic_name_to_rid (src/name.c), written as an ordered sequence of small attempts in fossil's order: tip (which, like fossil, falls through to the ordinary forms when there is no check-in), rid:, tag:, root:/start:/merge-in: (branch points, via start_of_branch and, for merge-in, the youngest ancestor in the branch), NAME:TIMESTAMP, then a 4-64 character hex prefix (tried before a plain tag, matching any artifact and reporting more than one match as ambiguous), then a plain sym- tag chosen by event.mtime, and finally a retry with one or two trailing punctuation characters removed. There is no tip fallback for an unknown name. The exported signature is unchanged; "" still means tip. NAME:TIMESTAMP now rounds the boundary UP to the last millisecond of the stated precision (fossil's fossil_roundup_date / fossil_expand_datetime): a bare date becomes 23:59:59.999, HH:MM becomes :59.999, HH:MM:SS becomes .999. The compact YYYYMMDD forms, a fractional-second suffix and a trailing Z/UTC designator are accepted; the time is read as UTC, matching fossil's default timeline-utc. Bare YYYY-MM and hour-only forms are not supported. Unit tests cover the three round-up precisions (inside/outside the window), the zulu designator and the ambiguous-prefix path. check-in: 329ce1ad02 user: [email protected] tags: main, trunk | |
| 23:06 |
fix(branch): refuse an open branch's name on create; close a branch by its tip (#292) (#317)
branch.Create accepted a name an open branch already used, letting a second branch of the same name be created; and branch.List marked a branch closed when any check-in on it carried a closed tag, so closing a non-tip check-in hid an open branch. Create now checks branch_is_open first (an open leaf whose active branch is the name) and refuses before building any artifact, so nothing is stored on refusal, matching `fossil branch new`. The check uses fossil's exact tag types: a propagating branch tag (tagtype=2) and a singleton closed tag (tagtype=1). List now derives a branch's closed state from its newest check-in's closed tag, the row fossil's branch-list query reads, so a branch with an open tip lists open. Unit tests cover the refusal, the no-stored-blob guarantee and reuse of a closed branch's name. check-in: faf47b6220 user: [email protected] tags: main, trunk | |
| 22:59 |
fix(checkout): count only open leaves for forks and fork warnings (#291) (#315)
BranchLeaves joined the leaf table without excluding leaves that carry a closed tag, so a closed leaf still counted toward a branch's leaves, and WouldFork, built on it, reported a fork for a version that was not itself a leaf. This made a closed leaf look like an open one and a non-leaf commit look like it would fork. BranchLeaves now excludes any leaf with a closed tag (tagtype>0), matching fossil's leaf_is_closed_sql, so it returns a branch's open leaves. WouldFork is now fossil's leaf_ambiguity: the current version must itself be an open leaf (is_a_leaf AND not closed) before another open leaf on the same branch can make a commit fork; a non-leaf or closed version returns false. currentBranch now surfaces a real query error instead of masking it as "trunk". Unit tests cover the open-sibling, closed-sibling and non-leaf-parent cases. check-in: f8496d12cc user: [email protected] tags: main, trunk | |
| 20:59 |
test(conformance): sync suite follows the s7.3 uvgimme Read erratum (#313)
The new erratum to draft-fossil-sync-protocol-00 §7.3 has the server answer uvgimme only for a requester with Read, with Clone implying Read, and refuse silently otherwise. fossil 2.28 answers any uvgimme. - The erratum is copied unmodified into conformance/rfcs/erratum and pinned in requirements/sync.tsv, with two new requirements, SP-7.3-11 and SP-7.3-12. - Server cases can carry a fossil note again: a case that expects something fossil(1) does not do says why, and fails once fossil(1) does it. sync.7.3.uvgimme-no-read uses it, so the case flags the day CI's pinned fossil takes the matching patch. - sync.7.3.uvgimme-clone-implies-read finds that go-libfossil's read gates ignore Clone; it is marked as a gap, tracked in #312. check-in: 2cb62c37bc user: [email protected] tags: main, trunk | |
| 13:57 |
test(conformance): sync-protocol conformance suite (#262) (#311)
* test(conformance): sync-protocol conformance suite (#262) Checks go-libfossil against draft-fossil-sync-protocol-00, requirement by requirement: 149 requirements, 199 cases. Server cases send raw requests to Repo.XferHandler and, through `fossil http --in`, to fossil(1) on a copy of the same repository; client cases drive the public Sync and Clone against a scripted peer, with fossil(1)'s client run against the same script for most of them; HTTP cases cover redirects, status codes and media types; interop cases run whole exchanges in both directions. Known failures are marked with the placeholder #0 until their issues are filed, so TestCoverage fails until then. * test(conformance): sync suite review fixes (#262) Cases for the requirements that had none where fossil(1) can show the behavior: server-version, the SHA3 gate, shun admission, cookie and cleartext logins, pushed configuration, ci-unlock, cluster brackets, the clone's unversioned catalog and the pull-only message. Two rows added: link ARG handling and the short uvigot. A fossil(1) disagreement, a harness failure or a deviation other than the one named now fails a case whatever its mark. The server's log output is discarded in the tests. * test(conformance): mark sync gaps with their issues; drop gaps v0.11.1 closed (#262) v0.11.1's read gate and redirect policy make seven sync cases conform, so their gap marks are removed. The rest are marked with the issues that track each fix: #296 and #299-#310. * test(conformance): accept either fossil refusal of a negative clone SEQNO fossil 2.28's macOS build answers clone 3 -1 with clone_seqno 0 and its Linux build, which CI pins, with the invalid-sequence error alone. The draft leaves the reply open; the case now holds go-libfossil only to sending no content. check-in: 80c72b27c7 user: [email protected] tags: main, trunk | |
| 13:32 | chore(release): v0.11.1 changelog (#298) leaf check-in: dbe626ea5d user: [email protected] tags: v0.11.1, trunk | |
| 13:28 |
fix(sync): send content only to readers; never follow HTTPS to HTTP (security) (#297)
* fix(sync): send content only to requesters allowed to read it The sync server answered `gimme HASH` with the artifact whether or not a pull card had been accepted, and answered `pragma uv-hash`, `uvgimme` and `uvigot` with the unversioned catalog and files for any requester, anonymous ones included. Match fossil's page_xfer: - gimme is answered only when a pull card was accepted, or in a clone, which fossil also treats as pulling. - pragma uv-hash sends the catalog only to a requester with Read (o, i or Admin). uv-push-ok goes only to a requester with y; other readers get uv-pull-only, as fossil sends. - uvgimme and uvigot send nothing to a requester without Read, and uvigot adopts a client's mtime only for a requester with y. - Table sync follows the same rules: xgimme needs an accepted pull and pragma xtable-hash needs Read. - pragma ci-lock takes or reports a lock only for a requester with Write, so an anonymous requester cannot learn who holds one. Pragma handling moves into handlePragma to keep handleControlCard short. GHSA-mvx3-6793-8x4r * docs(changelog): note that gating ci-lock on Write departs from fossil GHSA-mvx3-6793-8x4r * fix(sync): follow HTTP redirects as fossil does, never HTTPS to HTTP Both HTTP transports used net/http's default redirect policy. It follows a redirect from HTTPS to plain HTTP, so a 307 or 308 resent the request, login card included, in cleartext. It also turns a 301 or 302 into a GET without the body and stops after 10 redirects, neither of which fossil does. PostXfer now follows redirects the way fossil's http_exchange (src/http.c) and draft §4 describe. It replaces the client's redirect policy for these requests, then: - refuses a redirect from HTTPS to HTTP, and to any scheme but http(s); - re-POSTs the same body and Content-Type for 301, 302, 307 and 308; - strips one trailing /xfer from the new location, as fossil's loop does (the draft says "repeatedly"; the code strips once); - fails on the twentieth redirect (MAX_REDIRECTS 20). HTTPTransport and NewHTTPTransport both use it, including with a client passed through WithHTTPClient. GHSA-3f8x-8fr5-mr98 check-in: 3a1014cf60 user: [email protected] tags: trunk | |
| 02:13 |
test(conformance): repository-state conformance suite (#262) (#295)
* test(conformance): repository-state conformance suite (#262) Check go-libfossil against draft-fossil-repo-state-00, requirement by requirement. Each derived-state case is a scripted sequence of artifacts loaded into go-libfossil and into fossil(1); the tables derived from it are compared row by row, joined by artifact hash. Every case runs through manifest.Crosslink, as one sweep and one artifact at a time, and most also run as a ".rebuild" case through verify.Rebuild. fossil's rows must equal each expectation on every run, and each case states what ignoring its requirement would change, so a case cannot pass or fail by accident. API cases cover name resolution, leaves, the ambiguity test, branch creation and listing, and tagging, each compared with fossil's answer on the same repository. TestRepoStateCorpus compares verify.Rebuild and a Crosslink sweep with fossil rebuild on real repositories named by CONFORMANCE_REPOS. Known failures are marked with the placeholder Gap #0 until their issues are filed, so TestCoverage refuses them for now. * test(conformance): repository-state suite review fixes (#262) - Unsorted T-card cases use a non-hex name, so the ordering rule, not the hex-name rule, is what rejects them. - RS-4.2-7 crosslinks every artifact a second time over the rows the first left (repostate.4.2.relink-unchanged), instead of a sweep that links nothing. - RS-7.3-2, -3 and -4 are checked through manifest.Checkin and branch.Create, which maintain leaves one check-in at a time; a crosslink sweep recomputes the whole table. - retag-branch is built so the retagged check-in's own leaf status changes; branch membership is read through BranchLeaves, and the tagxref case it replaced is reattributed to RS-5.4-5; bad-prefix also compares tagxref. - parentless-untagged follows the draft and marks go-libfossil's crosslink, which follows fossil(1), as a deviation; the unused draft field becomes fossil, checked against fossil's rows. - Every fossil(1) command, in all suites, runs with HOME, FOSSIL_HOME and XDG_CONFIG_HOME in the test's directory, so it no longer records checkouts in the user's global configuration. * test(conformance): mark repository-state gaps with their issues (#262) Replace the placeholder gap marks with the issues that track each fix: #278 and #285-#294. check-in: 7fa0b9b5da user: [email protected] tags: trunk | |
| 01:38 | chore(release): v0.11.0 changelog (#283) check-in: 59df292529 user: [email protected] tags: trunk, v0.11.0 | |
| 01:34 |
fix: harden sync and repository defaults (security) (#282)
* fix(xfer): reject non-decimal card sizes and bound payload memory A payload card's size was parsed with strconv.Atoi, so a peer sending `file HASH -1` reached readPayload's negative-size assertion and panicked the client or server decoding the message, and a signed size such as `+5` was accepted. Sizes now go through parseSize, which follows the sync draft's `decimal = 1*DIGIT` grammar and fossil's 32-bit limit and returns a decoding error for anything else. uvfile FLAGS use the same parser, so negative FLAGS are rejected as the draft requires. The assertion stays, now unreachable from peer input. readPayload no longer allocates the declared size up front: its buffer grows with the bytes read, so a large size over a short message fails after allocating about what was sent. A cfile payload may inflate only to its USIZE (a margin above it for a delta) and not past its 4-byte size prefix, and all cfile payloads in one message share a MaxCFileInflateBytes (1 GiB) budget, so a few megabytes of zlib cannot inflate to tens of gigabytes. FuzzDecode exercises the decoder over both framings. * fix(sync): refuse malformed xrow payloads and cap HTTP reply bodies An xrow payload or xdelete PKData from a peer was decoded into a map and used as is. JSON null gave a nil map that reached coerceJSONNumbers' assertion, and a primary-key value of the wrong type or a nested value reached normalizeValue's, panicking the client or server. Both directions now decode through decodeRowJSON: one JSON object, nothing after it, and repo.ValidateRow's rules, which require scalar values and primary-key values that PKHash can normalize. A client refuses an xrow for a table it has not registered. Column types match case-insensitively, as table creation accepts them. Both HTTP transports read replies through xfer.ReadBody, which stops at MaxBodyBytes (MaxDecompressedBytes plus framing headroom) and returns ErrBodyTooLarge, untagged for retry, instead of reading an endless body into memory. FuzzTableSyncPayload drives the server handler and client over arbitrary payloads. * fix(sync): check synced-table rows and identifiers before building SQL repo.UpsertXRow wrote a row's map keys into its INSERT as column names, and the rows of an xrow payload or xdelete PKData come from the peer, so a key such as "val, mtime) VALUES(...) --" chose the statement's columns and values, and a key could select from other tables into a synced one. This held on the server for pushed rows and tombstones, and on the client for a server's rows. A peer's row must now use exactly the table's declared columns: repo.ValidateRow refuses any other key, which also keeps a peer from setting _owner. UpsertXRow no longer trusts its caller: it reads the table's registered schema and refuses any key it does not declare, apart from _owner on an ownership-mode table. ListXRows, LookupXRowOwner and DeleteXRowByPKHash validate the table name and the column names they interpolate, so no synced-table identifier reaches SQL text unchecked. * fix(sync): answer reqconfig only with fossil's transferable config The sync server answered reqconfig NAME with any row of the config table, for any requester, including one with no capabilities, so project-code, server-code and every local setting could be read by anyone who could reach the sync endpoint. handleReqConfig now follows fossil's page_xfer and configure.c. It answers only a /GROUP name, only for a requester with Read, matches the group by prefix in fossil's table order, and sends only the names fossil transfers in that group, in fossil's encoding. The user and subscriber tables also need Admin, which counts only for a verified login, and the concealed table needs RdAddr. A refused request gets no cards and no error. The groups, their names and their capability rules live in one place, config_groups.go. A differential test answers the same requests with fossil's own server and with HandleSync on the same repository and requires identical config cards. * test(sync): keep fossil http from leaving a backoffice process * fix(sync): check the login nonce against the request body The server verified a login card's signature over its nonce but never checked that the nonce was the SHA-1 of the request body after the login line, as fossil's check_tail_hash does (draft-fossil-sync-protocol-00 §6.2). A login card captured from one request therefore authenticated any other request body. The decoder now records, for each login card, the SHA-1 of the decompressed bytes after the card's line, and VerifyLogin refuses a card whose nonce does not match it. A login card that was never decoded has no tail and is refused. Login handling otherwise follows fossil's page_xfer and check_login: - a refused login ends the exchange with "error login failed" instead of continuing as nobody; - a second login card is refused with "multiple login cards"; - a successful login adds the user's capabilities to nobody's instead of replacing them; - nobody and anonymous log in without a password, and the reserved developer and reader logins and accounts with an empty password cannot be logged into. Fixes GHSA-2g5p-q5f3-477p. * fix(repo): seed nobody with fossil's default capabilities A new repository seeded the nobody user with "cghijknorswy", so every request without a login card could push check-ins and unversioned files, write wiki pages and tickets, and held Setup. Seed the four built-in users `fossil new` creates, with fossil's capabilities: nobody "gjorz", anonymous "hz", developer "ei" and reader "kptw". Setup and Admin now include check-in, read and clone, as they do in fossil, so the creating user can sync once given a password. A login to an account with no stored password is refused, as fossil's check_login refuses it; the creator starts with none, and the signature would otherwise be the SHA-1 of the nonce alone. Tests that push without credentials grant nobody push explicitly. Existing repositories keep their stored capabilities. * test(auth): reserved-login test updates the seeded developer and reader rows New repositories now seed developer and reader as fossil does, so the test gives the existing rows a password instead of inserting them. * docs(changelog): group the sync hardening fixes under Security * test(sync): reqconfig tests sign their logins over the request body The login nonce now has to cover the request body, so the reqconfig tests build their admin requests with signedRequest instead of a bare login card. check-in: 3a6126c3e9 user: [email protected] tags: trunk | |
| 01:15 |
fix(sync): a pull-only sync converges when the server lacks the client's artifacts (#277) (#281)
The server answered a pull-only client's igot cards with gimme for artifacts it lacked. The client was not pushing, so it never sent them and kept going round after round until it hit the round cap. Fossil's page_xfer handles igot only when the client sent a push card, and does the same here: a pull-only igot just records what the client has. The client also treated the server's gimme cards as outstanding work even when it was not pushing, so any server that did ask (an older libfossil, or another implementation) could hold a pull open. A session without push now ignores gimme cards. Two handler tests relied on a pull card alone producing a gimme; they now send a push card. check-in: 1e3e2df2f2 user: [email protected] tags: trunk | |
| 01:07 |
test(conformance): delta-format conformance suite (#262) (#280)
* test(conformance): delta-format conformance suite (#262) Check internal/delta against draft-fossil-delta-format-00, requirement by requirement: 39 requirements, 88 cases. Decoder cases (Apply and OutputSize) are byte vectors, each also given to fossil(1) through its SQL delta functions; fossil's verdict must match unless the case records why it follows the draft instead (fossil masks the high bit, does not verify the checksum, and returns the size as a signed int). Each rejection breaks one rule and keeps the trailer valid, so check order cannot hide the rule under test. Producer cases check Create against a strict oracle and against fossil's delta_apply, and fossil's delta_create against Apply. FuzzDeltaApply compares Apply with an independent model decoder; FuzzDeltaRoundTrip checks Apply(Create(s, t)) == t. DF-3.1-6 (Create has no 2^32 bound) has no case: it needs a 4 GiB input. * test(conformance): delta suite names the reason each rejection must give A rejection case could pass because some other rule rejected the input. Every rejection case now names a substring of go-libfossil's error (or ErrChecksum), and the harness fails the case when the message differs. Vectors that were rejected for the wrong reason are fixed or reattributed: copy-truncated (DF-4-2), reject-10-nul (non-empty count, so the NUL is the opcode), copy-overflow-64 (DF-4-11), and high-bit-count, which now tells a masking decoder from a conforming one (fossil accepts it). The phantom-base case uses a literal-only delta; the content-discard case checks ErrChecksum and the store-side header refusals move to DF-4.1-3. The allocation bound gets its own row (DF-6-5) and takes the least of five runs. DF-3.1-6 notes issue #279. Drops a duplicate size-query case, the sizeCase helper and the decorative fuzz guard bytes. check-in: 0629de2beb user: [email protected] tags: trunk | |
| 00:42 |
test(conformance): artifact-format conformance suite (#263) (#276)
* test(conformance): artifact-format conformance suite (#263) Adds the suite for draft-fossil-artifact-format-00 with errata 4.1.2, 4.2 and 4.6.4, so the parser, writer, naming, hash policy and delta manifest code are checked requirement by requirement instead of by scattered unit tests that each encode one reading of fossil. - requirements/artifact.tsv: 101 requirements from Sections 3 to 8, pinned to the draft and the three errata. - Parse cases are a known-good base artifact per type plus one edit, with the Z card recomputed, so the edit alone explains a rejection. Every case also runs through fossil test-parse-manifest; fossil 2.28 agrees with all of them. Card legality, duplicates, arity and hash positions are generated from the bases and the draft's own tables. - Accepted inputs must re-encode byte for byte unless marked non-canonical. - API cases cover naming, verification, the comparator, the writer, the R card, the hash policy and delta-manifest enumeration. - FuzzArtifactRoundTrip checks parse/marshal/parse stability, and TestArtifactCorpus (CONFORMANCE_REPOS) parses every artifact fossil crosslinked in real repositories. 267 cases go-libfossil fails are marked with a "#0" placeholder in artifact_gaps_test.go, grouped by the code at fault, for issues to be filed against. * test(conformance): tighten the artifact suite after review (#263) A rejection only counts when the rule the case names makes it, so every rejection case that is not about the Z card must now pass deck.VerifyZ first. Inputs go-libfossil rejected only through the Z check (framing, residue, sterilizer, unstripped wrappers, an extra Z token) become gaps, along with VerifyZ itself, which rejects an absent Z card and reads the last byte where Section 6.2 says it must not. - Cases can name the substring fossil's rejection reason must contain; the unknown-letter cases sit in sorted position, the sterilizer cases are filed under card order, which is what fossil rejects them for, and generated legality cases take their requirement from the draft's table. - The ratchet that never persists the hash policy is a gap: fossil opening the repository still reads auto. - New rows for 4.7.9, 6.2 (absent Z, last byte), 6.3.4 and 7.1. - The audit refuses the #0 placeholder, a test keeps the gap map and the fuzz test's forgiven losses tied to live gap cases, and one Z-card helper replaces two. * test(conformance): mark artifact gaps with their issues (#263) Replace the placeholder gap marks with the issues that track each fix (#265-#275), and keep coverage_test.go within 100 columns. check-in: d52a6fe050 user: [email protected] tags: trunk | |
|
2026-10-10
| ||
| 23:52 |
test(conformance): harness for an RFC conformance suite (#262) (#264)
Adds the conformance package that the per-draft suites build on. Nothing in the repository has yet checked go-libfossil against the Fossil drafts requirement by requirement. - A Case names its requirement. A Gap (an issue) or Deviation (a reason) marks a known difference. A marked case still runs: while it fails it is skipped, and once it conforms it fails until the mark is removed, so the list of known failures cannot go stale. - Requirements tables pin the SHA-256 of the drafts and errata they were written against. Those are copied unmodified into conformance/rfcs by scripts/refresh-conformance-rfcs.sh. A changed draft fails the coverage test until its table has been re-read and its pin moved. - Coverage is derived from the cases rather than recorded: - COVERED: every case conforms. - PARTIAL: the table's row says what its cases leave out. - DEVIATION: a case is marked as a deliberate difference. - FAILING: a case is marked with an open gap. - GAP: no case yet. CONFORMANCE_REPORT=<path> writes the requirement-by-requirement report as Markdown. - fossil(1) is run on every case's input, through test-parse-manifest, so a case's expectation is checked against fossil each time the suite runs. withZ appends the Z card, so a case that edits one card never also breaks the checksum. The harness's own rules are tested in harness_self_test.go. No suites are registered yet; the artifact-format suite is next (#263). check-in: 57a558fcbf user: [email protected] tags: trunk | |
|
2026-10-09
| ||
| 15:35 |
Store check-in, link and tag rows as fossil does (#258) (#261)
* fix(manifest): store check-in, link and tag rows as fossil does (#258) A rebuilt or locally written repository differed from fossil's in three derived values: event and plink times in their last digits, event.omtime left NULL, and a valueless tag stored as '' instead of NULL. Fossil stores a tag time as SQLite's julianday() bound as a double, but writes event, plink, attachment and forumpost times into SQL text with its own printf("%.17g"), which emits 16 digits in long double arithmetic. TimeToJulian is now julianday() exactly (whole milliseconds over 86400000) and TimeToTextJulian ports fossil's printf conversion, so both match fossil bit for bit. The conversion follows builds where long double is a double (macOS, Windows); fossil itself differs across platforms in the last digit. Check-in events record omtime, tag writes bind NULL for an empty value, the readers of event and plink times under the ncruces driver rebuild the text form, and the CLI's tag listing reads NULL values. * test(db): sweep both stored julian forms; scan the text form from driver times (#258) * test(verify): compare text times bit for bit only where fossil computes them in double (#258) check-in: 2c2e5e74b6 user: [email protected] tags: trunk | |
| 15:15 |
Clone and sync exchanges follow the sync draft (#255) (#260)
* fix(sync): clone and sync exchanges follow the sync draft (#255) * docs(changelog): clone and sync exchanges follow the sync draft (#255) * fix(sync): fail unfinished clones; ask for held phantoms once (#255 review) check-in: e46a6c3f60 user: [email protected] tags: trunk | |
| 14:47 |
fix(annotate): credit lines the way fossil annotate does (#256, #257) (#259)
Annotate aligned each version with its parent using its own LCS, so where a line repeats (blank lines, braces) it could pick a different copy as the new one than fossil does, crediting the line to another check-in. It also trimmed every trailing newline, dropping trailing blank lines. Annotate now uses a port of fossil's line diff (break_into_lines, diff_all, longestCommonSequence and the rest of diff.c that annotation uses), hash included, since the hash orders the candidates and so decides ties. Like fossil's annotate, it diffs each older version against the starting one and credits a line to the version just newer than the first ancestor that lacks it. This also drops the table the size of both versions that the old LCS allocated at every step. check-in: ada7aadbb5 user: [email protected] tags: trunk | |
| 14:25 |
fix(annotate): credit lines through the start-version mapping (#253) (#254)
Past the first parent, walkParentChain wrote each line's attribution by its index in an older version, so lines were credited to the wrong check-in and an older version longer than the start panicked. Carry each compared line's start-version index through the walk and stop following lines that no longer match. check-in: a63adf1dcf user: [email protected] tags: trunk | |
|
2026-10-06
| ||
| 15:57 |
fix(checkout): merge the way fossil does, and commit merges with their parent (#244) (#252)
The CLI's merge marked a conflict with vfile.chnged=5, which fossil reads as "added by integrate", and recorded no merge-in version or vmerge row, so a merge went unseen and the next commit dropped the merge parent. Merging now lives in the library as Checkout.Merge and records what fossil records: chnged=2 and mrid for a file taken from the merged-in version, chnged=3 for an added file, a per-file vmerge row for a merged file, and vmerge id 0 for the merge. Conflicts use fossil's markers and are found by looking for them (Checkout.Conflicts), so fossil and go-libfossil read each other's merges. Where fossil relies on its undo copy, Merge keeps the file or refuses instead. Commit reads vmerge for merge parents, and, as fossil's commit does, refuses unresolved conflict markers, empty commits, unknown names and partial commits of a merge. The CLI's merge, conflicts, mark-resolved and ci commands call the library; ci now commits the checkout. check-in: 7ea6fba63f user: [email protected] tags: trunk | |
|
2026-10-03
| ||
| 19:00 |
ci(release): drop the downstream dispatch to EdgeSync (#251)
The release workflow's last step sent EdgeSync an upstream-bump event with a personal access token. It has failed with 401 on every release since v0.8.1 because the token expired, marking each release run failed after the release itself was published. EdgeSync also stopped accepting the event when the repository was renamed. EdgeSync now tracks go-libfossil with Dependabot (danmestas/EdgeSync #199), which needs no token here, so the step is removed. A release run now succeeds once the GitHub release exists. check-in: af8e531e68 user: [email protected] tags: trunk | |
| 17:47 | chore(release): v0.10.0 changelog (#250) check-in: e241048f77 user: [email protected] tags: trunk, v0.10.0 | |
| 17:38 |
fix(checkout): revert undoes a pending rename (#242) (#249)
* fix(checkout): revert undoes a pending rename (#242) Revert-all selected rows by content and removal, so a rename recorded by fossil mv (origname set, chnged 0) was skipped; a renamed and edited file had its committed content written under the new name, leaving the rename pending. A named revert only matched the current name. Like fossil's revert, a pending rename is now undone: the file under the new name is removed, the row takes its old name back (UPDATE OR REPLACE, as fossil does), and the committed content is written there. Revert-all includes renamed rows, and a named revert answers to either the new or the old name. A renamed file a merge added is still removed outright, as before and as in fossil. removeRegularFile is shared by the rename undo and removeMergeAdded, so neither deletes a directory the user made at that path. * fix(checkout): plan a revert before changing anything (#242) Undoing a rename with UPDATE OR REPLACE let a swap destroy files: reverting a.txt and b.txt exchanged through fossil mv deleted the other row, the loop then removed a file it had just restored from its stale row list, and the next restore failed half way, leaving both files gone from disk. A new file added under the old name was overwritten the same way. A named revert of the old name also stopped at the first match. Revert now plans, checks, then acts. planRevert decides every target row's action (restore, un-manage, remove a merge-added file, undo a rename) before anything changes; checkRenameUndoFree refuses the whole revert when undoing a rename would land on a name another tracked row holds, since go-libfossil has no undo copy to recover an overwritten file (fossil saves one); revertFile carries out the planned action. The rename undo is a plain UPDATE, which fails rather than replace a row. A name selects every row it matches, new name or old. The callback reports a rename under the name the file gets back. Swaps and chains, which fossil completes via its undo store, are #248. * fix(checkout): revert named paths as one; refuse untracked file at old name (#242) Review of the planned revert found two remaining ways to lose work: - Several named paths were reverted one at a time, so in `revert b.txt z.txt` the edit to b.txt was discarded before z.txt's rename undo was refused. revertPaths now gathers every named row (new or old name) and plans and checks them as one revert: a refusal reverts none of them. - Undoing a rename restored the committed content over an untracked file at the old name. checkRenameUndoFree now also refuses when a file on disk there differs from the committed content (wouldClobber), for the same reason it refuses a tracked one: there is no undo copy. * docs(changelog): note revert's untracked-file and multi-path rules (#242) check-in: 50d1a7f72b user: [email protected] tags: trunk | |
| 17:27 |
fix(content): report database faults from the availability check (#239) (#247)
* fix(content): report database faults from the availability check (#239) IsAvailable and AvailableByUUID return a bool, so a busy, closed or I/O-failed database read as "content missing". Where missing content is tolerated on purpose, that swallowed the fault: since #231, CreateCheckout leaves vfile empty for a partly synced tip, and a fault during resolveFiles' availability check took the same path, so Create succeeded with an empty checkout. The chain walk moves to checkAvailable, which returns an error for a fault and a plain false for an unknown rid, a phantom, a cycle or an over-long chain. New CheckAvailableByUUID exposes that distinction, and resolveFiles uses it, so a fault fails LoadVFile with its own error instead of errMissingContent. IsAvailable and AvailableByUUID keep their signatures and their fail-closed reading of a fault; their ~30 callers treat "not available" as a failure already. * docs(changelog): note the availability fault fix (#239) * fix(content): NULL size is missing content; don't cache faults (#239) checkAvailable scanned blob.size into an int64, so a NULL size (legal in the schema) became a hard error in resolveFiles where it used to read as missing content. Both walks now read a NULL size as a phantom. AvailabilityCache memoized a walk's verdict for every node it visited, including a false that came from a database fault, so one transient fault stayed wrong for the rest of the crosslink sweep. A faulted walk now returns false without being remembered; an unknown rid is still a verdict and is cached as before. check-in: 4b14509374 user: [email protected] tags: trunk | |
| 17:22 |
fix(checkout): dry runs change nothing; switching ends a merge (#236) (#246)
* fix(checkout): dry runs change nothing; switching ends a merge (#236) A dry-run Extract still called LoadVFile(rid, clear=true) and finalizeExtract, replacing the current version's vfile rows (pending adds, renames, merge state) and moving vvar to the target. A dry-run Update still ran finalizeUpdate, with the same effect. Extract now lists the target's files from its manifest (resolveFiles) instead of reading them back from vfile, so a dry run never loads them; only a real extract replaces vfile and records the version. Update skips finalizeUpdate on a dry run. finalizeExtract also clears vmerge, as fossil's checkout does: a forced switch used to leave a merge record that would give the next commit a bogus merge parent. * fix(checkout): forced extract starts fresh; re-extract keeps a merge (#236) Two gaps against fossil's checkout found in review: - A forced extract dropped only other versions' vfile rows (LoadVFile keeps the target's), so re-forcing the current version left a pending rename's two names on disk and in vfile, and a removed file restored but still marked deleted. fossil checkout --force deletes every row first; replaceFileList now does the same when Force is set. - Extract cleared vmerge on every completed run. fossil's checkout returns early when the version does not change and Force is off, keeping a pending merge; finalizeExtract now ends the merge only for a switch or a forced extract. * refactor(checkout): finalizeExtract reads the prior version itself (#236) Only the final step needs the version being replaced, and nothing changes vvar before it, so finalizeExtract reads it there. A dry run no longer calls Version at all, and Extract drops to 64 lines. check-in: 139d426384 user: [email protected] tags: trunk | |
| 17:09 |
fix(cli): add, rm, rename and revert call the library (#234) (#245)
* fix(cli): add, rm, rename and revert call the library (#234) These commands wrote the checkout's vfile with raw SQL, each with its own copy of rules the library already gets right, and ignored most Exec errors. rename recorded the new name but left the file under the old one, so the next commit failed on a "missing" tracked file. revert only cleared change flags over an edited file and restored nothing. add wrote a SHA1 mhash whatever the repo's hash policy. They now open the checkout through the library (openWorkingCheckout, which finds the repository from the checkout when -R is not given) and call Add, Remove, Rename and Revert, returning every error, including from closing. rename moves the file, like fossil mv --hard; revert restores committed content, like fossil revert. Two library gaps the CLI exposed are fixed where they live, matching fossil's revert: - Revert refreshes change flags from disk before choosing files, so an edit no scan has recorded yet is reverted instead of skipped. - Reverting a never-committed add un-manages it and leaves the file on disk. It used to delete the file, destroying the only copy; fossil prints UNMANAGE and keeps it. revertFile's restore path moves to restoreCommitted, bringing it under 70 lines. The merge, merge-resolve and conflicts commands are #244. * docs(changelog): note the CLI and Revert changes (#234) * fix(checkout,cli): revert and rename match fossil; find repo without -R (#234) Review of the CLI switch found the library and the new helper falling short of fossil: - Without -R, every command failed on a fresh fossil checkout: the repository lookup opened the checkout database read-only, which clashes with the WAL pragma on a database fossil left in delete journal mode, and hid the real error. It now opens it as the library does, reports the error, and resolves a relative repository path, as fossil open stores it, against the checkout's directory. - Rename moved the file onto an existing untracked file, destroying it. It now refuses, as fossil mv --hard does, before changing anything. - Revert skipped a file missing from disk (no change flag records it) while the CLI reported it reverted. A named file is now reverted whatever its flags say, and revert-all includes missing files. - Revert left merge state behind: restored rows kept mrid on the merged-in version and vmerge kept its row, so the next scan saw the file changed and the next commit recorded a merge parent. Restored rows now reset mrid and mhash, and revert-all clears vmerge, as fossil's revert does. Rename's vfile lookups move to renameRowID and revert-all's selection to needsRevert, keeping both functions under 70 lines. The re-add test now checks the vfile row itself, since fossil's own scan would hide a stray chnged=1. * fix(checkout): revert removes a file a merge added, as fossil does (#234) Revert decided between restore and un-manage by rid alone. A file a merge added has a non-zero rid (its merged-in artifact) but is not in the checked-out version, so revert restored it as if committed and left it tracked; the next commit then included it. Fossil looks the file up in the version's manifest and, when it is absent and rid is not 0, deletes the file and its row (its content lives on in the merged-in version). revertFile now takes the vfile row and the version's path set and does the same: a pending add is un-managed and kept on disk; a file the version lacks, under its name or its name before a pending rename, is removed; anything else gets its committed content back. Checking the pre-rename name keeps a renamed, edited file from being deleted. Rename's refusal message now fits a directory at the destination too. * fix(checkout): never remove a non-file when reverting a merge add (#234) removeMergeAdded deletes only a regular file at the merge-added path; anything else there, such as a directory the user made, is left alone. Revert's doc now says edits to reverted files are discarded: fossil saves an undo copy first, go-libfossil has no undo yet. check-in: de8f3ed302 user: [email protected] tags: trunk | |
| 16:47 |
refactor(vfile): give the checkout's vfile encoding one owner (#233) (#243)
* refactor(vfile): give the checkout's vfile encoding one owner (#233) Checkin, stash and status each decoded fossil's vfile columns on their own: rid=0 for an add, origname==pathname on unrenamed rows, chnged merge codes. That is how #228 happened, and stash still had it wrong: it selected chnged=1 only, so files changed by a merge (chnged 2..9) were silently left out, and it stashed a rename as a plain edit. New package internal/vfile owns the encoding. Load reads a version's rows; Row answers what callers ask (IsAdded, IsRemoved, RenamedFrom, ContentChanged, PendingMerge); Classify gives the change type in fossil's order. checkout.FileChange is now an alias of vfile.Change, so the checkout API is unchanged. internal/checkout's status, Extract safety check and checkin read rows through it. The never-read vfileCommitEntry.changed field is gone. internal/stash selects changes with vfile.Classify against the checked- out version and refuses a pending merge or rename by name, since its save and revert paths restore files by current name against the committed version and would drop either. Its store-and-revert loop is split into stashRecord and revertStashedFile; removed and modified files share one revert path, keyed by row id. * fix(vfile): a pending merge is chnged 2-5 or mrid != rid (#233) PendingMerge counted every chnged above 1 as a merge, but 6 through 9 record a change to the executable or symlink bit; fossil leaves chnged=6 after a plain chmod +x. Stash then refused an ordinary permission change as "a pending merge". It also ignored mrid: an edit made on top of a merge drops chnged back to 1 while mrid still names the merged-in content, so the refusal could be bypassed and the merge half-dropped. Row now carries mrid; PendingMerge is chnged 2..5 or mrid != rid, and ModeChanged covers 6..9. Stash refuses a permission change with that reason, since its revert writes default permissions. Load reads a NULL rid as 0 and a NULL mrid as rid, the tolerance the checkin code had when it scanned rid into a NullInt64. * refactor(checkout): ask vfile.Row about merges in Extract's check (#233) Extract's safety check relaxed only rows with chnged == 1, decoding the column itself. It now asks the row (not a pending merge, not a mode change), keeping fossil's encoding in internal/vfile. Equivalent for every real row. nextChangedState's comment no longer calls the mode changes (6..9) merge states. check-in: f55d0ed2de user: [email protected] tags: trunk | |
| 16:28 |
fix(checkout): classify changes in one place; report missing files (#232) (#241)
* fix(checkout): classify changes in one place; report missing files (#232) HasChanges, VisitChanges and Extract's safety check each decided what counts as a change: two SQL WHERE clauses and classifyChange, with an error path whose only job was catching their disagreement. They had already drifted once (HasChanges missed renames until #229). Now one reader, loadVFileRows, returns every row of a version and one classifier, classifyChange, judges it, in the order fossil's changes command uses: removed > missing > added > renamed > modified. A renamed file that is also edited stays "renamed", so callers that see only the change type do not lose the rename. The WHERE clauses and the malformed-row error are gone. VisitChanges with a scan now reports a tracked file that is gone from disk as ChangeMissing, which the public API already exposed as "missing" but nothing produced; such files were left out, or shown as added or modified. HasChanges sees a fossil add (rid=0, chnged=0) without a scan, since rid=0 now always means added (#231). * fix(checkout): treat a non-file at a tracked path as missing (#232) ScanChanges read each tracked path and failed the whole scan when a directory stood where a file belongs. Fossil's scan reports that path as NOT_A_FILE under its missing filter. The scan now stats first and treats anything that is not a regular file as missing, so Status reports it instead of erroring. loadVFileRows orders rows by pathname, as fossil's changes lists them. The changelog notes that checkouts written before #231 may hold rid=0 placeholder rows, which HasChanges now counts. check-in: ff9c1af49d user: [email protected] tags: trunk | |
| 16:16 |
fix(checkout): refuse to load a version with missing content (#231) (#240)
* fix(checkout): refuse to load a version with missing content (#231) LoadVFile wrote a vfile row with rid=0 for each file whose content the repo does not hold (a phantom, or a delta on one), as a partial sync leaves it. In fossil's vfile rid=0 means "added", so those rows were indistinguishable from pending adds, and Extract then panicked in content.Expand(0) trying to write them. LoadVFile now does what fossil's checkout does: it resolves every file first and refuses the version, naming up to ten missing files and wrapping errMissingContent, before changing vfile at all. It no longer returns a missing count, which no caller used. Create is the one caller that tolerates the refusal. A tip that a partial sync has not finished is no reason to fail CreateCheckout: the caller may be about to extract an older, complete version (EdgeSync's ExtractTo). vfile then stays empty instead of claiming files the repo cannot produce. Create's environment defaulting, shared with Open, moves to resolveEnv, and the initial load to loadInitialVFile. Tests cover the refusal (vfile untouched, files named), Extract leaving the checkout as it was, Create on a partial tip followed by Extract of a complete version, and parity with the fossil binary, which refuses the same repository with "missing content". * fix(checkout): refuse missing content in Update before writing (#231) Update wrote changed files to disk and only then reloaded vfile, which now refuses a version with missing content. A target sharing a missing file with the current version (its content is never read, because it does not change) could therefore leave disk on the target while vvar and vfile stayed on the old version. Like fossil's update, Update now resolves the target's content before writing anything, through resolveFiles, the resolution step split out of LoadVFile. Update moves its target selection into updateTarget, reuses Extract's checkinTime, and builds its result through sortedUpdateResult, bringing it under 70 lines. Create now reports a tip it could not load to the observer instead of tolerating it silently. * fix(checkout): report Update refusals to observers; tighten checks (#231) The missing-content check in Update moves after ExtractStarted, so a refused update still reaches observers as an ExtractCompleted carrying the error, as other Update failures do. A negative target from the public UpdateOpts is an error, not a panic. resolveFiles errors carry their own prefix instead of LoadVFile's. Adds the entry and exit assertions the new helpers were missing, a compile-time check on missingNamesMax, and wraps lines over 100 columns; tests check Version's error instead of discarding it. check-in: a317896683 user: [email protected] tags: trunk | |