diff: read precomputed hunks for stat output

Teach builtin_diffstat() to consult the hunk provider interface through diff_provider_consult(), new here: the consult-only entry that answers without loading content or computing, so it never returns DIFF_PROVIDER_ERROR. On an answer, the summing callback accumulates the provided counts directly into the diffstat entry; the blobs were already loaded for the binary check, so an answer saves the diff run, not the content load (blame, taught next, skips its loads too). On an unanswered outcome it computes as before and, with a writer attached, records what it computed; on unanswered-no-record it computes without recording. The provider behind the consult is the diff-hunks store, registered in front of the terminal builtin computation. Its consult serves a recorded pair through diff_hunks_replay(), which validates the sequence before any hunk reaches the callback, so direct accumulation is safe. The request gains the pair's object ids and the diff options read by the exclusions below. A side whose bytes are not a stored blob, such as a working-tree file or a gitlink, has a NULL id; the store passes it by and the terminal provider computes it. diff_provider_emit_hunks() walks the same chain, so blame's requests follow these rules the moment blame supplies identity. The walk also insists, as a BUG check, that a request's diff options belong to the repository whose chain it walks. Each exclusion lives with the provider whose key cannot express it. -I patterns and --anchored shape the diff outside the store key, and break detection (-B) rescores the pair outside it; the store's consult maps all three to stop-no-record, so such a request is neither served nor recorded for any consumer. The consumer-side guard the recording commit carried for those three comes out here. The compile-time assert on xpparam_t's layout sits next to that decision, forcing an explicit keying decision whenever a diff parameter is added. The stat consumer keeps only the exclusion that is not about the key: --ignore-blank-lines is part of the key but coalesces hunks differently between the text-emitting and coordinate-callback paths, so the consumer returns before consulting. A "log -L" range-scoped stat neither reads nor records; the line-range filter computes it as before. "git diff", "git log", "git show", and "git diff-tree" with the --stat, --numstat, and --shortstat formats consult the interface. Reading is controlled by core.diffHunks. An answer is invisible in the output, so the store counts the pairs it serves and the consultations it cannot, and diff_hunks_read_stats() reports both; the stat path emits the hits as a trace2 "read-hits" datum for tests and tuning. The counters live on the store because only the store knows whether a consultation reached it, and none of its exclusion legs reaches the replay, so none counts as a miss. Extend t4220 with the read half: - output parity with and without the store, at several context lengths and both directions, and reversed pairs keying apart; - the consultation made visible through the read-hits datum, and the trim-divergent pair correct at every context; - the settings that must bypass the store doing so in both directions (-I, -B, --anchored, --ignore-blank-lines), asserted through the trace rather than output parity alone, which a coincidentally equal count could satisfy; - a driver-forced algorithm keying apart rather than bypassing: it is part of the key, so a read under it misses the default entries and a warm records under its own. A "log -L" range-scoped stat neither reads nor records. Signed-off-by: Michael Montalbo <mmontalbo@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael Montalbo committed Aug 1, 2026 at 10:41 UTC 02c68ee9cbdab50362281c72ba11be36385b8f93
9 files changed +539 -47
builtin/log.c
+7
@@ -2225,6 +2225,13 @@ int cmd_format_patch(int argc,
2225 if (argc > 1)
2226 die(_("unrecognized argument: %s"), argv[1]);
2227
2228 + /*
2229 + * A patch generated by format-patch carries the builtin diffstat,
2230 + * not one served from a local store, so its counts do not depend
2231 + * on whether the sender warmed the store.
2232 + */
2233 + rev.diffopt.flags.no_precomputed_hunks = 1;
2234 +
2235 if (rev.diffopt.output_format & DIFF_FORMAT_NAME)
2236 die(_("--name-only does not make sense"));
2237 if (rev.diffopt.output_format & DIFF_FORMAT_NAME_STATUS)
diff-hunks.c
+87 -1
@@ -26,6 +26,7 @@
26 #include "csum-file.h"
27 #include "diff-hunks.h"
28 #include "diff-provider-internal.h"
29 +#include "diff.h"
30 #include "gettext.h"
31 #include "hash.h"
32 #include "hashmap.h"
@@ -140,6 +141,10 @@ struct diff_hunks_store {
141 uint32_t num_entries;
142 const unsigned char *hdat;
143 size_t hdat_size;
144 +
145 + /* Consultation counters; see diff_hunks_read_stats(). */
146 + unsigned long read_hits;
147 + unsigned long read_misses;
148 };
149
150 static void free_store(struct diff_hunks_store *s)
@@ -256,6 +261,15 @@ struct diff_hunks_store *repo_diff_hunks_store(struct repository *r)
261 return r->objects->diff_hunks_store;
262 }
263
264 +void diff_hunks_read_stats(struct repository *r,
265 + unsigned long *hits, unsigned long *misses)
266 +{
267 + struct diff_hunks_store *s = repo_diff_hunks_store(r);
268 +
269 + *hits = s ? s->read_hits : 0;
270 + *misses = s ? s->read_misses : 0;
271 +}
272 +
273 void close_diff_hunks_store(struct object_database *o)
274 {
275 if (!o->diff_hunks_store)
@@ -413,18 +427,90 @@ int diff_hunks_replay(struct diff_hunks_store *s,
427 struct precomputed_entry e;
428 uint32_t i;
429
430 + if (!s)
431 + return 0;
432 if (!diff_hunks_store_get(s, old_oid, new_oid, xdl_opts, &e) ||
417 - !replayable_hunks(&e))
433 + !replayable_hunks(&e)) {
434 + s->read_misses++;
435 return 0;
436 + }
437 for (i = 0; i < e.num_hunks; i++) {
438 struct precomputed_hunk h;
439 nth_precomputed_hunk(&e, i, &h);
440 hunk_func(h.old_start, h.old_count,
441 h.new_start, h.new_count, cb_data);
442 }
443 + s->read_hits++;
444 return 1;
445 }
446
447 +/*
448 + * The store's consult implementation. The store is not
449 + * authoritative, so it serves a recorded pair or passes; what the
450 + * recording key cannot express, it excludes here with the
451 + * stop-no-record disposition. None of those legs reaches
452 + * diff_hunks_replay(), so none of them counts as a miss.
453 + */
454 +static enum diff_provider_disposition
455 +diff_hunks_store_consult(struct diff_provider *provider UNUSED,
456 + const struct diff_provider_request *req,
457 + diff_provider_fill_fn fill UNUSED,
458 + void *fill_data UNUSED,
459 + xdl_emit_hunk_consume_func_t hunk_cb, void *cb_data)
460 +{
461 + /*
462 + * xpparam_t is the consult's parameter input. Its flags are
463 + * the store key's xdl_opts; ignore_regex (-I) and anchors
464 + * (--anchored) shape the diff outside the key, so such a
465 + * request is neither served nor recorded.
466 + *
467 + * Adding an xpparam_t field fires this assert (its size no
468 + * longer matches the reference struct). To clear it: (1) add
469 + * the field to the reference struct below; then (2) decide how
470 + * it affects the key: make it part of the key, or exclude
471 + * diffs that use it here with the disposition below. The
472 + * assert only tracks size: a same-size reorder or a changed
473 + * field meaning slips past, so re-read the fields when it
474 + * fires.
475 + */
476 + (void)BUILD_ASSERT_OR_ZERO(sizeof(xpparam_t) == sizeof(struct {
477 + unsigned long flags;
478 + regex_t **ignore_regex;
479 + size_t ignore_regex_nr;
480 + char **anchors;
481 + size_t anchors_nr;
482 + }));
483 + if (req->xpp->ignore_regex_nr || req->xpp->anchors_nr)
484 + return DIFF_PROVIDER_DISP_STOP_NO_RECORD;
485 + /*
486 + * Break detection (-B) rescores the pair outside xpparam_t, so
487 + * it is outside the key for the same reason.
488 + */
489 + if (req->diffopt && req->diffopt->break_opt != -1)
490 + return DIFF_PROVIDER_DISP_STOP_NO_RECORD;
491 +
492 + if (!req->old_oid || !req->new_oid)
493 + return DIFF_PROVIDER_DISP_PASS;
494 + if (diff_hunks_replay(repo_diff_hunks_store(req->repo),
495 + req->old_oid, req->new_oid,
496 + req->xpp->flags, hunk_cb, cb_data))
497 + return DIFF_PROVIDER_DISP_ANSWERED;
498 + return DIFF_PROVIDER_DISP_PASS;
499 +}
500 +
501 +/*
502 + * The provider borrows the repository's store through
503 + * repo_diff_hunks_store() per request; the object database owns the
504 + * file and tears it down, so there is nothing to release here.
505 + */
506 +struct diff_provider *diff_hunks_store_provider_new(void)
507 +{
508 + struct diff_provider *p = xcalloc(1, sizeof(*p));
509 +
510 + p->consult = diff_hunks_store_consult;
511 + return p;
512 +}
513 +
514 /* Validate one store file. Returns 0 if valid or absent, -1 on any error. */
515 static int verify_store_at(struct repository *r, const char *fname)
516 {
diff-hunks.h
+8
@@ -62,6 +62,14 @@ struct diff_hunks_store *repo_diff_hunks_store(struct repository *r);
62 /* Free the repository's cached store, at object-database teardown. */
63 void close_diff_hunks_store(struct object_database *o);
64
65 +/*
66 + * Consultation counters for the repository's store: pairs the store
67 + * served (hits) and pairs it was consulted for but could not serve
68 + * (misses). Both zero when reading is disabled or no store exists.
69 + */
70 +void diff_hunks_read_stats(struct repository *r,
71 + unsigned long *hits, unsigned long *misses);
72 +
73 /*
74 * Replay the recorded hunks of an (old blob, new blob) pair diffed
75 * under xdl_opts through hunk_func. The sequence is validated before
diff-provider-internal.h
+7
@@ -88,6 +88,13 @@ struct diff_provider {
88 struct diff_provider *next;
89 };
90
91 +/*
92 + * The providers Git ships, besides the builtin computation that
93 + * diff-provider.c holds itself. Each call returns a fresh provider
94 + * for one repository's chain.
95 + */
96 +struct diff_provider *diff_hunks_store_provider_new(void);
97 +
98 /*
99 * Incremental well-formedness check for a provider-supplied hunk
100 * sequence, shared by every provider. Each coordinate, and each
diff-provider.c
+48 -14
@@ -1,5 +1,7 @@
1 #include "git-compat-util.h"
2 +#include "diff.h"
3 #include "diff-provider-internal.h"
4 +#include "replace-object.h"
5 #include "repository.h"
6
7 /*
@@ -39,10 +41,11 @@ static struct diff_provider *builtin_provider_new(void)
41
42 /*
43 * The repository's chain, assembled on first walk. The composition
42 - * is fixed; the builtin computation is the terminal provider, so the
43 - * chain always ends in an implementor that can answer. Nothing is
44 - * decided per repository here; each provider gates itself per
45 - * request.
44 + * is fixed, and the order is the authority resolution: the store is
45 + * consulted before the builtin computation, the terminal provider,
46 + * so the chain always ends in an implementor that can answer.
47 + * Nothing is decided per repository here; each provider gates itself
48 + * per request.
49 */
50 static struct diff_provider *provider_chain(struct repository *r)
51 {
@@ -50,6 +53,8 @@ static struct diff_provider *provider_chain(struct repository *r)
53
54 if (*tail)
55 return *tail;
56 + *tail = diff_hunks_store_provider_new();
57 + tail = &(*tail)->next;
58 *tail = builtin_provider_new();
59 return r->diff_providers;
60 }
@@ -70,16 +75,16 @@ void diff_providers_clear(struct repository *r)
75 }
76
77 /*
73 - * The walk behind diff_provider_emit_hunks(): consult the chain in
74 - * order and map its dispositions onto the outcome set. The first
75 - * answer ends the walk. A stop-no-record disposition
76 - * (diff-provider-internal.h) is a refusal, not a pass: the provider
77 - * does not answer, but rules the pair out of identity service and
78 - * out of recording, so from then on the walk consults only the
79 - * computing provider, and a walk that ends unanswered carries the
80 - * no-record verdict. With a fill callback the terminal provider
81 - * computes instead of passing, so an emit walk returns only
82 - * answered or error.
78 + * The walk shared by diff_provider_consult() and
79 + * diff_provider_emit_hunks(): consult the chain in order and map its
80 + * dispositions onto the outcome set. The first answer ends the
81 + * walk. A stop-no-record disposition (diff-provider-internal.h)
82 + * is a refusal, not a pass: the provider does not answer, but rules
83 + * the pair out of identity service and out of recording, so from
84 + * then on the walk consults only the computing provider, and a walk
85 + * that ends unanswered carries the no-record verdict. With a fill
86 + * callback the terminal provider computes instead of passing, so an
87 + * emit walk returns only answered or error.
88 */
89 static enum diff_provider_outcome
90 walk_providers(const struct diff_provider_request *req,
@@ -89,6 +94,28 @@ walk_providers(const struct diff_provider_request *req,
94 struct diff_provider *p;
95 int no_record = 0;
96
97 + if (req->diffopt && req->diffopt->repo != req->repo)
98 + BUG("diff provider request walks one repository's chain "
99 + "with another repository's diff options");
100 +
101 + /*
102 + * An object replacement redirects a blob's content
103 + * (OBJECT_INFO_LOOKUP_REPLACE) while leaving the id that names it
104 + * unchanged, so an answer keyed on the raw id would be the
105 + * pre-replacement diff. A replacement is therefore a parameter
106 + * outside the recording key: no provider may serve a replaced pair
107 + * from its identity, and a result computed for it must not be
108 + * recorded under the raw id. Mark the walk no-record so the
109 + * identity providers step aside and the builtin computes from the
110 + * replaced content. The check is a no-op when the repository has
111 + * no replace refs.
112 + */
113 + if ((req->old_oid &&
114 + lookup_replace_object(req->repo, req->old_oid) != req->old_oid) ||
115 + (req->new_oid &&
116 + lookup_replace_object(req->repo, req->new_oid) != req->new_oid))
117 + no_record = 1;
118 +
119 for (p = provider_chain(req->repo); p; p = p->next) {
120 enum diff_provider_disposition disp;
121
@@ -118,6 +145,13 @@ walk_providers(const struct diff_provider_request *req,
145 DIFF_PROVIDER_UNANSWERED;
146 }
147
148 +enum diff_provider_outcome
149 +diff_provider_consult(const struct diff_provider_request *req,
150 + xdl_emit_hunk_consume_func_t hunk_cb, void *cb_data)
151 +{
152 + return walk_providers(req, NULL, NULL, hunk_cb, cb_data);
153 +}
154 +
155 enum diff_provider_hunks_error
156 diff_provider_check_hunk(struct diff_provider_hunks_check *c,
157 long old_start, long old_count,
diff-provider.h
+25 -3
@@ -26,6 +26,8 @@
26 * sees it.
27 */
28
29 +struct diff_options;
30 +struct object_id;
31 struct repository;
32
33 /*
@@ -89,15 +91,35 @@ enum diff_provider_outcome {
91 * A consultation request. The interface consults providers from
92 * these fields alone; no content is loaded before an answer.
93 *
92 - * repo owns the provider chain the request walks. xpp carries the
93 - * parameters the diff runs with. Each provider gates itself on the
94 - * fields that concern it.
94 + * repo owns the provider chain the request walks. old_oid/new_oid
95 + * name the blobs whose bytes are diffed; pass NULL for a side whose
96 + * bytes are not a stored blob (a working-tree file, textconv output,
97 + * a gitlink), so no provider answers from an id it cannot look up.
98 + * diffopt carries the diff settings that live outside xpp; xpp
99 + * carries the parameters the diff runs with. Each provider gates
100 + * itself on the fields that concern it.
101 */
102 struct diff_provider_request {
103 struct repository *repo;
104 + const struct object_id *old_oid;
105 + const struct object_id *new_oid;
106 + struct diff_options *diffopt;
107 const xpparam_t *xpp;
108 };
109
110 +/*
111 + * Consult the providers for the request's pair without computing.
112 + * On DIFF_PROVIDER_ANSWERED the hunks were emitted through hunk_cb
113 + * (0-based emission coordinates, context 0) and were validated
114 + * before the first callback ran, so a consumer may accumulate
115 + * directly into its result. Never returns DIFF_PROVIDER_ERROR.
116 + * The callback's return value is not consulted: emission of a
117 + * validated answer has no error leg, so the callback must return 0.
118 + */
119 +enum diff_provider_outcome
120 +diff_provider_consult(const struct diff_provider_request *req,
121 + xdl_emit_hunk_consume_func_t hunk_cb, void *cb_data);
122 +
123 /*
124 * Load the pair's content. Called at most once per request, only
125 * when the ranges are computed rather than provided. The buffers
diff.c
+62 -24
@@ -17,6 +17,7 @@
17 #include "quote.h"
18 #include "diff.h"
19 #include "diff-hunks.h"
20 +#include "diff-provider.h"
21 #include "diffcore.h"
22 #include "delta.h"
23 #include "hex.h"
@@ -35,6 +36,7 @@
36 #include "tmp-objdir.h"
37 #include "graph.h"
38 #include "oid-array.h"
39 +#include "trace2.h"
40 #include "packfile.h"
41 #include "pager.h"
42 #include "parse-options.h"
@@ -2992,6 +2994,11 @@ void diff_hunks_attach(struct diff_options *o)
2994
2995 void diff_hunks_detach(struct diff_options *o)
2996 {
2997 + unsigned long hits, misses;
2998 +
2999 + diff_hunks_read_stats(o->repo, &hits, &misses);
3000 + if (hits)
3001 + trace2_data_intmax("diff-hunks", o->repo, "read-hits", hits);
3002 diff_hunks_writer_finish(o->hunks_writer);
3003 o->hunks_writer = NULL;
3004 }
@@ -4321,18 +4328,35 @@ static const char *get_compact_summary(const struct diff_filepair *p, int is_ren
4328 }
4329
4330 /*
4324 - * Fill data->added/deleted for a modified pair by collecting its hunk
4325 - * coordinates, and record them into the store. Runs only on a warming
4326 - * run; returns 1 when it produced the counts, 0 when the caller must
4327 - * compute the diffstat itself.
4331 + * Hunk callback for the provider interface: sum counts into a
4332 + * diffstat entry.
4333 + */
4334 +static int diffstat_sum_hunk_cb(long start_a UNUSED, long count_a,
4335 + long start_b UNUSED, long count_b,
4336 + void *cb_data)
4337 +{
4338 + struct diffstat_file *data = cb_data;
4339 +
4340 + data->added += count_b;
4341 + data->deleted += count_a;
4342 + return 0;
4343 +}
4344 +
4345 +/*
4346 + * Fill data->added/deleted for a modified pair through the hunk provider
4347 + * interface: on an answer, sum the provided counts; on a warming run,
4348 + * compute and record them. Returns 1 when it produced the counts, 0 when
4349 + * the caller must compute the diffstat itself.
4350 *
4329 - * --ignore-blank-lines is excluded: that flag is part of the store
4330 - * key, but it coalesces hunks differently between the emit and
4331 - * hunk-callback paths, so a recorded entry would not match a
4332 - * store-less run's --stat output. (--inter-hunk-context is not
4333 - * excluded: it only groups hunks, and diffstat sums their counts,
4334 - * which grouping does not change.) Recording requires both sides to
4335 - * be valid regular files whose blobs the key can name.
4351 + * The providers own the exclusions the request can express (-B, -I,
4352 + * and --anchored are outside the store key). This consumer additionally
4353 + * excludes --ignore-blank-lines before consulting: that flag is part of
4354 + * the key, but it coalesces hunks differently between the emit and
4355 + * hunk-callback paths, so a served answer would not match a store-less
4356 + * run's --stat output. (--inter-hunk-context is not excluded: it only
4357 + * groups hunks, and diffstat sums their counts, which grouping does not
4358 + * change.) Recording requires both sides to be valid regular files whose
4359 + * blobs the key can name.
4360 */
4361 static int diffstat_from_hunks(struct diff_options *o,
4362 struct diff_filespec *one,
@@ -4347,23 +4371,37 @@ static int diffstat_from_hunks(struct diff_options *o,
4371 .ignore_regex_nr = o->ignore_regex_nr,
4372 .anchors = o->anchors,
4373 .anchors_nr = o->anchors_nr };
4374 + struct diff_provider_request req = {
4375 + .repo = o->repo,
4376 + .old_oid = (one->oid_valid && !S_ISGITLINK(one->mode)) ?
4377 + &one->oid : NULL,
4378 + .new_oid = (two->oid_valid && !S_ISGITLINK(two->mode)) ?
4379 + &two->oid : NULL,
4380 + .diffopt = o,
4381 + .xpp = &xpp,
4382 + };
4383
4384 if (o->xdl_opts & XDF_IGNORE_BLANK_LINES)
4385 return 0;
4386 + /* format-patch keeps its diffstat off the store (see the flag). */
4387 + if (o->flags.no_precomputed_hunks)
4388 + return 0;
4389
4354 - /* Not a warming run: the caller computes the diffstat. */
4355 - if (!o->hunks_writer)
4390 + switch (diff_provider_consult(&req, diffstat_sum_hunk_cb, data)) {
4391 + case DIFF_PROVIDER_ANSWERED:
4392 + return 1;
4393 + case DIFF_PROVIDER_UNANSWERED:
4394 + break;
4395 + case DIFF_PROVIDER_ERROR: /* not returned by a consult */
4396 + case DIFF_PROVIDER_UNANSWERED_NO_RECORD:
4397 return 0;
4357 - /*
4358 - * -I patterns, --anchored anchors, and break detection (-B)
4359 - * shape the diff outside the store key, so what they compute
4360 - * must not be recorded under it.
4361 - */
4362 - if (o->ignore_regex_nr || o->anchors_nr || o->break_opt != -1)
4398 + }
4399 +
4400 + /* A miss on a read-only run: let the caller compute the diffstat. */
4401 + if (!o->hunks_writer)
4402 return 0;
4403 /* Recording needs blobs the key can name, on both sides. */
4365 - if (!one->oid_valid || !two->oid_valid ||
4366 - S_ISGITLINK(one->mode) || S_ISGITLINK(two->mode) ||
4404 + if (!req.old_oid || !req.new_oid ||
4405 !DIFF_FILE_VALID(one) || !DIFF_FILE_VALID(two) ||
4406 !S_ISREG(one->mode) || !S_ISREG(two->mode))
4407 return 0;
@@ -4453,9 +4491,9 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
4491
4492 else if (may_differ) {
4493 /*
4456 - * Record into the diff-hunks store on a warming run. A
4457 - * "log -L" range-scoped stat is not the whole-pair diff
4458 - * the store keys, so it does not record. Otherwise diff
4494 + * Serve or record via the diff-hunks store. A "log -L"
4495 + * range-scoped stat is not the whole-pair diff the store
4496 + * keys, so it neither reads nor records. Otherwise diff
4497 * normally.
4498 */
4499 if (p->line_ranges || !diffstat_from_hunks(o, one, two, data)) {
diff.h
+15 -5
@@ -206,6 +206,13 @@ struct diff_flags {
206 unsigned suppress_diff_headers;
207 unsigned dual_color_diffed_diffs;
208 unsigned suppress_hunk_header_line_count;
209 +
210 + /*
211 + * Do not serve the diffstat from the precomputed-hunks store.
212 + * Set by format-patch so a generated patch carries the builtin
213 + * counts and does not depend on the sender's local store state.
214 + */
215 + unsigned no_precomputed_hunks;
216 };
217
218 static inline void diff_flags_or(struct diff_flags *a,
@@ -422,9 +429,11 @@ struct diff_options {
429 int max_depth_valid;
430
431 /*
425 - * Precomputed diff hunks (see diff-hunks.h). When hunks_writer is
426 - * set (a warming run), diffstat records the hunks it computes;
427 - * the writer is attached only for the stat output formats.
432 + * Precomputed diff hunks (see diff-hunks.h). diffstat consults the
433 + * hunk provider interface before running xdiff, keyed by each file
434 + * pair's blob object IDs. When hunks_writer is set (a warming run),
435 + * diffstat also records the hunks it computes; the writer is
436 + * attached only for the stat output formats.
437 */
438 struct diff_hunks_writer *hunks_writer;
439 };
@@ -679,8 +688,9 @@ void diff_free(struct diff_options*);
688 /*
689 * Attach a diff-hunks writer to a diff producing a stat format, so a
690 * warming run records the hunks it computes; a no-op when writing is off
682 - * or for other formats. Pair with diff_hunks_detach() once the diff is
683 - * done.
691 + * or for other formats. (Reading is separate: consumers consult the
692 + * providers through diff_provider_consult(); see diff-provider.h.) Pair
693 + * with diff_hunks_detach() once the diff is done.
694 */
695 void diff_hunks_attach(struct diff_options *o);
696 void diff_hunks_detach(struct diff_options *o);
t/t4220-diff-hunks.sh
+280
@@ -81,6 +81,79 @@ test_expect_success 'a second warming run refreshes the store in place' '
81 test_cmp expect actual
82 '
83
84 +test_expect_success 'log --stat matches with and without the store' '
85 + no_store log --stat >expect &&
86 + warm &&
87 + git log --stat >actual &&
88 + test_cmp expect actual
89 +'
90 +
91 +test_expect_success 'log --numstat and --shortstat match' '
92 + no_store log --numstat >expect_num &&
93 + no_store log --shortstat >expect_short &&
94 + warm &&
95 + git log --numstat >actual_num &&
96 + git log --shortstat >actual_short &&
97 + test_cmp expect_num actual_num &&
98 + test_cmp expect_short actual_short
99 +'
100 +
101 +# A built store must reproduce diffstat output at every context
102 +# length. Only trim-stable pairs are recorded, so one entry serves
103 +# every context; a trim-divergent pair is never recorded and always
104 +# computed. Zero context is where trim_common_tail runs, which is
105 +# what makes the two diffs differ.
106 +test_expect_success 'diffstat matches at several context lengths' '
107 + no_store log --stat >expect_def &&
108 + no_store log -U0 --stat >expect_u0 &&
109 + no_store log -U7 --stat >expect_u7 &&
110 + warm &&
111 + git log --stat >got_def &&
112 + git log -U0 --stat >got_u0 &&
113 + git log -U7 --stat >got_u7 &&
114 + test_cmp expect_def got_def &&
115 + test_cmp expect_u0 got_u0 &&
116 + test_cmp expect_u7 got_u7
117 +'
118 +
119 +test_expect_success 'store built at a nonzero context stays correct at that context' '
120 + no_store -c diff.context=5 log --stat >expect &&
121 + warm -c diff.context=5 &&
122 + git -c diff.context=5 log --stat >actual &&
123 + test_cmp expect actual
124 +'
125 +
126 +# This blob pair (a real git test file being modernized) has different
127 +# valid diffs at different contexts: at zero context, where
128 +# trim_common_tail runs, "diff -U0" reports 9/6, while "diff -U3"
129 +# reports 10/7. Such a trim-divergent pair is exactly what the writer
130 +# must never record, since no single entry could serve both readers.
131 +# A compact synthetic pair cannot show this count split: on small
132 +# inputs xdiff produces minimal diffs, minimal diffs of one pair all
133 +# add and delete the same number of lines, and trimming the common
134 +# tail preserves minimality, so the counts agree by construction (a
135 +# search over thousands of synthetic pairs up to 8 lines found no
136 +# split). The split needs the cost-capping heuristics that only larger
137 +# inputs trigger, so the pair is shipped as a fixture under t4220/.
138 +test_expect_success 'a trim-divergent file is correct at each context' '
139 + cp "$TEST_DIRECTORY/t4220/trim-divergent-old" div.sh &&
140 + git add div.sh &&
141 + git commit -m divergent-old &&
142 + cp "$TEST_DIRECTORY/t4220/trim-divergent-new" div.sh &&
143 + git add div.sh &&
144 + git commit -m divergent-new &&
145 + no_store log -1 --format= --stat -- div.sh >expect_def &&
146 + no_store log -1 --format= -U0 --stat -- div.sh >expect_u0 &&
147 + warm &&
148 + git log -1 --format= --stat -- div.sh >got_def &&
149 + git log -1 --format= -U0 --stat -- div.sh >got_u0 &&
150 + test_cmp expect_def got_def &&
151 + test_cmp expect_u0 got_u0 &&
152 + # The fixture must actually diverge, or the test would pass without
153 + # exercising the split; fail loudly if a diff change ever levels it.
154 + ! test_cmp expect_def expect_u0
155 +'
156 +
157 # A warming run displays the diffstat it computes. At zero context xdi_diff
158 # trims, so the displayed counts must be the trimmed ones (what a store-less
159 # run shows), not the untrimmed ones the writer compares against when it
@@ -99,6 +172,16 @@ test_expect_success 'warming --stat at zero context matches a store-less run' '
172 )
173 '
174
175 +test_expect_success 'diff --stat matches with and without the store, both directions' '
176 + no_store diff --stat second fourth >expect_fwd &&
177 + no_store diff --stat fourth second >expect_rev &&
178 + warm &&
179 + git diff --stat second fourth >got_fwd &&
180 + git diff --stat fourth second >got_rev &&
181 + test_cmp expect_fwd got_fwd &&
182 + test_cmp expect_rev got_rev
183 +'
184 +
185 test_expect_success 'show and diff-tree --stat use the store' '
186 test_when_finished "git diff-hunks clear" &&
187 # diff_hunks_attach() runs for show and diff-tree: a write-enabled
@@ -121,6 +204,193 @@ test_expect_success 'show and diff-tree --stat use the store' '
204 test_cmp expect_dt got_dt
205 '
206
207 +test_expect_success 'log -R --stat matches (reversed pairs keyed apart)' '
208 + no_store log -R --stat >expect &&
209 + warm &&
210 + git log -R --stat >actual &&
211 + test_cmp expect actual
212 +'
213 +
214 +# The diffstat read path produces identical output on a hit or a miss, so
215 +# it emits a trace2 "read-hits" count to prove it consulted the store.
216 +test_expect_success 'diffstat consults the store (trace shows read hits)' '
217 + warm &&
218 + GIT_TRACE2_EVENT="$PWD/trace_on.json" git log --stat >/dev/null &&
219 + test_grep read-hits trace_on.json &&
220 + test_env GIT_TRACE2_EVENT="$PWD/trace_off.json" no_store log --stat >/dev/null &&
221 + test_grep ! read-hits trace_off.json
222 +'
223 +
224 +# Diff settings that change hunks but are not part of the store key must
225 +# bypass it in both directions, so output stays byte-identical to a
226 +# store-less run.
227 +test_expect_success 'setup ignore fixture' '
228 + git init ignore-repo &&
229 + (
230 + cd ignore-repo &&
231 + test_write_lines code keep "# c" >f &&
232 + git add f &&
233 + git commit -m c1 &&
234 + test_write_lines codeCH keep "# cX" >f &&
235 + git add f &&
236 + git commit -m c2 &&
237 + warm
238 + )
239 +'
240 +
241 +# Output parity alone cannot prove the guard: served counts can
242 +# coincide with computed ones, so each bypass below also asserts the
243 +# consultation itself (no read hit with the option, a hit without it)
244 +# and that a warming run under the option records nothing.
245 +test_expect_success '-I bypasses the store in both directions' '
246 + (
247 + cd ignore-repo &&
248 + no_store diff -I"^#" --numstat HEAD~ HEAD >expect &&
249 + git diff -I"^#" --numstat HEAD~ HEAD >actual &&
250 + test_cmp expect actual &&
251 + # -I does not change the key, so only the ignore_regex
252 + # guard keeps the warmed entry from serving here.
253 + GIT_TRACE2_EVENT="$PWD/trace_i.json" \
254 + git diff -I"^#" --numstat HEAD~ HEAD >/dev/null &&
255 + test_grep ! read-hits trace_i.json &&
256 + GIT_TRACE2_EVENT="$PWD/trace_i_ctl.json" \
257 + git diff --numstat HEAD~ HEAD >/dev/null &&
258 + test_grep read-hits trace_i_ctl.json &&
259 + git diff-hunks clear &&
260 + GIT_DIFF_HUNKS_WRITE=1 git diff -I"^#" --numstat HEAD~ HEAD >/dev/null &&
261 + test_path_is_missing .git/objects/info/diff-hunks &&
262 + # Restore the warmed fixture for the tests below.
263 + warm
264 + )
265 +'
266 +
267 +test_expect_success '-B bypasses the store in both directions' '
268 + git init break-repo &&
269 + (
270 + cd break-repo &&
271 + test_write_lines a b c d e f g h >f &&
272 + git add f &&
273 + git commit -m orig &&
274 + test_write_lines 1 2 3 4 5 6 7 8 >f &&
275 + git add f &&
276 + git commit -m rewrite &&
277 + warm &&
278 + no_store diff -B --stat HEAD~ HEAD >expect &&
279 + git diff -B --stat HEAD~ HEAD >actual &&
280 + test_cmp expect actual &&
281 + GIT_TRACE2_EVENT="$PWD/trace_b.json" \
282 + git diff -B --stat HEAD~ HEAD >/dev/null &&
283 + test_grep ! read-hits trace_b.json &&
284 + GIT_TRACE2_EVENT="$PWD/trace_ctl.json" \
285 + git diff --stat HEAD~ HEAD >/dev/null &&
286 + test_grep read-hits trace_ctl.json &&
287 + git diff-hunks clear &&
288 + GIT_DIFF_HUNKS_WRITE=1 git diff -B --stat HEAD~ HEAD >/dev/null &&
289 + test_path_is_missing .git/objects/info/diff-hunks
290 + )
291 +'
292 +
293 +test_expect_success '--anchored bypasses the store in both directions' '
294 + (
295 + cd ignore-repo &&
296 + no_store diff --stat --anchored=keep HEAD~ HEAD >expect &&
297 + git diff --stat --anchored=keep HEAD~ HEAD >actual &&
298 + test_cmp expect actual &&
299 + # Anchors do not change the key, so only the anchors guard
300 + # keeps the warmed entry from serving here.
301 + GIT_TRACE2_EVENT="$PWD/trace_anchor.json" \
302 + git diff --stat --anchored=keep HEAD~ HEAD >/dev/null &&
303 + test_grep ! read-hits trace_anchor.json &&
304 + GIT_TRACE2_EVENT="$PWD/trace_plain.json" \
305 + git diff --stat HEAD~ HEAD >/dev/null &&
306 + test_grep read-hits trace_plain.json &&
307 + git diff-hunks clear &&
308 + GIT_DIFF_HUNKS_WRITE=1 \
309 + git diff --stat --anchored=keep HEAD~ HEAD >/dev/null &&
310 + test_path_is_missing .git/objects/info/diff-hunks
311 + )
312 +'
313 +
314 +test_expect_success '--ignore-blank-lines bypasses the store in both directions' '
315 + git init ibl-repo &&
316 + (
317 + cd ibl-repo &&
318 + printf "a\n\nx\ny\nb\n" >f &&
319 + git add f &&
320 + git commit -m v1 &&
321 + printf "a\nx\ny\nB\n" >f &&
322 + git add f &&
323 + git commit -m v2 &&
324 + warm &&
325 + no_store diff --stat --ignore-blank-lines HEAD~ HEAD >expect &&
326 + git diff --stat --ignore-blank-lines HEAD~ HEAD >actual &&
327 + test_cmp expect actual &&
328 + # The flag is an xdl_opts bit and thus part of the key; the
329 + # stat consumer excludes it before consulting at all.
330 + GIT_TRACE2_EVENT="$PWD/trace_ibl.json" \
331 + git diff --stat --ignore-blank-lines HEAD~ HEAD >/dev/null &&
332 + test_grep ! read-hits trace_ibl.json &&
333 + GIT_TRACE2_EVENT="$PWD/trace_plain.json" \
334 + git diff --stat HEAD~ HEAD >/dev/null &&
335 + test_grep read-hits trace_plain.json &&
336 + git diff-hunks clear &&
337 + GIT_DIFF_HUNKS_WRITE=1 \
338 + git diff --stat --ignore-blank-lines HEAD~ HEAD >/dev/null &&
339 + test_path_is_missing .git/objects/info/diff-hunks
340 + )
341 +'
342 +
343 +test_expect_success 'a whitespace-ignoring diff is not served default entries' '
344 + git init ws-repo &&
345 + (
346 + cd ws-repo &&
347 + test_write_lines alpha beta gamma >f &&
348 + git add f &&
349 + git commit -m c1 &&
350 + test_write_lines " alpha" beta gamma delta >f &&
351 + git add f &&
352 + git commit -m c2 &&
353 + warm &&
354 + no_store diff -w --numstat HEAD~ HEAD >expect &&
355 + git diff -w --numstat HEAD~ HEAD >actual &&
356 + test_cmp expect actual
357 + )
358 +'
359 +
360 +test_expect_success 'a driver algorithm override keeps output correct and keys apart' '
361 + git init driver-algo &&
362 + (
363 + cd driver-algo &&
364 + echo "file.foo diff=foo" >.gitattributes &&
365 + git add .gitattributes &&
366 + git commit -m attributes &&
367 + test_write_lines 1 2 3 4 5 >file.foo &&
368 + git add file.foo &&
369 + git commit -m one &&
370 + test_write_lines 1 2 X 4 5 6 >file.foo &&
371 + git add file.foo &&
372 + git commit -m two &&
373 + warm -c diff.foo.algorithm=histogram &&
374 + no_store -c diff.foo.algorithm=histogram log --stat >expect &&
375 + git -c diff.foo.algorithm=histogram log --stat >actual &&
376 + test_cmp expect actual &&
377 + # The driver algorithm is an xdl_opts key bit: entries
378 + # warmed at the default settings must not serve a
379 + # driver-forced histogram read, and output stays correct.
380 + git diff-hunks clear &&
381 + warm &&
382 + no_store -c diff.foo.algorithm=histogram log --stat >expect2 &&
383 + git -c diff.foo.algorithm=histogram log --stat >actual2 &&
384 + test_cmp expect2 actual2 &&
385 + GIT_TRACE2_EVENT="$PWD/trace_algo.json" \
386 + git -c diff.foo.algorithm=histogram log --stat >/dev/null &&
387 + test_grep ! read-hits trace_algo.json &&
388 + GIT_TRACE2_EVENT="$PWD/trace_algo_ctl.json" \
389 + git log --stat >/dev/null &&
390 + test_grep read-hits trace_algo_ctl.json
391 + )
392 +'
393 +
394 # Cover the pair shapes an object walk encounters: binary and
395 # mode-only changes produce no text hunks to record.
396 test_expect_success 'binary and mode-only changes do not break the writer' '
@@ -141,6 +411,16 @@ test_expect_success 'binary and mode-only changes do not break the writer' '
411 test_cmp expect actual
412 '
413
414 +test_expect_success 'log -L --stat neither reads nor records' '
415 + warm &&
416 + GIT_TRACE2_EVENT="$PWD/trace_linelog.json" \
417 + git log -L1,1:file.txt --stat >/dev/null &&
418 + test_grep ! read-hits trace_linelog.json &&
419 + git diff-hunks clear &&
420 + GIT_DIFF_HUNKS_WRITE=1 git log -L1,1:file.txt --stat >/dev/null &&
421 + test_path_is_missing $STORE
422 +'
423 +
424 test_expect_success 'verify succeeds on a valid store and on an absent one' '
425 warm &&
426 git diff-hunks verify &&