820 lines
28 KiB
Bash
Executable File
820 lines
28 KiB
Bash
Executable File
#!/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
|