blob: 086c53f651438fc416c1534944978e25ff17d6d4 [file] [edit]
#!/bin/sh
test_description='precomputed diff hunks store (git diff-hunks)
The store maps an (old blob, new blob, diff settings) key to the hunks of
diffing the pair. It is a cache: reading is on by default
(core.diffHunks), while writing is
off by default and enabled per run by GIT_DIFF_HUNKS_WRITE (or the
diffHunks.write config), so a diff or log warms the store only when the
owner opts in. These tests check that a warmed store never changes
output, that lookups honor the diff settings, and that a corrupt store is
read as absent while verify reports the corruption.'
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
. ./test-lib.sh
STORE=.git/objects/info/diff-hunks
# Warm the store the way a repository owner would: a stat walk with
# writing enabled. A --stat walk records one entry per trim-stable blob
# pair, serving blame and the summary formats alike. Extra arguments
# (e.g. -c options) are passed to git before "log".
warm () {
GIT_DIFF_HUNKS_WRITE=1 git "$@" log --all --stat >/dev/null
}
# Run a command with the store disabled, for ground truth.
no_store () {
git -c core.diffhunks=false "$@"
}
test_expect_success 'setup' '
test_commit initial file.txt "line 1" &&
test_commit second file.txt "line 1
line 2" &&
test_commit third file.txt "line 1
line 2
line 3" &&
test_commit fourth file.txt "changed line 1
line 2
line 3
line 4"
'
test_expect_success 'ordinary commands do not create the store' '
git log --stat >/dev/null &&
git blame file.txt >/dev/null &&
git diff --stat second third >/dev/null &&
test_path_is_missing $STORE
'
test_expect_success 'writing is gated by env and config, env wins' '
test_when_finished "git diff-hunks clear" &&
# The diffHunks.write config enables writing.
git -c diffHunks.write=true log --all --stat >/dev/null &&
test_path_is_file $STORE &&
git diff-hunks clear &&
# GIT_DIFF_HUNKS_WRITE overrides the config: 0 disables it.
GIT_DIFF_HUNKS_WRITE=0 git -c diffHunks.write=true log --all --stat >/dev/null &&
test_path_is_missing $STORE &&
# and enables it without any config.
GIT_DIFF_HUNKS_WRITE=1 git log --all --stat >/dev/null &&
test_path_is_file $STORE
'
test_expect_success 'a warm builds a store that verifies' '
warm &&
test_path_is_file $STORE &&
git diff-hunks verify
'
test_expect_success 'a second warming run refreshes the store in place' '
warm &&
test_commit fifth file.txt "brand new line" &&
warm &&
git diff-hunks verify &&
no_store log --stat >expect &&
git log --stat >actual &&
test_cmp expect actual
'
test_expect_success 'core.diffhunks=false disables lookups' '
warm &&
git -c core.diffhunks=false blame --show-stats file.txt >out 2>&1 &&
test_grep "num precomputed hits: 0" out
'
# Writing seeds from the current store and merges into it, so a later
# warming run keeps the entries an earlier one recorded rather than
# rebuilding. Warm one pair, then a different pair, and confirm the first
# is still served.
test_expect_success 'a later warming run preserves earlier entries' '
git init incr &&
(
cd incr &&
test_commit a1 f.txt "1" &&
test_commit a2 f.txt "1
2" &&
test_commit a3 f.txt "1
2
3" &&
GIT_DIFF_HUNKS_WRITE=1 git diff --stat a1 a2 >/dev/null &&
git diff-hunks verify &&
GIT_DIFF_HUNKS_WRITE=1 git diff --stat a2 a3 >/dev/null &&
git diff-hunks verify &&
# Blaming as of a2 diffs the a1..a2 pair. If seeding had
# dropped it when the a2..a3 pair was warmed, this would
# report zero precomputed hits.
git blame --show-stats a2 -- f.txt >out 2>&1 &&
test_grep "num precomputed hits: [1-9]" out &&
# The second warm ADDED the a2..a3 pair; blaming a3 diffs
# both a2..a3 and a1..a2, so a hit on each shows the store
# gained the new pair while keeping the earlier one.
git blame --show-stats a3 -- f.txt >out3 2>&1 &&
test_grep "num precomputed hits: 2" out3 &&
no_store log --stat >expect &&
git log --stat >actual &&
test_cmp expect actual
)
'
test_expect_success 'log --stat matches with and without the store' '
no_store log --stat >expect &&
warm &&
git log --stat >actual &&
test_cmp expect actual
'
test_expect_success 'log --numstat and --shortstat match' '
no_store log --numstat >expect_num &&
no_store log --shortstat >expect_short &&
warm &&
git log --numstat >actual_num &&
git log --shortstat >actual_short &&
test_cmp expect_num actual_num &&
test_cmp expect_short actual_short
'
# A built store must reproduce diffstat output at every context
# length. Only trim-stable pairs are recorded, so one entry serves
# every context; a trim-divergent pair is never recorded and always
# computed. Zero context is where trim_common_tail runs, which is
# what makes the two diffs differ.
test_expect_success 'diffstat matches at several context lengths' '
no_store log --stat >expect_def &&
no_store log -U0 --stat >expect_u0 &&
no_store log -U7 --stat >expect_u7 &&
warm &&
git log --stat >got_def &&
git log -U0 --stat >got_u0 &&
git log -U7 --stat >got_u7 &&
test_cmp expect_def got_def &&
test_cmp expect_u0 got_u0 &&
test_cmp expect_u7 got_u7
'
test_expect_success 'store built at a nonzero context stays correct at that context' '
no_store -c diff.context=5 log --stat >expect &&
warm -c diff.context=5 &&
git -c diff.context=5 log --stat >actual &&
test_cmp expect actual
'
# This blob pair (a real git test file being modernized) has different
# valid diffs at different contexts: at zero context, where
# trim_common_tail runs, "diff -U0" reports 9/6, while "diff -U3"
# reports 10/7. Such a trim-divergent pair is exactly what the writer
# must never record, since no single entry could serve both readers.
# A compact synthetic pair cannot show this count split: on small
# inputs xdiff produces minimal diffs, minimal diffs of one pair all
# add and delete the same number of lines, and trimming the common
# tail preserves minimality, so the counts agree by construction (a
# search over thousands of synthetic pairs up to 8 lines found no
# split). The split needs the cost-capping heuristics that only larger
# inputs trigger, so the pair is shipped as a fixture under t4220/.
test_expect_success 'a trim-divergent file is correct at each context' '
cp "$TEST_DIRECTORY/t4220/trim-divergent-old" div.sh &&
git add div.sh &&
git commit -m divergent-old &&
cp "$TEST_DIRECTORY/t4220/trim-divergent-new" div.sh &&
git add div.sh &&
git commit -m divergent-new &&
no_store log -1 --format= --stat -- div.sh >expect_def &&
no_store log -1 --format= -U0 --stat -- div.sh >expect_u0 &&
warm &&
git log -1 --format= --stat -- div.sh >got_def &&
git log -1 --format= -U0 --stat -- div.sh >got_u0 &&
test_cmp expect_def got_def &&
test_cmp expect_u0 got_u0 &&
# The fixture must actually diverge, or the test would pass without
# exercising the split; fail loudly if a diff change ever levels it.
! test_cmp expect_def expect_u0
'
# A warming run displays the diffstat it computes. At zero context xdi_diff
# trims, so the displayed counts must be the trimmed ones (what a store-less
# run shows), not the untrimmed ones the writer compares against when it
# decides whether the pair is stable enough to record.
test_expect_success 'warming --stat at zero context matches a store-less run' '
git init -q warm-u0 &&
(
cd warm-u0 &&
cp "$TEST_DIRECTORY/t4220/trim-divergent-old" div.sh &&
git add div.sh && git commit -q -m old &&
cp "$TEST_DIRECTORY/t4220/trim-divergent-new" div.sh &&
git add div.sh && git commit -q -m new &&
git -c core.diffhunks=false log -1 --format= -U0 --stat -- div.sh >expect &&
GIT_DIFF_HUNKS_WRITE=1 git log -1 --format= -U0 --stat -- div.sh >got &&
test_cmp expect got
)
'
test_expect_success 'diff --stat matches with and without the store, both directions' '
no_store diff --stat second fourth >expect_fwd &&
no_store diff --stat fourth second >expect_rev &&
warm &&
git diff --stat second fourth >got_fwd &&
git diff --stat fourth second >got_rev &&
test_cmp expect_fwd got_fwd &&
test_cmp expect_rev got_rev
'
test_expect_success 'show and diff-tree --stat use the store' '
test_when_finished "git diff-hunks clear" &&
# diff_hunks_attach() runs for show and diff-tree: a write-enabled
# --stat records into the store (without the attach there is no
# writer, so nothing is written).
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git show --stat fourth >/dev/null &&
test_path_is_file "$STORE" &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git diff-tree --stat fourth >/dev/null &&
test_path_is_file "$STORE" &&
# Reading never changes their output.
git diff-hunks clear &&
no_store show --stat fourth >expect_show &&
no_store diff-tree --stat fourth >expect_dt &&
warm &&
git show --stat fourth >got_show &&
git diff-tree --stat fourth >got_dt &&
test_cmp expect_show got_show &&
test_cmp expect_dt got_dt
'
test_expect_success 'log -R --stat matches (reversed pairs keyed apart)' '
no_store log -R --stat >expect &&
warm &&
git log -R --stat >actual &&
test_cmp expect actual
'
# One warm serves both diffstat and blame: the blob pairs a blame
# walks are the same parent-child pairs the diffstat warm recorded.
test_expect_success 'a single warming run serves both blame and diffstat' '
warm &&
git blame --show-stats file.txt >out 2>&1 &&
test_grep "num precomputed hits: [1-9][0-9]*" out
'
# The diffstat read path produces identical output on a hit or a miss, so
# it emits a trace2 "read-hits" count to prove it consulted the store.
test_expect_success 'diffstat consults the store (trace shows read hits)' '
warm &&
GIT_TRACE2_EVENT="$PWD/trace_on.json" git log --stat >/dev/null &&
test_grep read-hits trace_on.json &&
test_env GIT_TRACE2_EVENT="$PWD/trace_off.json" no_store log --stat >/dev/null &&
test_grep ! read-hits trace_off.json
'
test_expect_success 'blame matches with and without the store' '
no_store blame file.txt >expect &&
warm &&
git blame file.txt >actual &&
test_cmp expect actual
'
test_expect_success 'blame --porcelain and --incremental match' '
no_store blame --porcelain file.txt >expect_p &&
no_store blame --incremental file.txt >expect_i &&
warm &&
git blame --porcelain file.txt >got_p &&
git blame --incremental file.txt >got_i &&
test_cmp expect_p got_p &&
test_cmp expect_i got_i
'
# Diff settings that change hunks but are not part of the store key must
# bypass it in both directions, so output stays byte-identical to a
# store-less run.
test_expect_success 'setup ignore fixture' '
git init ignore-repo &&
(
cd ignore-repo &&
test_write_lines code keep "# c" >f &&
git add f &&
git commit -m c1 &&
test_write_lines codeCH keep "# cX" >f &&
git add f &&
git commit -m c2 &&
warm
)
'
# Output parity alone cannot prove the guard: served counts can
# coincide with computed ones, so each bypass below also asserts the
# consultation itself (no read hit with the option, a hit without it)
# and that a warming run under the option records nothing.
test_expect_success '-I bypasses the store in both directions' '
(
cd ignore-repo &&
no_store diff -I"^#" --numstat HEAD~ HEAD >expect &&
git diff -I"^#" --numstat HEAD~ HEAD >actual &&
test_cmp expect actual &&
# -I does not change the key, so only the ignore_regex
# guard keeps the warmed entry from serving here.
GIT_TRACE2_EVENT="$PWD/trace_i.json" \
git diff -I"^#" --numstat HEAD~ HEAD >/dev/null &&
test_grep ! read-hits trace_i.json &&
GIT_TRACE2_EVENT="$PWD/trace_i_ctl.json" \
git diff --numstat HEAD~ HEAD >/dev/null &&
test_grep read-hits trace_i_ctl.json &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git diff -I"^#" --numstat HEAD~ HEAD >/dev/null &&
test_path_is_missing .git/objects/info/diff-hunks &&
# Restore the warmed fixture for the tests below.
warm
)
'
test_expect_success '-B bypasses the store in both directions' '
git init break-repo &&
(
cd break-repo &&
test_write_lines a b c d e f g h >f &&
git add f &&
git commit -m orig &&
test_write_lines 1 2 3 4 5 6 7 8 >f &&
git add f &&
git commit -m rewrite &&
warm &&
no_store diff -B --stat HEAD~ HEAD >expect &&
git diff -B --stat HEAD~ HEAD >actual &&
test_cmp expect actual &&
GIT_TRACE2_EVENT="$PWD/trace_b.json" \
git diff -B --stat HEAD~ HEAD >/dev/null &&
test_grep ! read-hits trace_b.json &&
GIT_TRACE2_EVENT="$PWD/trace_ctl.json" \
git diff --stat HEAD~ HEAD >/dev/null &&
test_grep read-hits trace_ctl.json &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git diff -B --stat HEAD~ HEAD >/dev/null &&
test_path_is_missing .git/objects/info/diff-hunks
)
'
test_expect_success '--anchored bypasses the store in both directions' '
(
cd ignore-repo &&
no_store diff --stat --anchored=keep HEAD~ HEAD >expect &&
git diff --stat --anchored=keep HEAD~ HEAD >actual &&
test_cmp expect actual &&
# Anchors do not change the key, so only the anchors guard
# keeps the warmed entry from serving here.
GIT_TRACE2_EVENT="$PWD/trace_anchor.json" \
git diff --stat --anchored=keep HEAD~ HEAD >/dev/null &&
test_grep ! read-hits trace_anchor.json &&
GIT_TRACE2_EVENT="$PWD/trace_plain.json" \
git diff --stat HEAD~ HEAD >/dev/null &&
test_grep read-hits trace_plain.json &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 \
git diff --stat --anchored=keep HEAD~ HEAD >/dev/null &&
test_path_is_missing .git/objects/info/diff-hunks
)
'
test_expect_success '--ignore-blank-lines bypasses the store in both directions' '
git init ibl-repo &&
(
cd ibl-repo &&
printf "a\n\nx\ny\nb\n" >f &&
git add f &&
git commit -m v1 &&
printf "a\nx\ny\nB\n" >f &&
git add f &&
git commit -m v2 &&
warm &&
no_store diff --stat --ignore-blank-lines HEAD~ HEAD >expect &&
git diff --stat --ignore-blank-lines HEAD~ HEAD >actual &&
test_cmp expect actual &&
# The flag is an xdl_opts bit and thus part of the key; the
# stat consumer excludes it before consulting at all.
GIT_TRACE2_EVENT="$PWD/trace_ibl.json" \
git diff --stat --ignore-blank-lines HEAD~ HEAD >/dev/null &&
test_grep ! read-hits trace_ibl.json &&
GIT_TRACE2_EVENT="$PWD/trace_plain.json" \
git diff --stat HEAD~ HEAD >/dev/null &&
test_grep read-hits trace_plain.json &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 \
git diff --stat --ignore-blank-lines HEAD~ HEAD >/dev/null &&
test_path_is_missing .git/objects/info/diff-hunks
)
'
test_expect_success 'a whitespace-ignoring diff is not served default entries' '
git init ws-repo &&
(
cd ws-repo &&
test_write_lines alpha beta gamma >f &&
git add f &&
git commit -m c1 &&
test_write_lines " alpha" beta gamma delta >f &&
git add f &&
git commit -m c2 &&
warm &&
no_store diff -w --numstat HEAD~ HEAD >expect &&
git diff -w --numstat HEAD~ HEAD >actual &&
test_cmp expect actual
)
'
test_expect_success 'blame -w stays correct and does not hit default entries' '
(
cd ws-repo &&
no_store blame -w f >expect &&
git blame -w --show-stats f >out 2>&1 &&
test_grep "num precomputed hits: 0" out &&
git blame -w f >actual &&
test_cmp expect actual
)
'
test_expect_success 'blame with indentHeuristic off stays correct and misses' '
warm &&
git -c diff.indentHeuristic=false blame --show-stats file.txt >out 2>&1 &&
test_grep "num precomputed hits: 0" out &&
no_store -c diff.indentHeuristic=false blame file.txt >expect &&
git -c diff.indentHeuristic=false blame file.txt >actual &&
test_cmp expect actual
'
test_expect_success 'a driver algorithm override keeps output correct and keys apart' '
git init driver-algo &&
(
cd driver-algo &&
echo "file.foo diff=foo" >.gitattributes &&
git add .gitattributes &&
git commit -m attributes &&
test_write_lines 1 2 3 4 5 >file.foo &&
git add file.foo &&
git commit -m one &&
test_write_lines 1 2 X 4 5 6 >file.foo &&
git add file.foo &&
git commit -m two &&
warm -c diff.foo.algorithm=histogram &&
no_store -c diff.foo.algorithm=histogram log --stat >expect &&
git -c diff.foo.algorithm=histogram log --stat >actual &&
test_cmp expect actual &&
# The driver algorithm is an xdl_opts key bit: entries
# warmed at the default settings must not serve a
# driver-forced histogram read, and output stays correct.
git diff-hunks clear &&
warm &&
no_store -c diff.foo.algorithm=histogram log --stat >expect2 &&
git -c diff.foo.algorithm=histogram log --stat >actual2 &&
test_cmp expect2 actual2 &&
GIT_TRACE2_EVENT="$PWD/trace_algo.json" \
git -c diff.foo.algorithm=histogram log --stat >/dev/null &&
test_grep ! read-hits trace_algo.json &&
GIT_TRACE2_EVENT="$PWD/trace_algo_ctl.json" \
git log --stat >/dev/null &&
test_grep read-hits trace_algo_ctl.json
)
'
test_expect_success 'blame --reverse never consults the store' '
warm &&
git blame --reverse HEAD~3..HEAD file.txt >actual 2>/dev/null &&
no_store blame --reverse HEAD~3..HEAD file.txt >expect 2>/dev/null &&
test_cmp expect actual &&
# Reverse blame withholds the pair identity. Zero hits alone
# cannot prove that: reverse pairs are never warmed, so a
# consulted pair would miss, not hit. Zero misses is what shows
# the store was never consulted.
git blame --reverse --show-stats HEAD~3..HEAD file.txt \
>stats 2>/dev/null &&
test_grep "num precomputed hits: 0" stats &&
test_grep "num precomputed misses: 0" stats
'
test_expect_success 'blame with a textconv driver bypasses the store' '
echo "tc.txt diff=tc" >>.gitattributes &&
git add .gitattributes &&
git commit -m tc-attr &&
git config diff.tc.textconv "sed -e s/1/one/" &&
test_commit tc1 tc.txt "line 1" &&
test_commit tc2 tc.txt "line 1
line 2" &&
warm &&
git blame --show-stats tc.txt >out 2>&1 &&
test_grep "num precomputed hits: 0" out &&
no_store blame tc.txt >expect &&
git blame tc.txt >actual &&
test_cmp expect actual
'
test_expect_success 'a replaced blob makes the store step aside' '
git init replace-repo &&
(
cd replace-repo &&
test_commit r1 f.txt "a" &&
test_commit r2 f.txt "a
b" &&
warm &&
# Control: without a replacement the pair is served.
GIT_TRACE2_EVENT="$PWD/trace_ctl.json" \
git log -1 --format= --numstat -- f.txt >/dev/null &&
test_grep read-hits trace_ctl.json &&
# Replace r2 blob: the diff now reads different content
# (through OBJECT_INFO_LOOKUP_REPLACE) under the id the store
# keyed, so a served answer would be the pre-replacement diff.
# Identity is withheld, the store steps aside, and the builtin
# computes from the replaced content.
new_blob=$(git rev-parse HEAD:f.txt) &&
repl=$(printf "a\nB\nC\nD\n" | git hash-object -w --stdin) &&
git replace "$new_blob" "$repl" &&
no_store log -1 --format= --numstat -- f.txt >expect &&
git log -1 --format= --numstat -- f.txt >actual &&
test_cmp expect actual &&
GIT_TRACE2_EVENT="$PWD/trace_repl.json" \
git log -1 --format= --numstat -- f.txt >/dev/null &&
test_grep ! read-hits trace_repl.json
)
'
test_expect_success 'blame -M and -C stay correct with the store' '
warm &&
no_store blame -M file.txt >expect_m &&
no_store blame -C file.txt >expect_c &&
git blame -M file.txt >got_m &&
git blame -C file.txt >got_c &&
test_cmp expect_m got_m &&
test_cmp expect_c got_c
'
# Copy-detecting (and reverse) blame still diff blob pairs through
# pass_blame_to_parent, so they must use the real blame xdl_opts. A
# whitespace-only change is invisible under -w; if -w were dropped on
# these paths the -w and non-w results would coincide.
test_expect_success 'blame -C honors -w' '
git init -q blame-cw &&
(
cd blame-cw &&
printf "one\ntwo\nthree\n" >f &&
git add f && git commit -q -m base &&
printf "one\n two \nthree\n" >f &&
git add f && git commit -q -m reindent &&
git blame -C -w f >with_w &&
git blame -C f >without_w &&
! test_cmp with_w without_w
)
'
# Cover the pair shapes an object walk encounters: binary and
# mode-only changes produce no text hunks to record.
test_expect_success 'binary and mode-only changes do not break the writer' '
printf "\\000\\001\\002" >bin.dat &&
git add bin.dat &&
git commit -m binary-1 &&
printf "\\000\\001\\003\\004" >bin.dat &&
git add bin.dat &&
git commit -m binary-2 &&
echo "mode content" >mode.txt &&
git add mode.txt &&
git commit -m mode-1 &&
test_chmod +x mode.txt &&
git commit -m mode-2 &&
no_store log --stat >expect &&
warm &&
git log --stat >actual &&
test_cmp expect actual
'
test_expect_success 'blame across a rename matches' '
echo "original content" >rename-src.txt &&
git add rename-src.txt &&
git commit -m "add rename-src" &&
echo "more" >>rename-src.txt &&
git add rename-src.txt &&
git commit -m "modify rename-src" &&
git mv rename-src.txt rename-dst.txt &&
git commit -m "rename" &&
echo "post" >>rename-dst.txt &&
git add rename-dst.txt &&
git commit -m "modify after rename" &&
no_store blame rename-dst.txt >expect &&
warm &&
git blame rename-dst.txt >actual &&
test_cmp expect actual
'
test_expect_success 'blame handles merge commits' '
git checkout -b merge-side main~2 &&
test_commit merge-change merge-file.txt "side content" &&
git checkout main &&
git merge --no-edit merge-side &&
no_store blame merge-file.txt >expect &&
warm &&
git blame merge-file.txt >actual &&
test_cmp expect actual
'
test_expect_success 'distinct --contents against one revision do not collide' '
warm &&
test_write_lines "line 1" "appended line" >c1 &&
test_write_lines "rewritten line" >c2 &&
# Ground truth without the store.
no_store blame -s --contents=c2 file.txt initial >expect &&
# With the store, an intervening c1 run must not poison the c2 lookup.
git blame -s --contents=c1 file.txt initial >/dev/null &&
git blame -s --contents=c2 file.txt initial >actual &&
test_cmp expect actual &&
# The --contents side is a working-tree pseudo-commit (a null commit
# id), so its pairs withhold identity and never consult the store.
# Output parity alone cannot show that: a consulted unwarmed pair
# would miss, not hit, so zero misses is what proves the pair was
# never looked up.
git blame -s --show-stats --contents=c2 file.txt initial >stats 2>&1 &&
test_grep "num precomputed hits: 0" stats &&
test_grep "num precomputed misses: 0" stats
'
test_expect_success 'blame --ignore-rev bypasses the store for ignored pairs' '
git init ignore-rev-repo &&
(
cd ignore-rev-repo &&
test_commit ir1 f.txt "base" &&
test_commit ir2 f.txt "base
more" &&
warm &&
# Control: the ordinary pass is served, nothing is computed.
git blame --show-stats f.txt >ctl 2>&1 &&
test_grep "num precomputed hits: 1" ctl &&
test_grep "num get patch: 0" ctl &&
no_store blame --ignore-rev ir2 f.txt >expect &&
git blame --ignore-rev ir2 f.txt >actual &&
test_cmp expect actual &&
# The ignored revision adds a pass that withholds identity:
# it computes its diff (get patch rises) instead of being
# served or even counted as a store consultation.
git blame --ignore-rev ir2 --show-stats f.txt >stats 2>&1 &&
test_grep "num precomputed hits: 1" stats &&
test_grep "num precomputed misses: 0" stats &&
test_grep "num get patch: 1" stats
)
'
test_expect_success 'blame counts misses for pairs the store does not hold' '
(
cd ignore-rev-repo &&
test_commit ir3 f.txt "base
more
third" &&
git blame --show-stats f.txt >stats 2>&1 &&
test_grep "num precomputed hits: 1" stats &&
test_grep "num precomputed misses: 1" stats
)
'
test_expect_success 'log -L --stat neither reads nor records' '
warm &&
GIT_TRACE2_EVENT="$PWD/trace_linelog.json" \
git log -L1,1:file.txt --stat >/dev/null &&
test_grep ! read-hits trace_linelog.json &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git log -L1,1:file.txt --stat >/dev/null &&
test_path_is_missing $STORE
'
# Integrity: a structurally broken header is read as absent (the reader
# falls back to xdiff and stays correct); a checksum mismatch is caught
# by verify, which is when integrity is checked.
test_expect_success 'a truncated store is read as absent' '
warm &&
test_copy_bytes 20 <$STORE >truncated &&
mv truncated $STORE &&
no_store blame file.txt >expect &&
git blame file.txt >actual &&
test_cmp expect actual
'
test_expect_success 'a corrupt signature is read as absent' '
warm &&
printf "XXXX" >corrupt &&
tail -c +5 <$STORE >>corrupt &&
mv corrupt $STORE &&
no_store blame file.txt >expect &&
git blame file.txt >actual &&
test_cmp expect actual
'
# Byte 6 of the header is the chunk count; a value larger than the file
# can hold must be rejected before the chunk table is walked.
test_expect_success 'an over-claimed chunk count is read as absent' '
warm &&
printf "\377" | dd of=$STORE bs=1 seek=6 count=1 conv=notrunc 2>/dev/null &&
no_store blame file.txt >expect &&
git blame file.txt >actual &&
test_cmp expect actual
'
# A record with no hunks would replay as an equivalence claim, which
# the writer never records; the reader must treat such a record as a
# miss and recompute, and verify must flag it.
test_expect_success 'a zero-hunk record is read as a miss and fails verify' '
git init zero-hunk &&
(
cd zero-hunk &&
test_commit z1 f.txt "base" &&
test_commit z2 f.txt "base
more" &&
warm &&
# The store holds one entry of one hunk: a 4-byte count and
# one 16-byte hunk record, just before the trailing
# checksum. Zero the count to craft the record the writer
# refuses to produce.
rawsz=$(test_oid rawsz) &&
fsize=$(test_file_size $STORE) &&
printf "\\0\\0\\0\\0" | dd of=$STORE bs=1 \
seek=$((fsize - rawsz - 20)) count=4 conv=notrunc \
2>/dev/null &&
no_store blame f.txt >expect &&
git blame --show-stats f.txt >stats 2>&1 &&
test_grep "num precomputed hits: 0" stats &&
git blame f.txt >actual &&
test_cmp expect actual &&
test_must_fail git diff-hunks verify
)
'
test_expect_success 'verify succeeds on a valid store and on an absent one' '
warm &&
git diff-hunks verify &&
git diff-hunks clear &&
test_path_is_missing $STORE &&
git diff-hunks verify
'
test_expect_success 'verify detects a checksum mismatch' '
test_when_finished "git diff-hunks clear" &&
warm &&
fsize=$(test_file_size $STORE) &&
mid=$((fsize / 2)) &&
printf "\\377" | dd of=$STORE bs=1 seek=$mid count=1 conv=notrunc 2>/dev/null &&
test_must_fail git diff-hunks verify
'
test_expect_success 'a warm discards a corrupt store rather than seeding from it' '
test_when_finished "git diff-hunks clear" &&
warm &&
# Corrupt the checksum: the next warm must not carry the corrupt
# entries forward into a fresh checksum-valid file; it discards
# them (with a warning) and rewrites a store that verifies.
fsize=$(test_file_size $STORE) &&
printf "\\377" | dd of=$STORE bs=1 seek=$((fsize / 2)) count=1 conv=notrunc 2>/dev/null &&
warm 2>err &&
test_grep "failed its checksum" err &&
git diff-hunks verify &&
no_store log --stat >expect &&
git log --stat >actual &&
test_cmp expect actual
'
# A generated patch must carry the builtin diffstat, not one served from
# the sender's local store, so its counts do not depend on whether the
# sender warmed the store. Poison the store so a served answer diverges
# from the builtin, then confirm format-patch shows the builtin counts.
test_expect_success 'format-patch keeps its diffstat off the store' '
git init fp-repo &&
(
cd fp-repo &&
test_commit p1 f.txt "a" &&
test_commit p2 f.txt "a
b" &&
warm &&
# Bump the new-side count of the single recorded hunk. The
# record stays structurally valid, and a read skips the
# trailing checksum, so the store serves this poisoned count.
rawsz=$(test_oid rawsz) &&
fsize=$(test_file_size .git/objects/info/diff-hunks) &&
printf "\\0\\0\\0\\7" | dd of=.git/objects/info/diff-hunks bs=1 \
seek=$((fsize - rawsz - 4)) count=4 conv=notrunc 2>/dev/null &&
# The store now serves a divergent count, proving the poison
# is live and observable through a store consumer.
printf "7\t0\tf.txt\n" >poisoned &&
git log -1 --format= --numstat -- f.txt >served &&
test_cmp poisoned served &&
# format-patch does not consult the store, so its output is
# identical with the store poisoned and with it disabled.
no_store format-patch -1 --stdout --stat -- f.txt >expect &&
git format-patch -1 --stdout --stat -- f.txt >actual &&
test_cmp expect actual
)
'
test_expect_success 'diff-hunks clear removes the store file' '
warm &&
test_path_is_file $STORE &&
git diff-hunks clear &&
test_path_is_missing $STORE
'
test_done