Artifact d60512a3e7a4b7542ac95dd08f119f52d50570ee6c0aec4f30e8fe46afecfc5e:
- File
internal/manifest/leaf_candidates_test.go
— part of check-in
[628f932301]
at
2026-10-13 16:51:51
on branch main
— fix(manifest): maintain the leaf table incrementally (RS-7.3-3) (#434)
* fix(manifest): maintain the leaf table incrementally (RS-7.3-3)
The leaf table was recomputed from scratch at the end of every crosslink batch, so a check-in lost its leaf verdict whenever a later batch linked an untagged child to it. repo-state-01 Section 7.3, as corrected, and fossil keep that verdict: a check-in and its plink parents are rechecked only when a branch tag is written to it or propagation updates or deletes its branch row (src/tag.c:195, :100-102), and the checks run when the batch ends (src/leaf.c leaf_do_pending_checks).
The rule now lives in a new internal/leaf package: Check (leaf_check), Rebuild (the explicit full rebuild) and Pending (the deferred schedule). internal/tag schedules a recheck at exactly the branch-row writes; the crosslink sweep, receive linker and repair gate apply a batch's schedule at the gate, each phantom-fill cascade transaction and each commit or tag transaction apply their own before commit, and verify.Rebuild keeps the whole-table recompute. Reparenting schedules nothing, as in fossil, so the reparent case is split into a one-batch case and a per-artifact case.
Tracked in #426.
* fix(manifest): recheck leaves a cancel reaches; drop rechecks of a failed receive tx (user: [email protected], size: 4139) [annotate] [blame] [check-ins using]
package manifest import ( "testing" "time" "github.com/danmestas/go-libfossil/internal/blob" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/leaf" ) // TestLeafRebuildOnlyHoldsCheckins pins Section 7.1: a leaf is a check-in. // fossil writes plink rows for forum posts and wiki edits that carry a P-card, // so a candidate set drawn from plink children alone puts them in the table. func TestLeafRebuildOnlyHoldsCheckins(t *testing.T) { r := setupTestRepo(t) base := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC) c1 := storeCheckin(t, r, "c1", "trunk", base) storeCheckin(t, r, "c2", "", base.Add(time.Minute), c1) if _, err := Crosslink(r); err != nil { t.Fatalf("Crosslink: %v", err) } var parent int64 if err := r.DB().QueryRow("SELECT rid FROM blob WHERE uuid=?", c1).Scan(&parent); err != nil { t.Fatal(err) } for i, typ := range []string{"f", "w"} { rid, _, err := blob.Store(r.DB(), []byte{byte('a' + i)}) if err != nil { t.Fatal(err) } if _, err := r.DB().Exec("INSERT INTO event(type,mtime,objid,user,comment) "+ "VALUES(?,2460000.5,?,'u','x')", typ, int64(rid)); err != nil { t.Fatal(err) } if _, err := r.DB().Exec("INSERT INTO plink(pid,cid,isprim,mtime) "+ "VALUES(?,?,1,2460000.5)", parent, int64(rid)); err != nil { t.Fatal(err) } } if err := leaf.Rebuild(r.DB()); err != nil { t.Fatalf("leaf.Rebuild: %v", err) } var n int if err := r.DB().QueryRow("SELECT count(*) FROM leaf JOIN event ON objid=rid " + "WHERE type<>'ci'").Scan(&n); err != nil { t.Fatal(err) } if n != 0 { t.Fatalf("%d non-check-in rows in leaf", n) } assertLeaves(t, r, "after repair", []string{"c2"}) } // TestLeafUntaggedChildUsesMainBranch pins Section 6.1: with main-branch set, // a check-in with no branch tag is on that branch, so it joins its parent // there and the parent is no leaf, by leaf.Rebuild and by leaf.Check. func TestLeafUntaggedChildUsesMainBranch(t *testing.T) { r := setupTestRepo(t) if err := r.SetConfig("main-branch", "main"); err != nil { t.Fatal(err) } base := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC) c1 := storeCheckin(t, r, "c1", "main", base) storeCheckin(t, r, "c2", "", base.Add(time.Minute), c1) if _, err := Crosslink(r); err != nil { t.Fatalf("Crosslink: %v", err) } // Propagation gave c2 the branch; drop it so c2 carries no active branch // row, as a check-in with a non-propagating parent tag does. if _, err := r.DB().Exec("DELETE FROM tagxref WHERE tagid=? AND rid=("+ "SELECT objid FROM event WHERE comment='c2')", leaf.BranchTagID); err != nil { t.Fatal(err) } if err := leaf.Rebuild(r.DB()); err != nil { t.Fatal(err) } // c2 is never rechecked (it has no tag, as in fossil), so only c1's // verdict is at stake: it has a same-branch child and is no leaf. assertLeaves(t, r, "after repair", nil) var rid int64 if err := r.DB().QueryRow("SELECT rid FROM blob WHERE uuid=?", c1).Scan(&rid); err != nil { t.Fatal(err) } if _, err := r.DB().Exec("INSERT OR IGNORE INTO leaf(rid) VALUES(?)", rid); err != nil { t.Fatal(err) } if err := leaf.Check(r.DB(), libfossil.FslID(rid)); err != nil { t.Fatal(err) } assertLeaves(t, r, "after leaf.Check", nil) } // TestInitialCheckinStartsMainBranch pins Section 6.1: the initial check-in // starts the branch named by the main-branch setting, not the literal trunk. func TestInitialCheckinStartsMainBranch(t *testing.T) { r := setupTestRepo(t) if err := r.SetConfig("main-branch", "main"); err != nil { t.Fatal(err) } rid, _, err := Checkin(r, CheckinOpts{ Files: []File{{Name: "a.txt", Content: []byte("a")}}, Comment: "root", User: "test", Time: time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC), }) if err != nil { t.Fatal(err) } var n int if err := r.DB().QueryRow(`SELECT count(*) FROM tagxref x JOIN tag t USING(tagid) WHERE x.rid=? AND x.tagtype=2 AND ((t.tagname='branch' AND x.value='main') OR t.tagname='sym-main')`, int64(rid)).Scan(&n); err != nil { t.Fatal(err) } if n != 2 { t.Fatalf("%d of branch=main, sym-main tags on the root check-in, want 2", n) } }