| 1 | #!/bin/sh |
| 2 | |
| 3 | test_description='precomputed diff hunks store (git diff-hunks) |
| 4 | |
| 5 | The store maps an (old blob, new blob, diff settings) key to the hunks of |
| 6 | diffing the pair. It is a cache: reading is on by default |
| 7 | (core.diffHunks), while writing is |
| 8 | off by default and enabled per run by GIT_DIFF_HUNKS_WRITE (or the |
| 9 | diffHunks.write config), so a diff or log warms the store only when the |
| 10 | owner opts in. These tests check that a warmed store never changes |
| 11 | output, that lookups honor the diff settings, and that a corrupt store is |
| 12 | read as absent while verify reports the corruption.' |
| 13 | |
| 14 | GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main |
| 15 | export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME |
| 16 | |
| 17 | . ./test-lib.sh |
| 18 | |
| 19 | STORE=.git/objects/info/diff-hunks |
| 20 | |
| 21 | # Warm the store the way a repository owner would: a stat walk with |
| 22 | # writing enabled. A --stat walk records one entry per trim-stable blob |
| 23 | # pair, serving blame and the summary formats alike. Extra arguments |
| 24 | # (e.g. -c options) are passed to git before "log". |
| 25 | warm () { |
| 26 | GIT_DIFF_HUNKS_WRITE=1 git "$@" log --all --stat >/dev/null |
| 27 | } |
| 28 | |
| 29 | # Run a command with the store disabled, for ground truth. |
| 30 | no_store () { |
| 31 | git -c core.diffhunks=false "$@" |
| 32 | } |
| 33 | |
| 34 | test_expect_success 'setup' ' |
| 35 | test_commit initial file.txt "line 1" && |
| 36 | test_commit second file.txt "line 1 |
| 37 | line 2" && |
| 38 | test_commit third file.txt "line 1 |
| 39 | line 2 |
| 40 | line 3" && |
| 41 | test_commit fourth file.txt "changed line 1 |
| 42 | line 2 |
| 43 | line 3 |
| 44 | line 4" |
| 45 | ' |
| 46 | |
| 47 | test_expect_success 'ordinary commands do not create the store' ' |
| 48 | git log --stat >/dev/null && |
| 49 | git blame file.txt >/dev/null && |
| 50 | git diff --stat second third >/dev/null && |
| 51 | test_path_is_missing $STORE |
| 52 | ' |
| 53 | |
| 54 | test_expect_success 'writing is gated by env and config, env wins' ' |
| 55 | test_when_finished "git diff-hunks clear" && |
| 56 | # The diffHunks.write config enables writing. |
| 57 | git -c diffHunks.write=true log --all --stat >/dev/null && |
| 58 | test_path_is_file $STORE && |
| 59 | git diff-hunks clear && |
| 60 | # GIT_DIFF_HUNKS_WRITE overrides the config: 0 disables it. |
| 61 | GIT_DIFF_HUNKS_WRITE=0 git -c diffHunks.write=true log --all --stat >/dev/null && |
| 62 | test_path_is_missing $STORE && |
| 63 | # and enables it without any config. |
| 64 | GIT_DIFF_HUNKS_WRITE=1 git log --all --stat >/dev/null && |
| 65 | test_path_is_file $STORE |
| 66 | ' |
| 67 | |
| 68 | test_expect_success 'a warm builds a store that verifies' ' |
| 69 | warm && |
| 70 | test_path_is_file $STORE && |
| 71 | git diff-hunks verify |
| 72 | ' |
| 73 | |
| 74 | test_expect_success 'a second warming run refreshes the store in place' ' |
| 75 | warm && |
| 76 | test_commit fifth file.txt "brand new line" && |
| 77 | warm && |
| 78 | git diff-hunks verify && |
| 79 | no_store log --stat >expect && |
| 80 | git log --stat >actual && |
| 81 | test_cmp expect actual |
| 82 | ' |
| 83 | |
| 84 | test_expect_success 'core.diffhunks=false disables lookups' ' |
| 85 | warm && |
| 86 | git -c core.diffhunks=false blame --show-stats file.txt >out 2>&1 && |
| 87 | test_grep "num precomputed hits: 0" out |
| 88 | ' |
| 89 | |
| 90 | # Writing seeds from the current store and merges into it, so a later |
| 91 | # warming run keeps the entries an earlier one recorded rather than |
| 92 | # rebuilding. Warm one pair, then a different pair, and confirm the first |
| 93 | # is still served. |
| 94 | test_expect_success 'a later warming run preserves earlier entries' ' |
| 95 | git init incr && |
| 96 | ( |
| 97 | cd incr && |
| 98 | test_commit a1 f.txt "1" && |
| 99 | test_commit a2 f.txt "1 |
| 100 | 2" && |
| 101 | test_commit a3 f.txt "1 |
| 102 | 2 |
| 103 | 3" && |
| 104 | GIT_DIFF_HUNKS_WRITE=1 git diff --stat a1 a2 >/dev/null && |
| 105 | git diff-hunks verify && |
| 106 | GIT_DIFF_HUNKS_WRITE=1 git diff --stat a2 a3 >/dev/null && |
| 107 | git diff-hunks verify && |
| 108 | |
| 109 | # Blaming as of a2 diffs the a1..a2 pair. If seeding had |
| 110 | # dropped it when the a2..a3 pair was warmed, this would |
| 111 | # report zero precomputed hits. |
| 112 | git blame --show-stats a2 -- f.txt >out 2>&1 && |
| 113 | test_grep "num precomputed hits: [1-9]" out && |
| 114 | |
| 115 | # The second warm ADDED the a2..a3 pair; blaming a3 diffs |
| 116 | # both a2..a3 and a1..a2, so a hit on each shows the store |
| 117 | # gained the new pair while keeping the earlier one. |
| 118 | git blame --show-stats a3 -- f.txt >out3 2>&1 && |
| 119 | test_grep "num precomputed hits: 2" out3 && |
| 120 | |
| 121 | no_store log --stat >expect && |
| 122 | git log --stat >actual && |
| 123 | test_cmp expect actual |
| 124 | ) |
| 125 | ' |
| 126 | |
| 127 | test_expect_success 'log --stat matches with and without the store' ' |
| 128 | no_store log --stat >expect && |
| 129 | warm && |
| 130 | git log --stat >actual && |
| 131 | test_cmp expect actual |
| 132 | ' |
| 133 | |
| 134 | test_expect_success 'log --numstat and --shortstat match' ' |
| 135 | no_store log --numstat >expect_num && |
| 136 | no_store log --shortstat >expect_short && |
| 137 | warm && |
| 138 | git log --numstat >actual_num && |
| 139 | git log --shortstat >actual_short && |
| 140 | test_cmp expect_num actual_num && |
| 141 | test_cmp expect_short actual_short |
| 142 | ' |
| 143 | |
| 144 | # A built store must reproduce diffstat output at every context |
| 145 | # length. Only trim-stable pairs are recorded, so one entry serves |
| 146 | # every context; a trim-divergent pair is never recorded and always |
| 147 | # computed. Zero context is where trim_common_tail runs, which is |
| 148 | # what makes the two diffs differ. |
| 149 | test_expect_success 'diffstat matches at several context lengths' ' |
| 150 | no_store log --stat >expect_def && |
| 151 | no_store log -U0 --stat >expect_u0 && |
| 152 | no_store log -U7 --stat >expect_u7 && |
| 153 | warm && |
| 154 | git log --stat >got_def && |
| 155 | git log -U0 --stat >got_u0 && |
| 156 | git log -U7 --stat >got_u7 && |
| 157 | test_cmp expect_def got_def && |
| 158 | test_cmp expect_u0 got_u0 && |
| 159 | test_cmp expect_u7 got_u7 |
| 160 | ' |
| 161 | |
| 162 | test_expect_success 'store built at a nonzero context stays correct at that context' ' |
| 163 | no_store -c diff.context=5 log --stat >expect && |
| 164 | warm -c diff.context=5 && |
| 165 | git -c diff.context=5 log --stat >actual && |
| 166 | test_cmp expect actual |
| 167 | ' |
| 168 | |
| 169 | # This blob pair (a real git test file being modernized) has different |
| 170 | # valid diffs at different contexts: at zero context, where |
| 171 | # trim_common_tail runs, "diff -U0" reports 9/6, while "diff -U3" |
| 172 | # reports 10/7. Such a trim-divergent pair is exactly what the writer |
| 173 | # must never record, since no single entry could serve both readers. |
| 174 | # A compact synthetic pair cannot show this count split: on small |
| 175 | # inputs xdiff produces minimal diffs, minimal diffs of one pair all |
| 176 | # add and delete the same number of lines, and trimming the common |
| 177 | # tail preserves minimality, so the counts agree by construction (a |
| 178 | # search over thousands of synthetic pairs up to 8 lines found no |
| 179 | # split). The split needs the cost-capping heuristics that only larger |
| 180 | # inputs trigger, so the pair is shipped as a fixture under t4220/. |
| 181 | test_expect_success 'a trim-divergent file is correct at each context' ' |
| 182 | cp "$TEST_DIRECTORY/t4220/trim-divergent-old" div.sh && |
| 183 | git add div.sh && |
| 184 | git commit -m divergent-old && |
| 185 | cp "$TEST_DIRECTORY/t4220/trim-divergent-new" div.sh && |
| 186 | git add div.sh && |
| 187 | git commit -m divergent-new && |
| 188 | no_store log -1 --format= --stat -- div.sh >expect_def && |
| 189 | no_store log -1 --format= -U0 --stat -- div.sh >expect_u0 && |
| 190 | warm && |
| 191 | git log -1 --format= --stat -- div.sh >got_def && |
| 192 | git log -1 --format= -U0 --stat -- div.sh >got_u0 && |
| 193 | test_cmp expect_def got_def && |
| 194 | test_cmp expect_u0 got_u0 && |
| 195 | # The fixture must actually diverge, or the test would pass without |
| 196 | # exercising the split; fail loudly if a diff change ever levels it. |
| 197 | ! test_cmp expect_def expect_u0 |
| 198 | ' |
| 199 | |
| 200 | # A warming run displays the diffstat it computes. At zero context xdi_diff |
| 201 | # trims, so the displayed counts must be the trimmed ones (what a store-less |
| 202 | # run shows), not the untrimmed ones the writer compares against when it |
| 203 | # decides whether the pair is stable enough to record. |
| 204 | test_expect_success 'warming --stat at zero context matches a store-less run' ' |
| 205 | git init -q warm-u0 && |
| 206 | ( |
| 207 | cd warm-u0 && |
| 208 | cp "$TEST_DIRECTORY/t4220/trim-divergent-old" div.sh && |
| 209 | git add div.sh && git commit -q -m old && |
| 210 | cp "$TEST_DIRECTORY/t4220/trim-divergent-new" div.sh && |
| 211 | git add div.sh && git commit -q -m new && |
| 212 | git -c core.diffhunks=false log -1 --format= -U0 --stat -- div.sh >expect && |
| 213 | GIT_DIFF_HUNKS_WRITE=1 git log -1 --format= -U0 --stat -- div.sh >got && |
| 214 | test_cmp expect got |
| 215 | ) |
| 216 | ' |
| 217 | |
| 218 | test_expect_success 'diff --stat matches with and without the store, both directions' ' |
| 219 | no_store diff --stat second fourth >expect_fwd && |
| 220 | no_store diff --stat fourth second >expect_rev && |
| 221 | warm && |
| 222 | git diff --stat second fourth >got_fwd && |
| 223 | git diff --stat fourth second >got_rev && |
| 224 | test_cmp expect_fwd got_fwd && |
| 225 | test_cmp expect_rev got_rev |
| 226 | ' |
| 227 | |
| 228 | test_expect_success 'show and diff-tree --stat use the store' ' |
| 229 | test_when_finished "git diff-hunks clear" && |
| 230 | # diff_hunks_attach() runs for show and diff-tree: a write-enabled |
| 231 | # --stat records into the store (without the attach there is no |
| 232 | # writer, so nothing is written). |
| 233 | git diff-hunks clear && |
| 234 | GIT_DIFF_HUNKS_WRITE=1 git show --stat fourth >/dev/null && |
| 235 | test_path_is_file "$STORE" && |
| 236 | git diff-hunks clear && |
| 237 | GIT_DIFF_HUNKS_WRITE=1 git diff-tree --stat fourth >/dev/null && |
| 238 | test_path_is_file "$STORE" && |
| 239 | # Reading never changes their output. |
| 240 | git diff-hunks clear && |
| 241 | no_store show --stat fourth >expect_show && |
| 242 | no_store diff-tree --stat fourth >expect_dt && |
| 243 | warm && |
| 244 | git show --stat fourth >got_show && |
| 245 | git diff-tree --stat fourth >got_dt && |
| 246 | test_cmp expect_show got_show && |
| 247 | test_cmp expect_dt got_dt |
| 248 | ' |
| 249 | |
| 250 | test_expect_success 'log -R --stat matches (reversed pairs keyed apart)' ' |
| 251 | no_store log -R --stat >expect && |
| 252 | warm && |
| 253 | git log -R --stat >actual && |
| 254 | test_cmp expect actual |
| 255 | ' |
| 256 | |
| 257 | # One warm serves both diffstat and blame: the blob pairs a blame |
| 258 | # walks are the same parent-child pairs the diffstat warm recorded. |
| 259 | test_expect_success 'a single warming run serves both blame and diffstat' ' |
| 260 | warm && |
| 261 | git blame --show-stats file.txt >out 2>&1 && |
| 262 | test_grep "num precomputed hits: [1-9][0-9]*" out |
| 263 | ' |
| 264 | |
| 265 | # The diffstat read path produces identical output on a hit or a miss, so |
| 266 | # it emits a trace2 "read-hits" count to prove it consulted the store. |
| 267 | test_expect_success 'diffstat consults the store (trace shows read hits)' ' |
| 268 | warm && |
| 269 | GIT_TRACE2_EVENT="$PWD/trace_on.json" git log --stat >/dev/null && |
| 270 | test_grep read-hits trace_on.json && |
| 271 | test_env GIT_TRACE2_EVENT="$PWD/trace_off.json" no_store log --stat >/dev/null && |
| 272 | test_grep ! read-hits trace_off.json |
| 273 | ' |
| 274 | |
| 275 | test_expect_success 'blame matches with and without the store' ' |
| 276 | no_store blame file.txt >expect && |
| 277 | warm && |
| 278 | git blame file.txt >actual && |
| 279 | test_cmp expect actual |
| 280 | ' |
| 281 | |
| 282 | test_expect_success 'blame --porcelain and --incremental match' ' |
| 283 | no_store blame --porcelain file.txt >expect_p && |
| 284 | no_store blame --incremental file.txt >expect_i && |
| 285 | warm && |
| 286 | git blame --porcelain file.txt >got_p && |
| 287 | git blame --incremental file.txt >got_i && |
| 288 | test_cmp expect_p got_p && |
| 289 | test_cmp expect_i got_i |
| 290 | ' |
| 291 | |
| 292 | # Diff settings that change hunks but are not part of the store key must |
| 293 | # bypass it in both directions, so output stays byte-identical to a |
| 294 | # store-less run. |
| 295 | test_expect_success 'setup ignore fixture' ' |
| 296 | git init ignore-repo && |
| 297 | ( |
| 298 | cd ignore-repo && |
| 299 | test_write_lines code keep "# c" >f && |
| 300 | git add f && |
| 301 | git commit -m c1 && |
| 302 | test_write_lines codeCH keep "# cX" >f && |
| 303 | git add f && |
| 304 | git commit -m c2 && |
| 305 | warm |
| 306 | ) |
| 307 | ' |
| 308 | |
| 309 | # Output parity alone cannot prove the guard: served counts can |
| 310 | # coincide with computed ones, so each bypass below also asserts the |
| 311 | # consultation itself (no read hit with the option, a hit without it) |
| 312 | # and that a warming run under the option records nothing. |
| 313 | test_expect_success '-I bypasses the store in both directions' ' |
| 314 | ( |
| 315 | cd ignore-repo && |
| 316 | no_store diff -I"^#" --numstat HEAD~ HEAD >expect && |
| 317 | git diff -I"^#" --numstat HEAD~ HEAD >actual && |
| 318 | test_cmp expect actual && |
| 319 | # -I does not change the key, so only the ignore_regex |
| 320 | # guard keeps the warmed entry from serving here. |
| 321 | GIT_TRACE2_EVENT="$PWD/trace_i.json" \ |
| 322 | git diff -I"^#" --numstat HEAD~ HEAD >/dev/null && |
| 323 | test_grep ! read-hits trace_i.json && |
| 324 | GIT_TRACE2_EVENT="$PWD/trace_i_ctl.json" \ |
| 325 | git diff --numstat HEAD~ HEAD >/dev/null && |
| 326 | test_grep read-hits trace_i_ctl.json && |
| 327 | git diff-hunks clear && |
| 328 | GIT_DIFF_HUNKS_WRITE=1 git diff -I"^#" --numstat HEAD~ HEAD >/dev/null && |
| 329 | test_path_is_missing .git/objects/info/diff-hunks && |
| 330 | # Restore the warmed fixture for the tests below. |
| 331 | warm |
| 332 | ) |
| 333 | ' |
| 334 | |
| 335 | test_expect_success '-B bypasses the store in both directions' ' |
| 336 | git init break-repo && |
| 337 | ( |
| 338 | cd break-repo && |
| 339 | test_write_lines a b c d e f g h >f && |
| 340 | git add f && |
| 341 | git commit -m orig && |
| 342 | test_write_lines 1 2 3 4 5 6 7 8 >f && |
| 343 | git add f && |
| 344 | git commit -m rewrite && |
| 345 | warm && |
| 346 | no_store diff -B --stat HEAD~ HEAD >expect && |
| 347 | git diff -B --stat HEAD~ HEAD >actual && |
| 348 | test_cmp expect actual && |
| 349 | GIT_TRACE2_EVENT="$PWD/trace_b.json" \ |
| 350 | git diff -B --stat HEAD~ HEAD >/dev/null && |
| 351 | test_grep ! read-hits trace_b.json && |
| 352 | GIT_TRACE2_EVENT="$PWD/trace_ctl.json" \ |
| 353 | git diff --stat HEAD~ HEAD >/dev/null && |
| 354 | test_grep read-hits trace_ctl.json && |
| 355 | git diff-hunks clear && |
| 356 | GIT_DIFF_HUNKS_WRITE=1 git diff -B --stat HEAD~ HEAD >/dev/null && |
| 357 | test_path_is_missing .git/objects/info/diff-hunks |
| 358 | ) |
| 359 | ' |
| 360 | |
| 361 | test_expect_success '--anchored bypasses the store in both directions' ' |
| 362 | ( |
| 363 | cd ignore-repo && |
| 364 | no_store diff --stat --anchored=keep HEAD~ HEAD >expect && |
| 365 | git diff --stat --anchored=keep HEAD~ HEAD >actual && |
| 366 | test_cmp expect actual && |
| 367 | # Anchors do not change the key, so only the anchors guard |
| 368 | # keeps the warmed entry from serving here. |
| 369 | GIT_TRACE2_EVENT="$PWD/trace_anchor.json" \ |
| 370 | git diff --stat --anchored=keep HEAD~ HEAD >/dev/null && |
| 371 | test_grep ! read-hits trace_anchor.json && |
| 372 | GIT_TRACE2_EVENT="$PWD/trace_plain.json" \ |
| 373 | git diff --stat HEAD~ HEAD >/dev/null && |
| 374 | test_grep read-hits trace_plain.json && |
| 375 | git diff-hunks clear && |
| 376 | GIT_DIFF_HUNKS_WRITE=1 \ |
| 377 | git diff --stat --anchored=keep HEAD~ HEAD >/dev/null && |
| 378 | test_path_is_missing .git/objects/info/diff-hunks |
| 379 | ) |
| 380 | ' |
| 381 | |
| 382 | test_expect_success '--ignore-blank-lines bypasses the store in both directions' ' |
| 383 | git init ibl-repo && |
| 384 | ( |
| 385 | cd ibl-repo && |
| 386 | printf "a\n\nx\ny\nb\n" >f && |
| 387 | git add f && |
| 388 | git commit -m v1 && |
| 389 | printf "a\nx\ny\nB\n" >f && |
| 390 | git add f && |
| 391 | git commit -m v2 && |
| 392 | warm && |
| 393 | no_store diff --stat --ignore-blank-lines HEAD~ HEAD >expect && |
| 394 | git diff --stat --ignore-blank-lines HEAD~ HEAD >actual && |
| 395 | test_cmp expect actual && |
| 396 | # The flag is an xdl_opts bit and thus part of the key; the |
| 397 | # stat consumer excludes it before consulting at all. |
| 398 | GIT_TRACE2_EVENT="$PWD/trace_ibl.json" \ |
| 399 | git diff --stat --ignore-blank-lines HEAD~ HEAD >/dev/null && |
| 400 | test_grep ! read-hits trace_ibl.json && |
| 401 | GIT_TRACE2_EVENT="$PWD/trace_plain.json" \ |
| 402 | git diff --stat HEAD~ HEAD >/dev/null && |
| 403 | test_grep read-hits trace_plain.json && |
| 404 | git diff-hunks clear && |
| 405 | GIT_DIFF_HUNKS_WRITE=1 \ |
| 406 | git diff --stat --ignore-blank-lines HEAD~ HEAD >/dev/null && |
| 407 | test_path_is_missing .git/objects/info/diff-hunks |
| 408 | ) |
| 409 | ' |
| 410 | |
| 411 | test_expect_success 'a whitespace-ignoring diff is not served default entries' ' |
| 412 | git init ws-repo && |
| 413 | ( |
| 414 | cd ws-repo && |
| 415 | test_write_lines alpha beta gamma >f && |
| 416 | git add f && |
| 417 | git commit -m c1 && |
| 418 | test_write_lines " alpha" beta gamma delta >f && |
| 419 | git add f && |
| 420 | git commit -m c2 && |
| 421 | warm && |
| 422 | no_store diff -w --numstat HEAD~ HEAD >expect && |
| 423 | git diff -w --numstat HEAD~ HEAD >actual && |
| 424 | test_cmp expect actual |
| 425 | ) |
| 426 | ' |
| 427 | |
| 428 | test_expect_success 'blame -w stays correct and does not hit default entries' ' |
| 429 | ( |
| 430 | cd ws-repo && |
| 431 | no_store blame -w f >expect && |
| 432 | git blame -w --show-stats f >out 2>&1 && |
| 433 | test_grep "num precomputed hits: 0" out && |
| 434 | git blame -w f >actual && |
| 435 | test_cmp expect actual |
| 436 | ) |
| 437 | ' |
| 438 | |
| 439 | test_expect_success 'blame with indentHeuristic off stays correct and misses' ' |
| 440 | warm && |
| 441 | git -c diff.indentHeuristic=false blame --show-stats file.txt >out 2>&1 && |
| 442 | test_grep "num precomputed hits: 0" out && |
| 443 | no_store -c diff.indentHeuristic=false blame file.txt >expect && |
| 444 | git -c diff.indentHeuristic=false blame file.txt >actual && |
| 445 | test_cmp expect actual |
| 446 | ' |
| 447 | |
| 448 | test_expect_success 'a driver algorithm override keeps output correct and keys apart' ' |
| 449 | git init driver-algo && |
| 450 | ( |
| 451 | cd driver-algo && |
| 452 | echo "file.foo diff=foo" >.gitattributes && |
| 453 | git add .gitattributes && |
| 454 | git commit -m attributes && |
| 455 | test_write_lines 1 2 3 4 5 >file.foo && |
| 456 | git add file.foo && |
| 457 | git commit -m one && |
| 458 | test_write_lines 1 2 X 4 5 6 >file.foo && |
| 459 | git add file.foo && |
| 460 | git commit -m two && |
| 461 | warm -c diff.foo.algorithm=histogram && |
| 462 | no_store -c diff.foo.algorithm=histogram log --stat >expect && |
| 463 | git -c diff.foo.algorithm=histogram log --stat >actual && |
| 464 | test_cmp expect actual && |
| 465 | # The driver algorithm is an xdl_opts key bit: entries |
| 466 | # warmed at the default settings must not serve a |
| 467 | # driver-forced histogram read, and output stays correct. |
| 468 | git diff-hunks clear && |
| 469 | warm && |
| 470 | no_store -c diff.foo.algorithm=histogram log --stat >expect2 && |
| 471 | git -c diff.foo.algorithm=histogram log --stat >actual2 && |
| 472 | test_cmp expect2 actual2 && |
| 473 | GIT_TRACE2_EVENT="$PWD/trace_algo.json" \ |
| 474 | git -c diff.foo.algorithm=histogram log --stat >/dev/null && |
| 475 | test_grep ! read-hits trace_algo.json && |
| 476 | GIT_TRACE2_EVENT="$PWD/trace_algo_ctl.json" \ |
| 477 | git log --stat >/dev/null && |
| 478 | test_grep read-hits trace_algo_ctl.json |
| 479 | ) |
| 480 | ' |
| 481 | |
| 482 | test_expect_success 'blame --reverse never consults the store' ' |
| 483 | warm && |
| 484 | git blame --reverse HEAD~3..HEAD file.txt >actual 2>/dev/null && |
| 485 | no_store blame --reverse HEAD~3..HEAD file.txt >expect 2>/dev/null && |
| 486 | test_cmp expect actual && |
| 487 | # Reverse blame withholds the pair identity. Zero hits alone |
| 488 | # cannot prove that: reverse pairs are never warmed, so a |
| 489 | # consulted pair would miss, not hit. Zero misses is what shows |
| 490 | # the store was never consulted. |
| 491 | git blame --reverse --show-stats HEAD~3..HEAD file.txt \ |
| 492 | >stats 2>/dev/null && |
| 493 | test_grep "num precomputed hits: 0" stats && |
| 494 | test_grep "num precomputed misses: 0" stats |
| 495 | ' |
| 496 | |
| 497 | test_expect_success 'blame with a textconv driver bypasses the store' ' |
| 498 | echo "tc.txt diff=tc" >>.gitattributes && |
| 499 | git add .gitattributes && |
| 500 | git commit -m tc-attr && |
| 501 | git config diff.tc.textconv "sed -e s/1/one/" && |
| 502 | test_commit tc1 tc.txt "line 1" && |
| 503 | test_commit tc2 tc.txt "line 1 |
| 504 | line 2" && |
| 505 | warm && |
| 506 | git blame --show-stats tc.txt >out 2>&1 && |
| 507 | test_grep "num precomputed hits: 0" out && |
| 508 | no_store blame tc.txt >expect && |
| 509 | git blame tc.txt >actual && |
| 510 | test_cmp expect actual |
| 511 | ' |
| 512 | |
| 513 | test_expect_success 'a replaced blob makes the store step aside' ' |
| 514 | git init replace-repo && |
| 515 | ( |
| 516 | cd replace-repo && |
| 517 | test_commit r1 f.txt "a" && |
| 518 | test_commit r2 f.txt "a |
| 519 | b" && |
| 520 | warm && |
| 521 | # Control: without a replacement the pair is served. |
| 522 | GIT_TRACE2_EVENT="$PWD/trace_ctl.json" \ |
| 523 | git log -1 --format= --numstat -- f.txt >/dev/null && |
| 524 | test_grep read-hits trace_ctl.json && |
| 525 | # Replace r2 blob: the diff now reads different content |
| 526 | # (through OBJECT_INFO_LOOKUP_REPLACE) under the id the store |
| 527 | # keyed, so a served answer would be the pre-replacement diff. |
| 528 | # Identity is withheld, the store steps aside, and the builtin |
| 529 | # computes from the replaced content. |
| 530 | new_blob=$(git rev-parse HEAD:f.txt) && |
| 531 | repl=$(printf "a\nB\nC\nD\n" | git hash-object -w --stdin) && |
| 532 | git replace "$new_blob" "$repl" && |
| 533 | no_store log -1 --format= --numstat -- f.txt >expect && |
| 534 | git log -1 --format= --numstat -- f.txt >actual && |
| 535 | test_cmp expect actual && |
| 536 | GIT_TRACE2_EVENT="$PWD/trace_repl.json" \ |
| 537 | git log -1 --format= --numstat -- f.txt >/dev/null && |
| 538 | test_grep ! read-hits trace_repl.json |
| 539 | ) |
| 540 | ' |
| 541 | |
| 542 | test_expect_success 'blame -M and -C stay correct with the store' ' |
| 543 | warm && |
| 544 | no_store blame -M file.txt >expect_m && |
| 545 | no_store blame -C file.txt >expect_c && |
| 546 | git blame -M file.txt >got_m && |
| 547 | git blame -C file.txt >got_c && |
| 548 | test_cmp expect_m got_m && |
| 549 | test_cmp expect_c got_c |
| 550 | ' |
| 551 | |
| 552 | # Copy-detecting (and reverse) blame still diff blob pairs through |
| 553 | # pass_blame_to_parent, so they must use the real blame xdl_opts. A |
| 554 | # whitespace-only change is invisible under -w; if -w were dropped on |
| 555 | # these paths the -w and non-w results would coincide. |
| 556 | test_expect_success 'blame -C honors -w' ' |
| 557 | git init -q blame-cw && |
| 558 | ( |
| 559 | cd blame-cw && |
| 560 | printf "one\ntwo\nthree\n" >f && |
| 561 | git add f && git commit -q -m base && |
| 562 | printf "one\n two \nthree\n" >f && |
| 563 | git add f && git commit -q -m reindent && |
| 564 | git blame -C -w f >with_w && |
| 565 | git blame -C f >without_w && |
| 566 | ! test_cmp with_w without_w |
| 567 | ) |
| 568 | ' |
| 569 | |
| 570 | # Cover the pair shapes an object walk encounters: binary and |
| 571 | # mode-only changes produce no text hunks to record. |
| 572 | test_expect_success 'binary and mode-only changes do not break the writer' ' |
| 573 | printf "\\000\\001\\002" >bin.dat && |
| 574 | git add bin.dat && |
| 575 | git commit -m binary-1 && |
| 576 | printf "\\000\\001\\003\\004" >bin.dat && |
| 577 | git add bin.dat && |
| 578 | git commit -m binary-2 && |
| 579 | echo "mode content" >mode.txt && |
| 580 | git add mode.txt && |
| 581 | git commit -m mode-1 && |
| 582 | test_chmod +x mode.txt && |
| 583 | git commit -m mode-2 && |
| 584 | no_store log --stat >expect && |
| 585 | warm && |
| 586 | git log --stat >actual && |
| 587 | test_cmp expect actual |
| 588 | ' |
| 589 | |
| 590 | test_expect_success 'blame across a rename matches' ' |
| 591 | echo "original content" >rename-src.txt && |
| 592 | git add rename-src.txt && |
| 593 | git commit -m "add rename-src" && |
| 594 | echo "more" >>rename-src.txt && |
| 595 | git add rename-src.txt && |
| 596 | git commit -m "modify rename-src" && |
| 597 | git mv rename-src.txt rename-dst.txt && |
| 598 | git commit -m "rename" && |
| 599 | echo "post" >>rename-dst.txt && |
| 600 | git add rename-dst.txt && |
| 601 | git commit -m "modify after rename" && |
| 602 | no_store blame rename-dst.txt >expect && |
| 603 | warm && |
| 604 | git blame rename-dst.txt >actual && |
| 605 | test_cmp expect actual |
| 606 | ' |
| 607 | |
| 608 | test_expect_success 'blame handles merge commits' ' |
| 609 | git checkout -b merge-side main~2 && |
| 610 | test_commit merge-change merge-file.txt "side content" && |
| 611 | git checkout main && |
| 612 | git merge --no-edit merge-side && |
| 613 | no_store blame merge-file.txt >expect && |
| 614 | warm && |
| 615 | git blame merge-file.txt >actual && |
| 616 | test_cmp expect actual |
| 617 | ' |
| 618 | |
| 619 | test_expect_success 'distinct --contents against one revision do not collide' ' |
| 620 | warm && |
| 621 | test_write_lines "line 1" "appended line" >c1 && |
| 622 | test_write_lines "rewritten line" >c2 && |
| 623 | # Ground truth without the store. |
| 624 | no_store blame -s --contents=c2 file.txt initial >expect && |
| 625 | # With the store, an intervening c1 run must not poison the c2 lookup. |
| 626 | git blame -s --contents=c1 file.txt initial >/dev/null && |
| 627 | git blame -s --contents=c2 file.txt initial >actual && |
| 628 | test_cmp expect actual && |
| 629 | # The --contents side is a working-tree pseudo-commit (a null commit |
| 630 | # id), so its pairs withhold identity and never consult the store. |
| 631 | # Output parity alone cannot show that: a consulted unwarmed pair |
| 632 | # would miss, not hit, so zero misses is what proves the pair was |
| 633 | # never looked up. |
| 634 | git blame -s --show-stats --contents=c2 file.txt initial >stats 2>&1 && |
| 635 | test_grep "num precomputed hits: 0" stats && |
| 636 | test_grep "num precomputed misses: 0" stats |
| 637 | ' |
| 638 | |
| 639 | test_expect_success 'blame --ignore-rev bypasses the store for ignored pairs' ' |
| 640 | git init ignore-rev-repo && |
| 641 | ( |
| 642 | cd ignore-rev-repo && |
| 643 | test_commit ir1 f.txt "base" && |
| 644 | test_commit ir2 f.txt "base |
| 645 | more" && |
| 646 | warm && |
| 647 | # Control: the ordinary pass is served, nothing is computed. |
| 648 | git blame --show-stats f.txt >ctl 2>&1 && |
| 649 | test_grep "num precomputed hits: 1" ctl && |
| 650 | test_grep "num get patch: 0" ctl && |
| 651 | no_store blame --ignore-rev ir2 f.txt >expect && |
| 652 | git blame --ignore-rev ir2 f.txt >actual && |
| 653 | test_cmp expect actual && |
| 654 | # The ignored revision adds a pass that withholds identity: |
| 655 | # it computes its diff (get patch rises) instead of being |
| 656 | # served or even counted as a store consultation. |
| 657 | git blame --ignore-rev ir2 --show-stats f.txt >stats 2>&1 && |
| 658 | test_grep "num precomputed hits: 1" stats && |
| 659 | test_grep "num precomputed misses: 0" stats && |
| 660 | test_grep "num get patch: 1" stats |
| 661 | ) |
| 662 | ' |
| 663 | |
| 664 | test_expect_success 'blame counts misses for pairs the store does not hold' ' |
| 665 | ( |
| 666 | cd ignore-rev-repo && |
| 667 | test_commit ir3 f.txt "base |
| 668 | more |
| 669 | third" && |
| 670 | git blame --show-stats f.txt >stats 2>&1 && |
| 671 | test_grep "num precomputed hits: 1" stats && |
| 672 | test_grep "num precomputed misses: 1" stats |
| 673 | ) |
| 674 | ' |
| 675 | |
| 676 | test_expect_success 'log -L --stat neither reads nor records' ' |
| 677 | warm && |
| 678 | GIT_TRACE2_EVENT="$PWD/trace_linelog.json" \ |
| 679 | git log -L1,1:file.txt --stat >/dev/null && |
| 680 | test_grep ! read-hits trace_linelog.json && |
| 681 | git diff-hunks clear && |
| 682 | GIT_DIFF_HUNKS_WRITE=1 git log -L1,1:file.txt --stat >/dev/null && |
| 683 | test_path_is_missing $STORE |
| 684 | ' |
| 685 | |
| 686 | # Integrity: a structurally broken header is read as absent (the reader |
| 687 | # falls back to xdiff and stays correct); a checksum mismatch is caught |
| 688 | # by verify, which is when integrity is checked. |
| 689 | test_expect_success 'a truncated store is read as absent' ' |
| 690 | warm && |
| 691 | test_copy_bytes 20 <$STORE >truncated && |
| 692 | mv truncated $STORE && |
| 693 | no_store blame file.txt >expect && |
| 694 | git blame file.txt >actual && |
| 695 | test_cmp expect actual |
| 696 | ' |
| 697 | |
| 698 | test_expect_success 'a corrupt signature is read as absent' ' |
| 699 | warm && |
| 700 | printf "XXXX" >corrupt && |
| 701 | tail -c +5 <$STORE >>corrupt && |
| 702 | mv corrupt $STORE && |
| 703 | no_store blame file.txt >expect && |
| 704 | git blame file.txt >actual && |
| 705 | test_cmp expect actual |
| 706 | ' |
| 707 | |
| 708 | # Byte 6 of the header is the chunk count; a value larger than the file |
| 709 | # can hold must be rejected before the chunk table is walked. |
| 710 | test_expect_success 'an over-claimed chunk count is read as absent' ' |
| 711 | warm && |
| 712 | printf "\377" | dd of=$STORE bs=1 seek=6 count=1 conv=notrunc 2>/dev/null && |
| 713 | no_store blame file.txt >expect && |
| 714 | git blame file.txt >actual && |
| 715 | test_cmp expect actual |
| 716 | ' |
| 717 | |
| 718 | # A record with no hunks would replay as an equivalence claim, which |
| 719 | # the writer never records; the reader must treat such a record as a |
| 720 | # miss and recompute, and verify must flag it. |
| 721 | test_expect_success 'a zero-hunk record is read as a miss and fails verify' ' |
| 722 | git init zero-hunk && |
| 723 | ( |
| 724 | cd zero-hunk && |
| 725 | test_commit z1 f.txt "base" && |
| 726 | test_commit z2 f.txt "base |
| 727 | more" && |
| 728 | warm && |
| 729 | # The store holds one entry of one hunk: a 4-byte count and |
| 730 | # one 16-byte hunk record, just before the trailing |
| 731 | # checksum. Zero the count to craft the record the writer |
| 732 | # refuses to produce. |
| 733 | rawsz=$(test_oid rawsz) && |
| 734 | fsize=$(test_file_size $STORE) && |
| 735 | printf "\\0\\0\\0\\0" | dd of=$STORE bs=1 \ |
| 736 | seek=$((fsize - rawsz - 20)) count=4 conv=notrunc \ |
| 737 | 2>/dev/null && |
| 738 | no_store blame f.txt >expect && |
| 739 | git blame --show-stats f.txt >stats 2>&1 && |
| 740 | test_grep "num precomputed hits: 0" stats && |
| 741 | git blame f.txt >actual && |
| 742 | test_cmp expect actual && |
| 743 | test_must_fail git diff-hunks verify |
| 744 | ) |
| 745 | ' |
| 746 | |
| 747 | test_expect_success 'verify succeeds on a valid store and on an absent one' ' |
| 748 | warm && |
| 749 | git diff-hunks verify && |
| 750 | git diff-hunks clear && |
| 751 | test_path_is_missing $STORE && |
| 752 | git diff-hunks verify |
| 753 | ' |
| 754 | |
| 755 | test_expect_success 'verify detects a checksum mismatch' ' |
| 756 | test_when_finished "git diff-hunks clear" && |
| 757 | warm && |
| 758 | fsize=$(test_file_size $STORE) && |
| 759 | mid=$((fsize / 2)) && |
| 760 | printf "\\377" | dd of=$STORE bs=1 seek=$mid count=1 conv=notrunc 2>/dev/null && |
| 761 | test_must_fail git diff-hunks verify |
| 762 | ' |
| 763 | |
| 764 | test_expect_success 'a warm discards a corrupt store rather than seeding from it' ' |
| 765 | test_when_finished "git diff-hunks clear" && |
| 766 | warm && |
| 767 | # Corrupt the checksum: the next warm must not carry the corrupt |
| 768 | # entries forward into a fresh checksum-valid file; it discards |
| 769 | # them (with a warning) and rewrites a store that verifies. |
| 770 | fsize=$(test_file_size $STORE) && |
| 771 | printf "\\377" | dd of=$STORE bs=1 seek=$((fsize / 2)) count=1 conv=notrunc 2>/dev/null && |
| 772 | warm 2>err && |
| 773 | test_grep "failed its checksum" err && |
| 774 | git diff-hunks verify && |
| 775 | no_store log --stat >expect && |
| 776 | git log --stat >actual && |
| 777 | test_cmp expect actual |
| 778 | ' |
| 779 | |
| 780 | # A generated patch must carry the builtin diffstat, not one served from |
| 781 | # the sender's local store, so its counts do not depend on whether the |
| 782 | # sender warmed the store. Poison the store so a served answer diverges |
| 783 | # from the builtin, then confirm format-patch shows the builtin counts. |
| 784 | test_expect_success 'format-patch keeps its diffstat off the store' ' |
| 785 | git init fp-repo && |
| 786 | ( |
| 787 | cd fp-repo && |
| 788 | test_commit p1 f.txt "a" && |
| 789 | test_commit p2 f.txt "a |
| 790 | b" && |
| 791 | warm && |
| 792 | # Bump the new-side count of the single recorded hunk. The |
| 793 | # record stays structurally valid, and a read skips the |
| 794 | # trailing checksum, so the store serves this poisoned count. |
| 795 | rawsz=$(test_oid rawsz) && |
| 796 | fsize=$(test_file_size .git/objects/info/diff-hunks) && |
| 797 | printf "\\0\\0\\0\\7" | dd of=.git/objects/info/diff-hunks bs=1 \ |
| 798 | seek=$((fsize - rawsz - 4)) count=4 conv=notrunc 2>/dev/null && |
| 799 | # The store now serves a divergent count, proving the poison |
| 800 | # is live and observable through a store consumer. |
| 801 | printf "7\t0\tf.txt\n" >poisoned && |
| 802 | git log -1 --format= --numstat -- f.txt >served && |
| 803 | test_cmp poisoned served && |
| 804 | # format-patch does not consult the store, so its output is |
| 805 | # identical with the store poisoned and with it disabled. |
| 806 | no_store format-patch -1 --stdout --stat -- f.txt >expect && |
| 807 | git format-patch -1 --stdout --stat -- f.txt >actual && |
| 808 | test_cmp expect actual |
| 809 | ) |
| 810 | ' |
| 811 | |
| 812 | test_expect_success 'diff-hunks clear removes the store file' ' |
| 813 | warm && |
| 814 | test_path_is_file $STORE && |
| 815 | git diff-hunks clear && |
| 816 | test_path_is_missing $STORE |
| 817 | ' |
| 818 | |
| 819 | test_done |