Artifact 06c91b1119c30fdf9f8fb971bca30703dde70ea987bd5cacde542a4be90103a0:
- File
internal/manifest/cherrypick_arrival_test.go
— part of check-in
[28f343c11b]
at
2026-10-23 10:34:14
on branch feat/horizon-cones
— fix(manifest): link a cherrypick source that arrives after its child
A check-in crosslinked before its cherrypick ('+') source arrived kept no cherrypick row, since insertCherrypicks skipped a source it could not find, and so never received the auxiliary mlink rows for that source: the late-arrival pass walked plink alone, which holds no cherrypick edges. Canonical skips a missing source the same way and relies on the rebuild its clone ends with; a clone here links as artifacts arrive and has no such rebuild. A clone of fossil-scm.org's repository was left with 139 cherrypick rows and 262 mlink rows fewer than its origin.
insertCherrypicks now reserves a missing source as a phantom, as plink does for a missing parent, and the late-arrival pass (linkPlinkChildren, now linkLateChildren) walks the source's cherrypick children with plink's. (user: [email protected], size: 4842) [annotate] [blame] [check-ins using] [more...]
package manifest import ( "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/repo" ) // A check-in that arrives before its cherrypick source must end with the // cherrypick row and the source's auxiliary mlink rows a rebuild derives. // // How: the source repository holds root A, B (a child of A changing f.txt), // and C (a child of A that cherrypicks B and also changes g.txt). The clone // receives A and C, crosslinks, then receives B and crosslinks again, as a // sync whose rounds deliver C first does. Its mlink and cherrypick tables are // then compared with a rebuild of the same blobs. On fossil-scm.org's // repository, a clone missing this had 139 cherrypick rows and 262 mlink rows // fewer than its origin. func TestCherrypickSourceArrivingLateMatchesRebuild(t *testing.T) { src := setupTestRepo(t) base := time.Date(2026, 10, 23, 12, 0, 0, 0, time.UTC) files := map[string]string{} for _, name := range []string{"fa", "fb", "ga", "gc"} { files[name] = storeFileContent(t, src, "content "+name) } a := storeDeck(t, src, &deck.Deck{ Type: deck.Checkin, C: "A", D: base, U: deck.User("testuser"), T: []deck.TagCard{{Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: "trunk"}}, F: []deck.FileCard{{Name: "f.txt", UUID: files["fa"]}, {Name: "g.txt", UUID: files["ga"]}}, }) b := storeDeck(t, src, &deck.Deck{ Type: deck.Checkin, C: "B", D: base.Add(time.Minute), U: deck.User("testuser"), P: []string{a}, F: []deck.FileCard{{Name: "f.txt", UUID: files["fb"]}, {Name: "g.txt", UUID: files["ga"]}}, }) c := storeDeck(t, src, &deck.Deck{ Type: deck.Checkin, C: "C", D: base.Add(2 * time.Minute), U: deck.User("testuser"), P: []string{a}, Q: []deck.CherryPick{{Target: b}}, F: []deck.FileCard{{Name: "f.txt", UUID: files["fb"]}, {Name: "g.txt", UUID: files["gc"]}}, }) dst := setupTestRepo(t) late := map[string]bool{b: true} var lateBlobs [][]byte for _, data := range expandAll(t, src) { if late[repoHash(dst, data)] { lateBlobs = append(lateBlobs, data) continue } if _, _, err := blob.Store(dst.DB(), data); err != nil { t.Fatalf("deliver: %v", err) } } if len(lateBlobs) != 1 { t.Fatalf("held back %d blobs, want B alone", len(lateBlobs)) } if _, err := Crosslink(dst); err != nil { t.Fatalf("Crosslink without B: %v", err) } if _, _, err := blob.Store(dst.DB(), lateBlobs[0]); err != nil { t.Fatalf("deliver B: %v", err) } if _, err := Crosslink(dst); err != nil { t.Fatalf("Crosslink with B: %v", err) } got := linkageDigest(t, dst.DB()) rebuildFromScratch(t, dst) want := linkageDigest(t, dst.DB()) if got != want { t.Errorf("C=%s linked before its cherrypick source; mlink and cherrypick differ from a rebuild:\n got %s\n want %s", c[:10], got, want) } var aux int if err := dst.DB().QueryRow("SELECT count(*) FROM mlink WHERE isaux").Scan(&aux); err != nil { t.Fatalf("count aux rows: %v", err) } if aux != 1 { t.Errorf("rebuild derived %d auxiliary mlink rows, want 1 (g.txt relative to B)", aux) } } // storeFileContent stores content in r and returns its name. func storeFileContent(t *testing.T, r *repo.Repo, content string) string { t.Helper() if _, _, err := blob.Store(r.DB(), []byte(content)); err != nil { t.Fatalf("store %q: %v", content, err) } return repoHash(r, []byte(content)) } // storeDeck marshals d, stores it in r, and returns its name. func storeDeck(t *testing.T, r *repo.Repo, d *deck.Deck) string { t.Helper() data, err := d.Marshal() if err != nil { t.Fatalf("marshal %s: %v", d.C, err) } if _, _, err := blob.Store(r.DB(), data); err != nil { t.Fatalf("store %s: %v", d.C, err) } return repoHash(r, data) } // linkageDigest is every mlink and cherrypick row, every column, in one // string. func linkageDigest(t *testing.T, q db.Querier) string { t.Helper() var mlink, cherrypick string if err := q.QueryRow(`SELECT coalesce(group_concat(v, '|'), '') FROM (SELECT mid || ':' || coalesce(fid,'') || ':' || coalesce(pmid,'') || ':' || coalesce(pid,'') || ':' || fnid || ':' || coalesce(pfnid,'') || ':' || coalesce(mperm,'') || ':' || isaux AS v FROM mlink ORDER BY mid, fnid, pmid, isaux)`).Scan(&mlink); err != nil { t.Fatalf("digest mlink: %v", err) } if err := q.QueryRow(`SELECT coalesce(group_concat(v, '|'), '') FROM (SELECT parentid || ':' || childid || ':' || isExclude AS v FROM cherrypick ORDER BY parentid, childid)`).Scan(&cherrypick); err != nil { t.Fatalf("digest cherrypick: %v", err) } if mlink == "" { t.Fatal("linkageDigest: empty digest; the comparison would be vacuous") } return "mlink " + mlink + " cherrypick " + cherrypick }