Summary
The persistent VCS history cache ignores git's diff configuration.
diff.algorithm, and the diff drivers, decide how much churn is recorded
for each commit, but cache::fingerprint() hashes neither. After a change
to either, bca vcs serves stale churn_long / churn_recent values,
along with everything derived from them, including risk_score. The
incremental splice then persists the stale churn under each new head, so
it survives HEAD moving and only --clear-cache clears it.
This is the same failure class #1262 fixed for the mailmap, through a
different input that lives outside the commit graph.
Why the cache misses it
history::collect_events (src/vcs/git/history.rs:67) builds its blob
diffs from repo.diff_resource_cache_for_tree_diff(). gix configures
that platform from the repository's git config, including
diff.algorithm, so the recorded line counts depend on it.
cache::fingerprint() (src/vcs/cache.rs) hashes the window, traversal
and bot options, as_of, and (since #1262) a mailmap digest. It does not
hash the diff configuration, so a config change leaves the fingerprint
unchanged, and the old entry hits.
Myers always finds a minimal diff and histogram does not, so the two can
record different churn for the same commit. Histogram anchors on
low-occurrence lines, so moving a unique line across repeated content
separates them sharply.
Reproduction
Measured with a debug build of the fix/batch-2026-09-10 branch (#1424).
R=$(mktemp -d); C=$(mktemp -d); cd $R
git init -q; git config user.name A; git config user.email a@x
printf 'fn a() {}\nfn b() {}\nfn c() {}\nfn a() {}\nfn b() {}\nfn c() {}\nfn only_once() {}\n' > a.rs
git add a.rs; git commit -qm one
printf 'fn only_once() {}\nfn a() {}\nfn b() {}\nfn c() {}\nfn a() {}\nfn b() {}\nfn c() {}\n' > a.rs
git add a.rs; git commit -qm two
git's own view of commit two confirms the algorithms disagree:
$ git -c diff.algorithm=myers diff --numstat HEAD~1 HEAD # 1 added, 1 removed
$ git -c diff.algorithm=histogram diff --numstat HEAD~1 HEAD # 6 added, 6 removed
bca vcs (churn_long) with --cache-dir $C:
| step |
cached |
fresh (--no-cache) |
| prime under the default (myers) |
9 |
9 |
git config diff.algorithm histogram |
9 |
19 |
| add a commit (HEAD moves; splice runs) |
10 |
20 |
| a later pure hit on that persisted entry |
10 |
20 |
The last two rows are the worse case. The splice re-persists the stale
myers-based churn for commit two under the new head, so the divergence
survives HEAD moving and every later run reproduces it.
Fix shape
Fold the effective diff configuration into cache::fingerprint(), as
#1262 did for the mailmap: the resolved algorithm, plus whichever
driver settings (diff.<driver>.*, .gitattributes diff= bindings)
can change a recorded line count. Read the effective values from the
same gix platform the walk uses rather than re-deriving git's precedence
rules locally. #1262 made the same call for the mailmap, because a
hand-rolled source list is a coverage claim that nothing checks.
Drivers are the harder half. A textconv driver changes which bytes are
diffed at all, and .gitattributes can bind one per path. Decide whether
to fingerprint the attribute-resolved driver set or document drivers as a
known gap.
The max-effort review of #1424 found this. That branch corrects the docs
that claimed the cache contract was complete once the mailmap was
fingerprinted (src/vcs/cache.rs, src/vcs/git/cached.rs, and the book's
commands/vcs.md and python/vcs.md), so they now name the diff config
as an unfingerprinted input. It does not change the code. This issue
is for the fix.
Summary
The persistent VCS history cache ignores git's diff configuration.
diff.algorithm, and the diff drivers, decide how much churn is recordedfor each commit, but
cache::fingerprint()hashes neither. After a changeto either,
bca vcsserves stalechurn_long/churn_recentvalues,along with everything derived from them, including
risk_score. Theincremental splice then persists the stale churn under each new head, so
it survives
HEADmoving and only--clear-cacheclears it.This is the same failure class #1262 fixed for the mailmap, through a
different input that lives outside the commit graph.
Why the cache misses it
history::collect_events(src/vcs/git/history.rs:67) builds its blobdiffs from
repo.diff_resource_cache_for_tree_diff(). gix configuresthat platform from the repository's git config, including
diff.algorithm, so the recorded line counts depend on it.cache::fingerprint()(src/vcs/cache.rs) hashes the window, traversaland bot options,
as_of, and (since #1262) a mailmap digest. It does nothash the diff configuration, so a config change leaves the fingerprint
unchanged, and the old entry hits.
Myers always finds a minimal diff and histogram does not, so the two can
record different churn for the same commit. Histogram anchors on
low-occurrence lines, so moving a unique line across repeated content
separates them sharply.
Reproduction
Measured with a debug build of the
fix/batch-2026-09-10branch (#1424).git's own view of commit two confirms the algorithms disagree:
bca vcs(churn_long) with--cache-dir $C:--no-cache)git config diff.algorithm histogramThe last two rows are the worse case. The splice re-persists the stale
myers-based churn for commit two under the new head, so the divergence
survives
HEADmoving and every later run reproduces it.Fix shape
Fold the effective diff configuration into
cache::fingerprint(), as#1262 did for the mailmap: the resolved algorithm, plus whichever
driver settings (
diff.<driver>.*,.gitattributesdiff=bindings)can change a recorded line count. Read the effective values from the
same gix platform the walk uses rather than re-deriving git's precedence
rules locally. #1262 made the same call for the mailmap, because a
hand-rolled source list is a coverage claim that nothing checks.
Drivers are the harder half. A
textconvdriver changes which bytes arediffed at all, and
.gitattributescan bind one per path. Decide whetherto fingerprint the attribute-resolved driver set or document drivers as a
known gap.
Status on #1424
The max-effort review of #1424 found this. That branch corrects the docs
that claimed the cache contract was complete once the mailmap was
fingerprinted (
src/vcs/cache.rs,src/vcs/git/cached.rs, and the book'scommands/vcs.mdandpython/vcs.md), so they now name the diff configas an unfingerprinted input. It does not change the code. This issue
is for the fix.