Artifact 3f0de73b98e5169e26308ac046afdbac36829353dbeff4c89dccd2a691a12706:
- File
internal/manifest/log_test.go
— part of check-in
[df495ba69e]
at
2026-07-27 22:04:05
on branch trunk
— fix: derive leaf by canonical's branch rule, in both the sweep and the commit path (#197)
* fix: derive leaf from fossil's branch rule and bound the deferral rollup
Leaf derivation dropped 66% of rows on the real Fossil repository (487 of 1,426) because both leaf paths treated any parent link as retiring a leaf. Canonical fossil retires a check-in only when a child carries the *same* branch name, with an absent branch tag reading as "trunk" (src/leaf.c: is_a_leaf / leaf_check / leaf_rebuild all share that predicate and none consult plink.isprim). Verified against fossil 2.28 itself: after a branch commit off the trunk tip, both the incremental table and `fossil rebuild` keep the trunk tip as a leaf even though its only child is its primary child, and a branch tip merged into trunk stays a leaf as well.
Two code paths were wrong, in the same way:
- repairLeafTable excluded every `pid` in plink, so merged branch tips disappeared -- the class that accounts for 939 of the corpus's leaves. - Checkin deleted every parent from leaf, so committing onto a new branch silently retired the branch point.
Both now share leafCheck/repairLeafTable in internal/manifest/leaf.go. The recompute also has to run after tag propagation rather than before it: a check-in that declares no branch of its own only has a branch value once propagation has written it, and comparing against a missing value made every inherited-branch tip look like a leaf.
Applying the new predicate to the real repository's own event/plink/tagxref yields 1,428 against fossil's 1,426, and every one of the six differing rows is fossil's incremental bookkeeping rather than its predicate: 2 are phantom rids fossil's leaf_check inserts as plink parents of check-ins it linked, and 4 are parentless check-ins with no branch tag, which nothing ever enqueues for a leaf check. Running fossil's own leaf_rebuild() on that repository gives 1,494, so it is leaf_check, not leaf_rebuild, that the shipped table reflects.
Also caps the deferral rollup's identifier lists (issue #194). A clone deferring 180 check-ins on 417 missing blobs rendered a single ~28 KB log line; the counts stay exact at INFO and the lists print eight entries plus an "...and N more" so nothing is dropped without saying so.
Fixes #189 Fixes #194
* chore: retrigger code scanning after branch restore (user: [email protected], size: 2900) [annotate] [blame] [check-ins using]
package manifest import ( "bytes" "log/slog" "strconv" "strings" "testing" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // captureInfo redirects the default logger into a buffer at INFO for one test. func captureInfo(t *testing.T) *bytes.Buffer { t.Helper() buf := &bytes.Buffer{} prev := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo}))) t.Cleanup(func() { slog.SetDefault(prev) }) return buf } // TestLogDeferredCheckinsBoundsIdentifierLists pins issue #194: the rollup used // to render every deferred rid and every missing blob UUID inline, which on a // clone of the Fossil repository was 417 hashes -- roughly 27 KB -- in one line. // The counts are the actionable part and stay exact; the lists are capped. func TestLogDeferredCheckinsBoundsIdentifierLists(t *testing.T) { buf := captureInfo(t) const ( deferred = 180 missing = 417 ) rids := make([]libfossil.FslID, deferred) for i := range rids { rids[i] = libfossil.FslID(60000 + i) } blobs := make(map[string]struct{}, missing) for i := 0; i < missing; i++ { blobs[strings.Repeat(strconv.Itoa(i%10), 64)+strconv.Itoa(i)] = struct{}{} } logDeferredCheckins("Crosslink", rids, blobs, 3) line := buf.String() if !strings.Contains(line, "deferred="+strconv.Itoa(deferred)) { t.Errorf("deferred count missing from %q", line) } if !strings.Contains(line, "missing_blob_count="+strconv.Itoa(missing)) { t.Errorf("missing_blob_count missing from %q", line) } if !strings.Contains(line, "linked=3") { t.Errorf("linked count missing from %q", line) } // A line this size is readable in a terminal and survives a log shipper. // The pre-fix line for these inputs was ~28 KB. if len(line) > 2048 { t.Errorf("log line is %d bytes, want <= 2048:\n%s", len(line), line) } // The elided remainder is reported rather than silently dropped. for _, want := range []string{ "and " + strconv.Itoa(deferred-logDeferredSampleSize) + " more", "and " + strconv.Itoa(missing-logDeferredSampleSize) + " more", } { if !strings.Contains(line, want) { t.Errorf("missing elision marker %q in %q", want, line) } } } // TestLogDeferredCheckinsShortListsAreComplete keeps the common case whole: a // handful of deferred artifacts is small enough to print in full, and must not // grow a misleading "and 0 more". func TestLogDeferredCheckinsShortListsAreComplete(t *testing.T) { buf := captureInfo(t) logDeferredCheckins("AfterDephantomize", []libfossil.FslID{7, 9}, map[string]struct{}{"aa": {}, "bb": {}}, 1) line := buf.String() for _, want := range []string{"7", "9", "aa", "bb", "deferred=2", "missing_blob_count=2"} { if !strings.Contains(line, want) { t.Errorf("missing %q in %q", want, line) } } if strings.Contains(line, "more") { t.Errorf("unexpected elision marker in %q", line) } }