Raw
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