| #!/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 |