Artifact 8408d875a7f8338186f24b8d2649594b2ffb96616d3d1ce1208d2a6e00e1e4e7:
- File
internal/manifest/leaf.go
— part of check-in
[600861484f]
at
2026-10-13 15:55:30
on branch main
— test(conformance): re-pin the -01 drafts with the 2026-10-13 errata (#430)
* test(conformance): re-pin the -01 drafts with the 2026-10-13 errata
Re-pin the repo-state and sync-protocol drafts to zit check-in 5a1b741b25d8, which applies five more -01 errata: repo-state §5.3, §7.3 and §8.3, sync-protocol §9.4/§9.5 and §10.1. zit now keeps each -01 text under the -00 file name; the copies keep their -01 names and conformance/rfcs/README.md records the zit path and check-in. The new errata are pinned, and the requirement tables are re-read against every changed hunk.
The §7.3 and §8.3 cases that were deviations now pass as written, and new cases cover a date tag's arrival order (§5.3), untagged leaves and a merge parent's recheck (§7.3).
The sync server answers each guard-passing ci-lock with pragma avoid-delta-manifests while forbid-delta-manifests is on, defaulting to on as the draft and fossil 2.29 do (fossil 2.28 defaults it off). The client leaves its transfer unchanged on that pragma. A file or cfile whose content does not match its hash now fails the client's round, in a pull and a clone: the reply rolls back, prefix included, and no further round starts.
The leaf table is still recomputed per crosslink batch, so a parent whose untagged child arrives in a later batch loses the verdict §7.3 keeps; that case is marked as a gap, tracked in #426.
* fix(sync): continue after a uvfile content error; renumber the avoid-delta-manifests row
A uvfile whose content does not match its HASH is a handler-reported content error: warn, commit the prefix and follow ordinary continuation (sync-protocol-01 10.1). The avoid-delta-manifests client row is SP-9.5-7 now that share-links took SP-9.5-5 and SP-9.5-6. Older unreleased changelog entries drop the pending-erratum wording. (user: [email protected], size: 7205) [annotate] [blame] [check-ins using]
package manifest import ( "database/sql" "errors" "fmt" "github.com/danmestas/go-libfossil/db" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // Leaf maintenance, in one place so the sweep and the local commit path cannot // drift apart on what a leaf is. // // Canonical fossil's rule (src/leaf.c, leaf_check/is_a_leaf/leaf_rebuild): a // check-in stops being a leaf only once a child of it carries the *same* branch // name, where a check-in with no active branch tag reads as "trunk" (here, the // main-branch setting, see mainBranchSQL): // // SELECT 1 FROM plink // WHERE pid=:rid // AND coalesce((SELECT value FROM tagxref // WHERE tagid=TAG_BRANCH AND rid=plink.pid // AND tagtype>0),'trunk') // == coalesce((SELECT value FROM tagxref // WHERE tagid=TAG_BRANCH AND rid=plink.cid // AND tagtype>0),'trunk') // // Only an active branch row (tagtype > 0) names a branch. fossil's is_a_leaf // and leaf_rebuild filter so, but its leaf_check, which maintains the table, // does not: a check-in whose branch tag was cancelled with a value keeps that // value there. draft-fossil-repo-state-01 Section 6.4 requires the filter on // every path that decides branch membership, so both paths here apply it. // // Two consequences are easy to get wrong and are what issue #189 was: // // - The merge parent of a check-in on another branch stays a leaf. A branch // merged back into trunk keeps its tip in the leaf table -- that class was // 939 of the 1,426 leaves the real Fossil repository has. // - The plink row's isprim flag is not consulted at all. A branch point stays // a leaf even though its only child is its *primary* child, because that // child is on a different branch. // // The branch comparison reads tagxref, so both entry points must run after the // branch tag of every check-in involved is in tagxref -- including the ones a // check-in inherits by propagation rather than declaring itself. // // branchTagID is the tagid db/schema.go seeds for 'branch', which is fossil's // own TAG_BRANCH; both schemas fix it, so it is a constant here rather than a // lookup per row of a whole-repository recompute. const branchTagID = 8 // mainBranchSQL is the default branch a check-in with no active branch row // reads as: the main-branch setting, or trunk when it is unset. Section 6.1 // forbids assuming "trunk" when the setting is present. fossil's leaf.c uses // the literal 'trunk' here; the -01 text is followed, as for repo.MainBranch, // which also reads an empty value as unset. const mainBranchSQL = `coalesce(nullif((SELECT value FROM config WHERE name='main-branch'),''),'trunk')` // sameBranchSQL is true for a plink row whose child is on its parent's branch, // each read through its active branch row only. var sameBranchSQL = fmt.Sprintf(` coalesce((SELECT value FROM tagxref WHERE tagid=%[1]d AND rid=plink.pid AND tagtype>0),%[2]s) = coalesce((SELECT value FROM tagxref WHERE tagid=%[1]d AND rid=plink.cid AND tagtype>0),%[2]s)`, branchTagID, mainBranchSQL) // leafCheck brings rid's leaf row in line with the rule above, inserting or // deleting as needed. It is fossil's leaf_check(), and like fossil's it is // called on a check-in and on each of its parents once the surrounding write is // otherwise complete (leaf_eventually_check + leaf_do_pending_checks). func leafCheck(q db.Querier, rid libfossil.FslID) error { if q == nil { panic("manifest.leafCheck: q must not be nil") } if rid <= 0 { panic("manifest.leafCheck: rid must be positive") } var one int err := q.QueryRow(` SELECT 1 FROM plink WHERE plink.pid = ? AND `+sameBranchSQL+` LIMIT 1`, rid).Scan(&one) switch { case err == nil: if _, err := q.Exec("DELETE FROM leaf WHERE rid=?", rid); err != nil { return fmt.Errorf("leafCheck delete rid=%d: %w", rid, err) } return nil case errors.Is(err, sql.ErrNoRows): if _, err := q.Exec("INSERT OR IGNORE INTO leaf(rid) VALUES(?)", rid); err != nil { return fmt.Errorf("leafCheck insert rid=%d: %w", rid, err) } return nil default: return fmt.Errorf("leafCheck query rid=%d: %w", rid, err) } } // repairLeafTable recomputes leaf from scratch. Crosslink does not maintain // leaf incrementally as each checkin is linked -- that only produces the right // answer when parents are always linked before their children, which // delta-chain order does not guarantee -- so the whole table is rebuilt once // the sweep's plink edges and propagated branch tags are all in place. // // Which rows fossil's leaf table can contain is decided by which rids fossil // ever runs the rule against, and that is NOT "every check-in". Nothing calls // leaf_rebuild() outside `fossil leaves --recompute`; the table is maintained // by leaf_check(), scheduled by leaf_eventually_check() for a rid and EACH OF // ITS PARENTS whenever a branch tag lands on that rid -- from tag_insert() when // a check-in declares one, and from tag_propagate() when one is inherited // (src/leaf.c, src/tag.c). fossil's candidate set is therefore every rid // carrying a branch tagxref row, plus every rid that is a parent in plink. // // That set never checks a check-in that has a parent but carries no branch // tag, so such a check-in is never a leaf in fossil. draft-fossil-repo-state-01 // Section 7.3, as corrected on 2026-10-13, states the same rule: widening the // set to plink children changed the leaf table of a real fossil-authored // repository (the libfossil corpus holds a tagless check-in with a parent), // which `fossil rebuild` does not reproduce. Recomputing the table per batch // still departs from the section in one way: a parent whose untagged child // arrives in a later batch loses the verdict fossil keeps for it (#426). // // The set still differs from "every check-in" in both directions. A parent whose own // artifact never arrived has no event row at all, yet fossil does list such a // phantom as a leaf when the check-in naming it sits on another branch. In the // other direction, a check-in that neither declares nor inherits a branch tag // and has no ancestry -- `fossil merge --integrate` writes one of these, a // comment and a `T +closed` card and nothing else -- is never checked, and is // absent from fossil's leaf table even though the rule would admit it. Drawing // candidates from event instead put four such rows in a corpus clone that // `fossil rebuild` then removed (issue #193). func repairLeafTable(q db.Querier) error { if q == nil { panic("manifest.repairLeafTable: q must not be nil") } if _, err := q.Exec("DELETE FROM leaf"); err != nil { return fmt.Errorf("repairLeafTable clear: %w", err) } if _, err := q.Exec(fmt.Sprintf(` INSERT INTO leaf(rid) SELECT c.rid FROM ( SELECT rid FROM tagxref WHERE tagid=%d UNION SELECT pid AS rid FROM plink WHERE pid > 0 ) c WHERE NOT EXISTS( SELECT 1 FROM plink WHERE plink.pid = c.rid AND %s) `, branchTagID, sameBranchSQL)); err != nil { return fmt.Errorf("repairLeafTable insert: %w", err) } return nil }