diff: record precomputed hunks during stat output

The diff-hunks store has a writer, but nothing fills it. Teach builtin_diffstat() to do so: on a warming run (a writer is attached), a modified pair's stat is produced by collecting the pair's hunk coordinates instead of emitting text, the counts are summed from those hunks, and the pair is recorded. A run without a writer is unchanged, and nothing reads the store yet; the read side arrives next. The store records one context-free entry per pair, and only for a trim-stable pair: one whose zero-context trimmed diff (what blame will read) and untrimmed diff (whose counts a nonzero-context stat matches) are identical. The warming path computes both and hands them to diff_hunks_writer_record_stable(), new here, which records only when they agree; a divergent pair is never recorded and every consumer computes it. The warming run displays the counts it shows a store-less run: the trimmed ones, since xdi_diff trims at zero context, while the untrimmed counts serve only the stability comparison. Not everything the stat path computes may be recorded. --ignore-blank-lines is part of the key, but it coalesces hunks differently between the text-emitting and coordinate-callback paths, so a recorded entry would not match a store-less run's --stat. -I patterns, --anchored, and break detection (-B) shape the diff outside the key entirely; the guard for those three sits in this consumer for now and moves into the store's own provider when it registers, next. A "log -L" range-scoped stat is not the whole-pair diff the key describes, so it does not record. Recording also requires both sides to be valid regular files whose blobs the key can name: a working-tree side, textconv output, or a gitlink has no usable id. "git diff", "git log", "git show", and "git diff-tree" with the --stat, --numstat, and --shortstat formats attach a writer when writing is enabled and flush it when the traversal finishes, so a warming run such as GIT_DIFF_HUNKS_WRITE=1 git log --all --stat >/dev/null fills the cache as a side effect of the diff work the command already does. Writing is controlled by diffHunks.write and GIT_DIFF_HUNKS_WRITE. Add the write half of t4220: - ordinary commands never create the store, and creation is gated off by default, the environment overriding the config; - a warming run builds a store that verifies, and a second refreshes it in place; - a warming run displays parity at zero context on a trim-divergent pair, committed as a fixture (small synthetic pairs cannot diverge: minimal diffs add and delete equal counts, and trimming preserves that); - binary and mode-only pairs do not break the writer; - a corrupt store is discarded at seed; - verify and clear run against the files a warming run builds. 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 55b052a198f43984e2913788f013f6b7b56c1a54
12 files changed +1150 -30
builtin/diff-tree.c
+3
@@ -170,6 +170,8 @@ int cmd_diff_tree(int argc,
170
171 opt->diffopt.rotate_to_strict = 1;
172
173 + diff_hunks_attach(&opt->diffopt);
174 +
175 /*
176 * NOTE! We expect "a..b" to expand to "^a b" but it is
177 * perfectly valid for revision range parser to yield "b ^a",
@@ -234,5 +236,6 @@ int cmd_diff_tree(int argc,
236 diff_free(&opt->diffopt);
237 }
238
239 + diff_hunks_detach(&opt->diffopt);
240 return diff_result_code(opt);
241 }
builtin/diff.c
+10
@@ -568,6 +568,15 @@ int cmd_diff(int argc,
568 }
569 }
570
571 + /*
572 + * The hunk store is keyed by blob pair, so any diff whose
573 + * file pairs carry known blob object IDs (tree-to-tree,
574 + * index-to-tree) can consult the same entries that
575 + * "git log --stat" and "git blame" use; pairs without known
576 + * blobs bypass it at lookup time.
577 + */
578 + diff_hunks_attach(&rev.diffopt);
579 +
580 symdiff_prepare(&rev, &sdiff);
581 for (i = 0; i < rev.pending.nr; i++) {
582 struct object_array_entry *entry = &rev.pending.objects[i];
@@ -648,6 +657,7 @@ int cmd_diff(int argc,
657 result = diff_result_code(&rev);
658 if (1 < rev.diffopt.skip_stat_unmatch)
659 refresh_index_quietly();
660 + diff_hunks_detach(&rev.diffopt);
661 release_revisions(&rev);
662 object_array_clear(&ent);
663 symdiff_release(&sdiff);
builtin/log.c
+7
@@ -693,8 +693,11 @@ int cmd_show(int argc,
693 opt.tweak = show_setup_revisions_tweak;
694 cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);
695
696 + diff_hunks_attach(&rev.diffopt);
697 +
698 if (!rev.no_walk) {
699 ret = cmd_log_walk(&rev);
700 + diff_hunks_detach(&rev.diffopt);
701 release_revisions(&rev);
702 log_config_release(&cfg);
703 return ret;
@@ -767,6 +770,7 @@ int cmd_show(int argc,
770 }
771
772 rev.diffopt.no_free = 0;
773 + diff_hunks_detach(&rev.diffopt);
774 diff_free(&rev.diffopt);
775 release_revisions(&rev);
776 log_config_release(&cfg);
@@ -846,8 +850,11 @@ int cmd_log(int argc,
850 opt.tweak = log_setup_revisions_tweak;
851 cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);
852
853 + diff_hunks_attach(&rev.diffopt);
854 +
855 ret = cmd_log_walk(&rev);
856
857 + diff_hunks_detach(&rev.diffopt);
858 release_revisions(&rev);
859 log_config_release(&cfg);
860 return ret;
diff-hunks.c
+32
@@ -651,6 +651,38 @@ int diff_hunks_writer_add(struct diff_hunks_writer *w,
651 return 1;
652 }
653
654 +void diff_hunks_writer_record_stable(struct diff_hunks_writer *w,
655 + const struct object_id *old_oid,
656 + const struct object_id *new_oid,
657 + int xdl_opts,
658 + const struct precomputed_hunk *trimmed,
659 + size_t nr_trimmed,
660 + const struct precomputed_hunk *full,
661 + size_t nr_full)
662 +{
663 + size_t i;
664 +
665 + if (!w)
666 + return;
667 + /*
668 + * Record only a trim-stable pair, one whose trimmed and
669 + * untrimmed diffs are identical, so the single entry answers
670 + * any consumer at any context (see the top of this file). A
671 + * pair where the two diffs differ is never recorded and every
672 + * consumer computes it.
673 + */
674 + if (nr_trimmed != nr_full)
675 + return;
676 + for (i = 0; i < nr_trimmed; i++)
677 + if (trimmed[i].old_start != full[i].old_start ||
678 + trimmed[i].old_count != full[i].old_count ||
679 + trimmed[i].new_start != full[i].new_start ||
680 + trimmed[i].new_count != full[i].new_count)
681 + return;
682 + diff_hunks_writer_add(w, old_oid, new_oid, xdl_opts,
683 + trimmed, nr_trimmed);
684 +}
685 +
686 /*
687 * Seed the writer with fname's entries so a rewrite preserves them,
688 * setting *pruned when the rewrite will not carry the whole file
diff-hunks.h
+17 -1
@@ -94,7 +94,8 @@ struct diff_hunks_writer *diff_hunks_writer_maybe_new(struct repository *r);
94 * Record a blob pair's hunks as computed under xdl_opts; a later lookup
95 * with a matching key is served these hunks. The caller must have
96 * checked that the pair's trimmed and untrimmed diffs are identical
97 - * (see the top of this file), so the entry answers at any context.
97 + * (see the top of this file), so the entry answers at any context;
98 + * diff_hunks_writer_record_stable() below performs that check.
99 * NULL-safe. Returns 1 when the entry was recorded, 0 when the writer
100 * refused it (no hunks, a null object id, or values the on-disk
101 * 32-bit fields cannot hold).
@@ -106,6 +107,21 @@ int diff_hunks_writer_add(struct diff_hunks_writer *w,
107 const struct precomputed_hunk *hunks,
108 size_t nr_hunks);
109
110 +/*
111 + * Record the pair only if it is trim-stable: the recording caller
112 + * hands over both the trimmed (xdi_diff) and untrimmed (xdl_diff)
113 + * zero-context hunk sequences it computed, and the entry is added
114 + * only when the two are identical. NULL-safe.
115 + */
116 +void diff_hunks_writer_record_stable(struct diff_hunks_writer *w,
117 + const struct object_id *old_oid,
118 + const struct object_id *new_oid,
119 + int xdl_opts,
120 + const struct precomputed_hunk *trimmed,
121 + size_t nr_trimmed,
122 + const struct precomputed_hunk *full,
123 + size_t nr_full);
124 +
125 /* Flush the accumulated entries to the store and free the writer. NULL-safe. */
126 void diff_hunks_writer_finish(struct diff_hunks_writer *w);
127
diff.c
+189 -29
@@ -16,6 +16,7 @@
16 #include "revision.h"
17 #include "quote.h"
18 #include "diff.h"
19 +#include "diff-hunks.h"
20 #include "diffcore.h"
21 #include "delta.h"
22 #include "hex.h"
@@ -2929,6 +2930,72 @@ static struct diffstat_file *diffstat_add(struct diffstat_t *diffstat,
2930 return x;
2931 }
2932
2933 +struct diffstat_hunk_cb_data {
2934 + struct precomputed_hunk **h;
2935 + size_t *nr, *alloc;
2936 +};
2937 +
2938 +/*
2939 + * Hunk callback that appends each hunk's coordinates to a growable
2940 + * array, so one xdiff pass can both sum a diffstat and record hunks for
2941 + * the store.
2942 + */
2943 +static int diffstat_hunk_cb(long start_a, long count_a,
2944 + long start_b, long count_b,
2945 + void *cb_data)
2946 +{
2947 + struct diffstat_hunk_cb_data *d = cb_data;
2948 +
2949 + ALLOC_GROW(*d->h, *d->nr + 1, *d->alloc);
2950 + (*d->h)[*d->nr].old_start = start_a;
2951 + (*d->h)[*d->nr].old_count = count_a;
2952 + (*d->h)[*d->nr].new_start = start_b;
2953 + (*d->h)[*d->nr].new_count = count_b;
2954 + (*d->nr)++;
2955 + return 0;
2956 +}
2957 +
2958 +/*
2959 + * Collect the hunks of the two files at zero context. diff_fn chooses
2960 + * whether trimming runs: xdi_diff applies trim_common_tail, yielding the
2961 + * zero-context hunks blame reads; xdl_diff does not, yielding the
2962 + * untrimmed hunks. Both run at zero context, so the untrimmed hunks are
2963 + * not grouped the way a nonzero context would group them; diffstat only
2964 + * sums their counts, which grouping does not change. Sets *ph (caller
2965 + * frees) and *ph_nr.
2966 + */
2967 +typedef int (*xdiff_fn)(mmfile_t *, mmfile_t *, xpparam_t const *,
2968 + xdemitconf_t const *, xdemitcb_t *);
2969 +static int collect_hunks(xdiff_fn diff_fn, mmfile_t *mf1, mmfile_t *mf2,
2970 + xpparam_t *xpp, struct precomputed_hunk **ph,
2971 + size_t *ph_nr)
2972 +{
2973 + size_t ph_alloc = 0;
2974 + xdemitcb_t ecb = { 0 };
2975 + xdemitconf_t xecfg = { 0 };
2976 + struct diffstat_hunk_cb_data cd = { ph, ph_nr, &ph_alloc };
2977 +
2978 + *ph = NULL;
2979 + *ph_nr = 0;
2980 + xecfg.hunk_func = diffstat_hunk_cb;
2981 + ecb.priv = &cd;
2982 + return diff_fn(mf1, mf2, xpp, &xecfg, &ecb);
2983 +}
2984 +
2985 +void diff_hunks_attach(struct diff_options *o)
2986 +{
2987 + if (!(o->output_format &
2988 + (DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_NUMSTAT)))
2989 + return;
2990 + o->hunks_writer = diff_hunks_writer_maybe_new(o->repo);
2991 +}
2992 +
2993 +void diff_hunks_detach(struct diff_options *o)
2994 +{
2995 + diff_hunks_writer_finish(o->hunks_writer);
2996 + o->hunks_writer = NULL;
2997 +}
2998 +
2999 static int diffstat_consume(void *priv, char *line, unsigned long len)
3000 {
3001 struct diffstat_t *diffstat = priv;
@@ -4253,6 +4320,87 @@ static const char *get_compact_summary(const struct diff_filepair *p, int is_ren
4320 return NULL;
4321 }
4322
4323 +/*
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.
4328 + *
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.
4336 + */
4337 +static int diffstat_from_hunks(struct diff_options *o,
4338 + struct diff_filespec *one,
4339 + struct diff_filespec *two,
4340 + struct diffstat_file *data)
4341 +{
4342 + struct precomputed_hunk *ph_trim, *ph_full, *counts;
4343 + size_t n_trim, n_full, n_counts, k;
4344 + mmfile_t mf1, mf2;
4345 + xpparam_t xpp = { .flags = o->xdl_opts,
4346 + .ignore_regex = o->ignore_regex,
4347 + .ignore_regex_nr = o->ignore_regex_nr,
4348 + .anchors = o->anchors,
4349 + .anchors_nr = o->anchors_nr };
4350 +
4351 + if (o->xdl_opts & XDF_IGNORE_BLANK_LINES)
4352 + return 0;
4353 +
4354 + /* Not a warming run: the caller computes the diffstat. */
4355 + if (!o->hunks_writer)
4356 + 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)
4363 + return 0;
4364 + /* 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) ||
4367 + !DIFF_FILE_VALID(one) || !DIFF_FILE_VALID(two) ||
4368 + !S_ISREG(one->mode) || !S_ISREG(two->mode))
4369 + return 0;
4370 +
4371 + if (fill_mmfile(o->repo, &mf1, one) < 0 ||
4372 + fill_mmfile(o->repo, &mf2, two) < 0)
4373 + die("unable to read files to diff");
4374 +
4375 + /*
4376 + * Compute the zero-context trimmed diff (what blame reads) and the
4377 + * untrimmed diff (whose counts a nonzero-context stat matches).
4378 + * xdi_diff runs first: it enforces the size limit, so the xdl_diff
4379 + * call is already bounded.
4380 + */
4381 + if (collect_hunks(xdi_diff, &mf1, &mf2, &xpp, &ph_trim, &n_trim) ||
4382 + collect_hunks(xdl_diff, &mf1, &mf2, &xpp, &ph_full, &n_full))
4383 + die("unable to generate diffstat for %s", one->path);
4384 +
4385 + /*
4386 + * Match a store-less run: at zero context xdi_diff trims, so sum the
4387 + * trimmed diff; otherwise sum the untrimmed one.
4388 + */
4389 + counts = o->context ? ph_full : ph_trim;
4390 + n_counts = o->context ? n_full : n_trim;
4391 + for (k = 0; k < n_counts; k++) {
4392 + data->added += counts[k].new_count;
4393 + data->deleted += counts[k].old_count;
4394 + }
4395 +
4396 + diff_hunks_writer_record_stable(o->hunks_writer, &one->oid, &two->oid,
4397 + o->xdl_opts, ph_trim, n_trim,
4398 + ph_full, n_full);
4399 + free(ph_trim);
4400 + free(ph_full);
4401 + return 1;
4402 +}
4403 +
4404 static void builtin_diffstat(const char *name_a, const char *name_b,
4405 struct diff_filespec *one,
4406 struct diff_filespec *two,
@@ -4304,38 +4452,50 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
4452 }
4453
4454 else if (may_differ) {
4307 - /* Crazy xdl interfaces.. */
4308 - xpparam_t xpp;
4309 - xdemitconf_t xecfg;
4310 -
4311 - if (fill_mmfile(o->repo, &mf1, one) < 0 ||
4312 - fill_mmfile(o->repo, &mf2, two) < 0)
4313 - die("unable to read files to diff");
4314 -
4315 - memset(&xpp, 0, sizeof(xpp));
4316 - memset(&xecfg, 0, sizeof(xecfg));
4317 - xpp.flags = o->xdl_opts;
4318 - xpp.ignore_regex = o->ignore_regex;
4319 - xpp.ignore_regex_nr = o->ignore_regex_nr;
4320 - xpp.anchors = o->anchors;
4321 - xpp.anchors_nr = o->anchors_nr;
4322 - xecfg.ctxlen = o->context;
4323 - xecfg.interhunkctxlen = o->interhunkcontext;
4324 - xecfg.flags = XDL_EMIT_NO_HUNK_HDR;
4325 -
4326 - if (p->line_ranges) {
4327 - struct line_range_filter lr_filter;
4328 -
4329 - line_range_filter_init(&lr_filter, p->line_ranges,
4330 - diffstat_consume, diffstat);
4455 + /*
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
4459 + * normally.
4460 + */
4461 + if (p->line_ranges || !diffstat_from_hunks(o, one, two, data)) {
4462 + /* Crazy xdl interfaces.. */
4463 + xpparam_t xpp;
4464 + xdemitconf_t xecfg;
4465 +
4466 + if (fill_mmfile(o->repo, &mf1, one) < 0 ||
4467 + fill_mmfile(o->repo, &mf2, two) < 0)
4468 + die("unable to read files to diff");
4469 +
4470 + memset(&xpp, 0, sizeof(xpp));
4471 + memset(&xecfg, 0, sizeof(xecfg));
4472 + xpp.flags = o->xdl_opts;
4473 + xpp.ignore_regex = o->ignore_regex;
4474 + xpp.ignore_regex_nr = o->ignore_regex_nr;
4475 + xpp.anchors = o->anchors;
4476 + xpp.anchors_nr = o->anchors_nr;
4477 + xecfg.ctxlen = o->context;
4478 + xecfg.interhunkctxlen = o->interhunkcontext;
4479 + xecfg.flags = XDL_EMIT_NO_HUNK_HDR;
4480
4332 - if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
4333 - &xpp, &xecfg))
4481 + if (p->line_ranges) {
4482 + struct line_range_filter lr_filter;
4483 +
4484 + line_range_filter_init(&lr_filter,
4485 + p->line_ranges,
4486 + diffstat_consume,
4487 + diffstat);
4488 +
4489 + if (line_range_filter_diff(&lr_filter, &mf1,
4490 + &mf2, &xpp, &xecfg))
4491 + die("unable to generate diffstat for %s",
4492 + one->path);
4493 + } else if (xdi_diff_outf(&mf1, &mf2, NULL,
4494 + diffstat_consume, diffstat,
4495 + &xpp, &xecfg))
4496 die("unable to generate diffstat for %s",
4497 one->path);
4336 - } else if (xdi_diff_outf(&mf1, &mf2, NULL,
4337 - diffstat_consume, diffstat, &xpp, &xecfg))
4338 - die("unable to generate diffstat for %s", one->path);
4498 + }
4499
4500 if (DIFF_FILE_VALID(one) && DIFF_FILE_VALID(two)) {
4501 struct diffstat_file *file =
diff.h
+17
@@ -420,6 +420,13 @@ struct diff_options {
420 */
421 int max_depth;
422 int max_depth_valid;
423 +
424 + /*
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.
428 + */
429 + struct diff_hunks_writer *hunks_writer;
430 };
431
432 unsigned diff_filter_bit(char status);
@@ -668,6 +675,16 @@ void diffcore_fix_diff_index(void);
675 int diff_queue_is_empty(struct diff_options *o);
676 void diff_flush(struct diff_options*);
677 void diff_free(struct diff_options*);
678 +
679 +/*
680 + * Attach a diff-hunks writer to a diff producing a stat format, so a
681 + * 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.
684 + */
685 +void diff_hunks_attach(struct diff_options *o);
686 +void diff_hunks_detach(struct diff_options *o);
687 +
688 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);
689
690 /* diff-raw status letters */
t/meson.build
+1
@@ -583,6 +583,7 @@ integration_tests = [
583 't4216-log-bloom.sh',
584 't4217-log-limit.sh',
585 't4219-log-follow-merge.sh',
586 + 't4220-diff-hunks.sh',
587 't4252-am-options.sh',
588 't4253-am-keep-cr-dos.sh',
589 't4254-am-corrupt.sh',
t/t4220-diff-hunks.sh new
+184
@@ -0,0 +1,184 @@
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 +# A warming run displays the diffstat it computes. At zero context xdi_diff
85 +# trims, so the displayed counts must be the trimmed ones (what a store-less
86 +# run shows), not the untrimmed ones the writer compares against when it
87 +# decides whether the pair is stable enough to record.
88 +test_expect_success 'warming --stat at zero context matches a store-less run' '
89 + git init -q warm-u0 &&
90 + (
91 + cd warm-u0 &&
92 + cp "$TEST_DIRECTORY/t4220/trim-divergent-old" div.sh &&
93 + git add div.sh && git commit -q -m old &&
94 + cp "$TEST_DIRECTORY/t4220/trim-divergent-new" div.sh &&
95 + git add div.sh && git commit -q -m new &&
96 + git -c core.diffhunks=false log -1 --format= -U0 --stat -- div.sh >expect &&
97 + GIT_DIFF_HUNKS_WRITE=1 git log -1 --format= -U0 --stat -- div.sh >got &&
98 + test_cmp expect got
99 + )
100 +'
101 +
102 +test_expect_success 'show and diff-tree --stat use the store' '
103 + test_when_finished "git diff-hunks clear" &&
104 + # diff_hunks_attach() runs for show and diff-tree: a write-enabled
105 + # --stat records into the store (without the attach there is no
106 + # writer, so nothing is written).
107 + git diff-hunks clear &&
108 + GIT_DIFF_HUNKS_WRITE=1 git show --stat fourth >/dev/null &&
109 + test_path_is_file "$STORE" &&
110 + git diff-hunks clear &&
111 + GIT_DIFF_HUNKS_WRITE=1 git diff-tree --stat fourth >/dev/null &&
112 + test_path_is_file "$STORE" &&
113 + # Reading never changes their output.
114 + git diff-hunks clear &&
115 + no_store show --stat fourth >expect_show &&
116 + no_store diff-tree --stat fourth >expect_dt &&
117 + warm &&
118 + git show --stat fourth >got_show &&
119 + git diff-tree --stat fourth >got_dt &&
120 + test_cmp expect_show got_show &&
121 + test_cmp expect_dt got_dt
122 +'
123 +
124 +# Cover the pair shapes an object walk encounters: binary and
125 +# mode-only changes produce no text hunks to record.
126 +test_expect_success 'binary and mode-only changes do not break the writer' '
127 + printf "\\000\\001\\002" >bin.dat &&
128 + git add bin.dat &&
129 + git commit -m binary-1 &&
130 + printf "\\000\\001\\003\\004" >bin.dat &&
131 + git add bin.dat &&
132 + git commit -m binary-2 &&
133 + echo "mode content" >mode.txt &&
134 + git add mode.txt &&
135 + git commit -m mode-1 &&
136 + test_chmod +x mode.txt &&
137 + git commit -m mode-2 &&
138 + no_store log --stat >expect &&
139 + warm &&
140 + git log --stat >actual &&
141 + test_cmp expect actual
142 +'
143 +
144 +test_expect_success 'verify succeeds on a valid store and on an absent one' '
145 + warm &&
146 + git diff-hunks verify &&
147 + git diff-hunks clear &&
148 + test_path_is_missing $STORE &&
149 + git diff-hunks verify
150 +'
151 +
152 +test_expect_success 'verify detects a checksum mismatch' '
153 + test_when_finished "git diff-hunks clear" &&
154 + warm &&
155 + fsize=$(test_file_size $STORE) &&
156 + mid=$((fsize / 2)) &&
157 + printf "\\377" | dd of=$STORE bs=1 seek=$mid count=1 conv=notrunc 2>/dev/null &&
158 + test_must_fail git diff-hunks verify
159 +'
160 +
161 +test_expect_success 'a warm discards a corrupt store rather than seeding from it' '
162 + test_when_finished "git diff-hunks clear" &&
163 + warm &&
164 + # Corrupt the checksum: the next warm must not carry the corrupt
165 + # entries forward into a fresh checksum-valid file; it discards
166 + # them (with a warning) and rewrites a store that verifies.
167 + fsize=$(test_file_size $STORE) &&
168 + printf "\\377" | dd of=$STORE bs=1 seek=$((fsize / 2)) count=1 conv=notrunc 2>/dev/null &&
169 + warm 2>err &&
170 + test_grep "failed its checksum" err &&
171 + git diff-hunks verify &&
172 + no_store log --stat >expect &&
173 + git log --stat >actual &&
174 + test_cmp expect actual
175 +'
176 +
177 +test_expect_success 'diff-hunks clear removes the store file' '
178 + warm &&
179 + test_path_is_file $STORE &&
180 + git diff-hunks clear &&
181 + test_path_is_missing $STORE
182 +'
183 +
184 +test_done
t/t4220/README new
+55
@@ -0,0 +1,55 @@
1 +t4220 diff-hunks test fixtures
2 +==============================
3 +
4 +trim-divergent-old, trim-divergent-new
5 +--------------------------------------
6 +
7 +Two revisions of a single real file, used by t4220-diff-hunks.sh to
8 +exercise a "trim-divergent" blob pair: one whose diff hunk counts change
9 +with the amount of context, so the trimmed and untrimmed results
10 +disagree.
11 +
12 +They are two versions of git's own t/t6002-rev-list-bisect.sh, taken from
13 +git.git history around:
14 +
15 + 090af9957c ("t6002: fix use of `expr` with `set -e`",
16 + Patrick Steinhardt, 2026-04-21)
17 +
18 +which rewrites `$(expr ...)` arithmetic as `$((...))` and reformats a few
19 +test_expect_success blocks.
20 +
21 + trim-divergent-old = 090af9957c^:t/t6002-rev-list-bisect.sh (blob daa009c9a1)
22 + trim-divergent-new = 090af9957c :t/t6002-rev-list-bisect.sh (blob f2de40b5ed)
23 +
24 +To regenerate them from any git.git checkout:
25 +
26 + git show 090af9957c^:t/t6002-rev-list-bisect.sh >trim-divergent-old
27 + git show 090af9957c:t/t6002-rev-list-bisect.sh >trim-divergent-new
28 +
29 +Why this pair
30 +-------------
31 +
32 +The diff-hunks store records only "trim-stable" pairs: those whose hunks
33 +are identical whether or not xdiff trims the common head and tail (which
34 +it does at zero context, in trim_common_tail). This pair is deliberately
35 +NOT trim-stable:
36 +
37 + diff -U0 reports 9 added / 6 deleted
38 + diff -U3 reports 10 added / 7 deleted
39 +
40 +Because the counts diverge with context, the writer must refuse to record
41 +this pair and every command must recompute it from the blobs. t4220 uses
42 +it to prove that the displayed counts stay correct at each context, and
43 +that a divergent pair is never served from the store. See
44 +t4220-diff-hunks.sh ("a trim-divergent file is correct at each context"
45 +and the store-poison test).
46 +
47 +Why not a synthesized fixture
48 +-----------------------------
49 +
50 +The divergence needs real content that makes xdiff's common-tail trimming
51 +shift a hunk boundary while the added/deleted balance stays equal. A
52 +minimal hand-written file that reliably triggers the -U0 vs -U3 count
53 +disagreement has not been found yet; until one is, this real pair is kept
54 +verbatim. If you synthesize a smaller equivalent, replace these two files
55 +and delete this note.
t/t4220/trim-divergent-new new
+319
@@ -0,0 +1,319 @@
1 +#!/bin/sh
2 +#
3 +# Copyright (c) 2005 Jon Seymour
4 +#
5 +test_description='Tests git rev-list --bisect functionality'
6 +
7 +. ./test-lib.sh
8 +. "$TEST_DIRECTORY"/lib-t6000.sh # t6xxx specific functions
9 +
10 +# usage: test_bisection max-diff bisect-option head ^prune...
11 +#
12 +# e.g. test_bisection 1 --bisect l1 ^l0
13 +#
14 +test_bisection_diff()
15 +{
16 + _max_diff=$1
17 + _bisect_option=$2
18 + shift 2
19 + _bisection=$(git rev-list $_bisect_option "$@")
20 + _list_size=$(git rev-list "$@" | wc -l)
21 + _head=$1
22 + shift 1
23 + _bisection_size=$(git rev-list $_bisection "$@" | wc -l)
24 + [ -n "$_list_size" -a -n "$_bisection_size" ] ||
25 + error "test_bisection_diff failed"
26 +
27 + # Test if bisection size is close to half of list size within
28 + # tolerance.
29 + #
30 + _bisect_err=$(($_list_size - $_bisection_size * 2))
31 + if test "$_bisect_err" -lt 0
32 + then
33 + _bisect_err=$((0 - $_bisect_err))
34 + fi
35 + _bisect_err=$(($_bisect_err / 2)) ; # floor
36 +
37 + test_expect_success "bisection diff $_bisect_option $_head $* <= $_max_diff" '
38 + test $_bisect_err -le $_max_diff
39 + '
40 +}
41 +
42 +date >path0
43 +git update-index --add path0
44 +save_tag tree git write-tree
45 +on_committer_date "00:00" hide_error save_tag root unique_commit root tree
46 +on_committer_date "00:01" save_tag l0 unique_commit l0 tree -p root
47 +on_committer_date "00:02" save_tag l1 unique_commit l1 tree -p l0
48 +on_committer_date "00:03" save_tag l2 unique_commit l2 tree -p l1
49 +on_committer_date "00:04" save_tag a0 unique_commit a0 tree -p l2
50 +on_committer_date "00:05" save_tag a1 unique_commit a1 tree -p a0
51 +on_committer_date "00:06" save_tag b1 unique_commit b1 tree -p a0
52 +on_committer_date "00:07" save_tag c1 unique_commit c1 tree -p b1
53 +on_committer_date "00:08" save_tag b2 unique_commit b2 tree -p b1
54 +on_committer_date "00:09" save_tag b3 unique_commit b2 tree -p b2
55 +on_committer_date "00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2
56 +on_committer_date "00:11" save_tag c3 unique_commit c3 tree -p c2
57 +on_committer_date "00:12" save_tag a2 unique_commit a2 tree -p a1
58 +on_committer_date "00:13" save_tag a3 unique_commit a3 tree -p a2
59 +on_committer_date "00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3
60 +on_committer_date "00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3
61 +on_committer_date "00:16" save_tag l3 unique_commit l3 tree -p a4
62 +on_committer_date "00:17" save_tag l4 unique_commit l4 tree -p l3
63 +on_committer_date "00:18" save_tag l5 unique_commit l5 tree -p l4
64 +git update-ref HEAD $(tag l5)
65 +
66 +
67 +# E
68 +# / \
69 +# e1 |
70 +# | |
71 +# e2 |
72 +# | |
73 +# e3 |
74 +# | |
75 +# e4 |
76 +# | |
77 +# | f1
78 +# | |
79 +# | f2
80 +# | |
81 +# | f3
82 +# | |
83 +# | f4
84 +# | |
85 +# e5 |
86 +# | |
87 +# e6 |
88 +# | |
89 +# e7 |
90 +# | |
91 +# e8 |
92 +# \ /
93 +# F
94 +
95 +
96 +on_committer_date "00:00" hide_error save_tag F unique_commit F tree
97 +on_committer_date "00:01" save_tag e8 unique_commit e8 tree -p F
98 +on_committer_date "00:02" save_tag e7 unique_commit e7 tree -p e8
99 +on_committer_date "00:03" save_tag e6 unique_commit e6 tree -p e7
100 +on_committer_date "00:04" save_tag e5 unique_commit e5 tree -p e6
101 +on_committer_date "00:05" save_tag f4 unique_commit f4 tree -p F
102 +on_committer_date "00:06" save_tag f3 unique_commit f3 tree -p f4
103 +on_committer_date "00:07" save_tag f2 unique_commit f2 tree -p f3
104 +on_committer_date "00:08" save_tag f1 unique_commit f1 tree -p f2
105 +on_committer_date "00:09" save_tag e4 unique_commit e4 tree -p e5
106 +on_committer_date "00:10" save_tag e3 unique_commit e3 tree -p e4
107 +on_committer_date "00:11" save_tag e2 unique_commit e2 tree -p e3
108 +on_committer_date "00:12" save_tag e1 unique_commit e1 tree -p e2
109 +on_committer_date "00:13" save_tag E unique_commit E tree -p e1 -p f1
110 +
111 +on_committer_date "00:00" hide_error save_tag U unique_commit U tree
112 +on_committer_date "00:01" save_tag u0 unique_commit u0 tree -p U
113 +on_committer_date "00:01" save_tag u1 unique_commit u1 tree -p u0
114 +on_committer_date "00:02" save_tag u2 unique_commit u2 tree -p u0
115 +on_committer_date "00:03" save_tag u3 unique_commit u3 tree -p u0
116 +on_committer_date "00:04" save_tag u4 unique_commit u4 tree -p u0
117 +on_committer_date "00:05" save_tag u5 unique_commit u5 tree -p u0
118 +on_committer_date "00:06" save_tag V unique_commit V tree -p u1 -p u2 -p u3 -p u4 -p u5
119 +
120 +test_sequence()
121 +{
122 + _bisect_option=$1
123 +
124 + test_bisection_diff 0 $_bisect_option l0 ^root
125 + test_bisection_diff 0 $_bisect_option l1 ^root
126 + test_bisection_diff 0 $_bisect_option l2 ^root
127 + test_bisection_diff 0 $_bisect_option a0 ^root
128 + test_bisection_diff 0 $_bisect_option a1 ^root
129 + test_bisection_diff 0 $_bisect_option a2 ^root
130 + test_bisection_diff 0 $_bisect_option a3 ^root
131 + test_bisection_diff 0 $_bisect_option b1 ^root
132 + test_bisection_diff 0 $_bisect_option b2 ^root
133 + test_bisection_diff 0 $_bisect_option b3 ^root
134 + test_bisection_diff 0 $_bisect_option c1 ^root
135 + test_bisection_diff 0 $_bisect_option c2 ^root
136 + test_bisection_diff 0 $_bisect_option c3 ^root
137 + test_bisection_diff 0 $_bisect_option E ^F
138 + test_bisection_diff 0 $_bisect_option e1 ^F
139 + test_bisection_diff 0 $_bisect_option e2 ^F
140 + test_bisection_diff 0 $_bisect_option e3 ^F
141 + test_bisection_diff 0 $_bisect_option e4 ^F
142 + test_bisection_diff 0 $_bisect_option e5 ^F
143 + test_bisection_diff 0 $_bisect_option e6 ^F
144 + test_bisection_diff 0 $_bisect_option e7 ^F
145 + test_bisection_diff 0 $_bisect_option f1 ^F
146 + test_bisection_diff 0 $_bisect_option f2 ^F
147 + test_bisection_diff 0 $_bisect_option f3 ^F
148 + test_bisection_diff 0 $_bisect_option f4 ^F
149 + test_bisection_diff 0 $_bisect_option E ^F
150 +
151 + test_bisection_diff 1 $_bisect_option V ^U
152 + test_bisection_diff 0 $_bisect_option V ^U ^u1 ^u2 ^u3
153 + test_bisection_diff 0 $_bisect_option u1 ^U
154 + test_bisection_diff 0 $_bisect_option u2 ^U
155 + test_bisection_diff 0 $_bisect_option u3 ^U
156 + test_bisection_diff 0 $_bisect_option u4 ^U
157 + test_bisection_diff 0 $_bisect_option u5 ^U
158 +
159 +#
160 +# the following illustrates Linus' binary bug blatt idea.
161 +#
162 +# assume the bug is actually at l3, but you don't know that - all you know is that l3 is broken
163 +# and it wasn't broken before
164 +#
165 +# keep bisecting the list, advancing the "bad" head and accumulating "good" heads until
166 +# the bisection point is the head - this is the bad point.
167 +#
168 +
169 +test_output_expect_success "$_bisect_option l5 ^root" 'git rev-list $_bisect_option l5 ^root' <<EOF
170 +c3
171 +EOF
172 +
173 +test_output_expect_success "$_bisect_option l5 ^root ^c3" 'git rev-list $_bisect_option l5 ^root ^c3' <<EOF
174 +b4
175 +EOF
176 +
177 +test_output_expect_success "$_bisect_option l5 ^root ^c3 ^b4" 'git rev-list $_bisect_option l5 ^c3 ^b4' <<EOF
178 +l3
179 +EOF
180 +
181 +test_output_expect_success "$_bisect_option l3 ^root ^c3 ^b4" 'git rev-list $_bisect_option l3 ^root ^c3 ^b4' <<EOF
182 +a4
183 +EOF
184 +
185 +test_output_expect_success "$_bisect_option l5 ^b3 ^a3 ^b4 ^a4" 'git rev-list $_bisect_option l3 ^b3 ^a3 ^a4' <<EOF
186 +l3
187 +EOF
188 +
189 +#
190 +# if l3 is bad, then l4 is bad too - so advance the bad pointer by making b4 the known bad head
191 +#
192 +
193 +test_output_expect_success "$_bisect_option l4 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l4 ^a2 ^a3 ^a4' <<EOF
194 +l3
195 +EOF
196 +
197 +test_output_expect_success "$_bisect_option l3 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l3 ^a2 ^a3 ^a4' <<EOF
198 +l3
199 +EOF
200 +
201 +# found!
202 +
203 +#
204 +# as another example, let's consider a4 to be the bad head, in which case
205 +#
206 +
207 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
208 +c2
209 +EOF
210 +
211 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2' <<EOF
212 +c3
213 +EOF
214 +
215 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3' <<EOF
216 +a4
217 +EOF
218 +
219 +# found!
220 +
221 +#
222 +# or consider c3 to be the bad head
223 +#
224 +
225 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
226 +c2
227 +EOF
228 +
229 +test_output_expect_success "$_bisect_option c3 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option c3 ^a2 ^a3 ^b4 ^c2' <<EOF
230 +c3
231 +EOF
232 +
233 +# found!
234 +
235 +}
236 +
237 +test_sequence "--bisect"
238 +
239 +#
240 +#
241 +
242 +test_expect_success 'set up fake --bisect refs' '
243 + git update-ref refs/bisect/bad c3 &&
244 + good=$(git rev-parse b1) &&
245 + git update-ref refs/bisect/good-$good $good &&
246 + good=$(git rev-parse c1) &&
247 + git update-ref refs/bisect/good-$good $good
248 +'
249 +
250 +test_expect_success 'rev-list --bisect can default to good/bad refs' '
251 + # the only thing between c3 and c1 is c2
252 + git rev-parse c2 >expect &&
253 + git rev-list --bisect >actual &&
254 + test_cmp expect actual
255 +'
256 +
257 +test_expect_success 'rev-parse --bisect can default to good/bad refs' '
258 + git rev-parse c3 ^b1 ^c1 >expect &&
259 + git rev-parse --bisect >actual &&
260 +
261 + # output order depends on the refnames, which in turn depends on
262 + # the exact sha1s. We just want to make sure we have the same set
263 + # of lines in any order.
264 + sort <expect >expect.sorted &&
265 + sort <actual >actual.sorted &&
266 + test_cmp expect.sorted actual.sorted
267 +'
268 +
269 +test_output_expect_success '--bisect --first-parent' 'git rev-list --bisect --first-parent E ^F' <<EOF
270 +e4
271 +EOF
272 +
273 +test_output_expect_success '--first-parent' 'git rev-list --first-parent E ^F' <<EOF
274 +E
275 +e1
276 +e2
277 +e3
278 +e4
279 +e5
280 +e6
281 +e7
282 +e8
283 +EOF
284 +
285 +test_output_expect_success '--bisect-vars --first-parent' 'git rev-list --bisect-vars --first-parent E ^F' <<EOF
286 +bisect_rev='e5'
287 +bisect_nr=4
288 +bisect_good=4
289 +bisect_bad=3
290 +bisect_all=9
291 +bisect_steps=2
292 +EOF
293 +
294 +test_expect_success '--bisect-all --first-parent' '
295 + cat >expect.unsorted <<-EOF &&
296 + $(git rev-parse E) (tag: E, dist=0)
297 + $(git rev-parse e1) (tag: e1, dist=1)
298 + $(git rev-parse e2) (tag: e2, dist=2)
299 + $(git rev-parse e3) (tag: e3, dist=3)
300 + $(git rev-parse e4) (tag: e4, dist=4)
301 + $(git rev-parse e5) (tag: e5, dist=4)
302 + $(git rev-parse e6) (tag: e6, dist=3)
303 + $(git rev-parse e7) (tag: e7, dist=2)
304 + $(git rev-parse e8) (tag: e8, dist=1)
305 + EOF
306 +
307 + # expect results to be ordered by distance (descending),
308 + # commit hash (ascending)
309 + sort -k4,4r -k1,1 expect.unsorted >expect &&
310 + git rev-list --bisect-all --first-parent E ^F >actual &&
311 + test_cmp expect actual
312 +'
313 +
314 +test_expect_success '--bisect without any revisions' '
315 + git rev-list --bisect HEAD..HEAD >out &&
316 + test_must_be_empty out
317 +'
318 +
319 +test_done
t/t4220/trim-divergent-old new
+316
@@ -0,0 +1,316 @@
1 +#!/bin/sh
2 +#
3 +# Copyright (c) 2005 Jon Seymour
4 +#
5 +test_description='Tests git rev-list --bisect functionality'
6 +
7 +. ./test-lib.sh
8 +. "$TEST_DIRECTORY"/lib-t6000.sh # t6xxx specific functions
9 +
10 +# usage: test_bisection max-diff bisect-option head ^prune...
11 +#
12 +# e.g. test_bisection 1 --bisect l1 ^l0
13 +#
14 +test_bisection_diff()
15 +{
16 + _max_diff=$1
17 + _bisect_option=$2
18 + shift 2
19 + _bisection=$(git rev-list $_bisect_option "$@")
20 + _list_size=$(git rev-list "$@" | wc -l)
21 + _head=$1
22 + shift 1
23 + _bisection_size=$(git rev-list $_bisection "$@" | wc -l)
24 + [ -n "$_list_size" -a -n "$_bisection_size" ] ||
25 + error "test_bisection_diff failed"
26 +
27 + # Test if bisection size is close to half of list size within
28 + # tolerance.
29 + #
30 + _bisect_err=$(expr $_list_size - $_bisection_size \* 2)
31 + test "$_bisect_err" -lt 0 && _bisect_err=$(expr 0 - $_bisect_err)
32 + _bisect_err=$(expr $_bisect_err / 2) ; # floor
33 +
34 + test_expect_success \
35 + "bisection diff $_bisect_option $_head $* <= $_max_diff" \
36 + 'test $_bisect_err -le $_max_diff'
37 +}
38 +
39 +date >path0
40 +git update-index --add path0
41 +save_tag tree git write-tree
42 +on_committer_date "00:00" hide_error save_tag root unique_commit root tree
43 +on_committer_date "00:01" save_tag l0 unique_commit l0 tree -p root
44 +on_committer_date "00:02" save_tag l1 unique_commit l1 tree -p l0
45 +on_committer_date "00:03" save_tag l2 unique_commit l2 tree -p l1
46 +on_committer_date "00:04" save_tag a0 unique_commit a0 tree -p l2
47 +on_committer_date "00:05" save_tag a1 unique_commit a1 tree -p a0
48 +on_committer_date "00:06" save_tag b1 unique_commit b1 tree -p a0
49 +on_committer_date "00:07" save_tag c1 unique_commit c1 tree -p b1
50 +on_committer_date "00:08" save_tag b2 unique_commit b2 tree -p b1
51 +on_committer_date "00:09" save_tag b3 unique_commit b2 tree -p b2
52 +on_committer_date "00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2
53 +on_committer_date "00:11" save_tag c3 unique_commit c3 tree -p c2
54 +on_committer_date "00:12" save_tag a2 unique_commit a2 tree -p a1
55 +on_committer_date "00:13" save_tag a3 unique_commit a3 tree -p a2
56 +on_committer_date "00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3
57 +on_committer_date "00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3
58 +on_committer_date "00:16" save_tag l3 unique_commit l3 tree -p a4
59 +on_committer_date "00:17" save_tag l4 unique_commit l4 tree -p l3
60 +on_committer_date "00:18" save_tag l5 unique_commit l5 tree -p l4
61 +git update-ref HEAD $(tag l5)
62 +
63 +
64 +# E
65 +# / \
66 +# e1 |
67 +# | |
68 +# e2 |
69 +# | |
70 +# e3 |
71 +# | |
72 +# e4 |
73 +# | |
74 +# | f1
75 +# | |
76 +# | f2
77 +# | |
78 +# | f3
79 +# | |
80 +# | f4
81 +# | |
82 +# e5 |
83 +# | |
84 +# e6 |
85 +# | |
86 +# e7 |
87 +# | |
88 +# e8 |
89 +# \ /
90 +# F
91 +
92 +
93 +on_committer_date "00:00" hide_error save_tag F unique_commit F tree
94 +on_committer_date "00:01" save_tag e8 unique_commit e8 tree -p F
95 +on_committer_date "00:02" save_tag e7 unique_commit e7 tree -p e8
96 +on_committer_date "00:03" save_tag e6 unique_commit e6 tree -p e7
97 +on_committer_date "00:04" save_tag e5 unique_commit e5 tree -p e6
98 +on_committer_date "00:05" save_tag f4 unique_commit f4 tree -p F
99 +on_committer_date "00:06" save_tag f3 unique_commit f3 tree -p f4
100 +on_committer_date "00:07" save_tag f2 unique_commit f2 tree -p f3
101 +on_committer_date "00:08" save_tag f1 unique_commit f1 tree -p f2
102 +on_committer_date "00:09" save_tag e4 unique_commit e4 tree -p e5
103 +on_committer_date "00:10" save_tag e3 unique_commit e3 tree -p e4
104 +on_committer_date "00:11" save_tag e2 unique_commit e2 tree -p e3
105 +on_committer_date "00:12" save_tag e1 unique_commit e1 tree -p e2
106 +on_committer_date "00:13" save_tag E unique_commit E tree -p e1 -p f1
107 +
108 +on_committer_date "00:00" hide_error save_tag U unique_commit U tree
109 +on_committer_date "00:01" save_tag u0 unique_commit u0 tree -p U
110 +on_committer_date "00:01" save_tag u1 unique_commit u1 tree -p u0
111 +on_committer_date "00:02" save_tag u2 unique_commit u2 tree -p u0
112 +on_committer_date "00:03" save_tag u3 unique_commit u3 tree -p u0
113 +on_committer_date "00:04" save_tag u4 unique_commit u4 tree -p u0
114 +on_committer_date "00:05" save_tag u5 unique_commit u5 tree -p u0
115 +on_committer_date "00:06" save_tag V unique_commit V tree -p u1 -p u2 -p u3 -p u4 -p u5
116 +
117 +test_sequence()
118 +{
119 + _bisect_option=$1
120 +
121 + test_bisection_diff 0 $_bisect_option l0 ^root
122 + test_bisection_diff 0 $_bisect_option l1 ^root
123 + test_bisection_diff 0 $_bisect_option l2 ^root
124 + test_bisection_diff 0 $_bisect_option a0 ^root
125 + test_bisection_diff 0 $_bisect_option a1 ^root
126 + test_bisection_diff 0 $_bisect_option a2 ^root
127 + test_bisection_diff 0 $_bisect_option a3 ^root
128 + test_bisection_diff 0 $_bisect_option b1 ^root
129 + test_bisection_diff 0 $_bisect_option b2 ^root
130 + test_bisection_diff 0 $_bisect_option b3 ^root
131 + test_bisection_diff 0 $_bisect_option c1 ^root
132 + test_bisection_diff 0 $_bisect_option c2 ^root
133 + test_bisection_diff 0 $_bisect_option c3 ^root
134 + test_bisection_diff 0 $_bisect_option E ^F
135 + test_bisection_diff 0 $_bisect_option e1 ^F
136 + test_bisection_diff 0 $_bisect_option e2 ^F
137 + test_bisection_diff 0 $_bisect_option e3 ^F
138 + test_bisection_diff 0 $_bisect_option e4 ^F
139 + test_bisection_diff 0 $_bisect_option e5 ^F
140 + test_bisection_diff 0 $_bisect_option e6 ^F
141 + test_bisection_diff 0 $_bisect_option e7 ^F
142 + test_bisection_diff 0 $_bisect_option f1 ^F
143 + test_bisection_diff 0 $_bisect_option f2 ^F
144 + test_bisection_diff 0 $_bisect_option f3 ^F
145 + test_bisection_diff 0 $_bisect_option f4 ^F
146 + test_bisection_diff 0 $_bisect_option E ^F
147 +
148 + test_bisection_diff 1 $_bisect_option V ^U
149 + test_bisection_diff 0 $_bisect_option V ^U ^u1 ^u2 ^u3
150 + test_bisection_diff 0 $_bisect_option u1 ^U
151 + test_bisection_diff 0 $_bisect_option u2 ^U
152 + test_bisection_diff 0 $_bisect_option u3 ^U
153 + test_bisection_diff 0 $_bisect_option u4 ^U
154 + test_bisection_diff 0 $_bisect_option u5 ^U
155 +
156 +#
157 +# the following illustrates Linus' binary bug blatt idea.
158 +#
159 +# assume the bug is actually at l3, but you don't know that - all you know is that l3 is broken
160 +# and it wasn't broken before
161 +#
162 +# keep bisecting the list, advancing the "bad" head and accumulating "good" heads until
163 +# the bisection point is the head - this is the bad point.
164 +#
165 +
166 +test_output_expect_success "$_bisect_option l5 ^root" 'git rev-list $_bisect_option l5 ^root' <<EOF
167 +c3
168 +EOF
169 +
170 +test_output_expect_success "$_bisect_option l5 ^root ^c3" 'git rev-list $_bisect_option l5 ^root ^c3' <<EOF
171 +b4
172 +EOF
173 +
174 +test_output_expect_success "$_bisect_option l5 ^root ^c3 ^b4" 'git rev-list $_bisect_option l5 ^c3 ^b4' <<EOF
175 +l3
176 +EOF
177 +
178 +test_output_expect_success "$_bisect_option l3 ^root ^c3 ^b4" 'git rev-list $_bisect_option l3 ^root ^c3 ^b4' <<EOF
179 +a4
180 +EOF
181 +
182 +test_output_expect_success "$_bisect_option l5 ^b3 ^a3 ^b4 ^a4" 'git rev-list $_bisect_option l3 ^b3 ^a3 ^a4' <<EOF
183 +l3
184 +EOF
185 +
186 +#
187 +# if l3 is bad, then l4 is bad too - so advance the bad pointer by making b4 the known bad head
188 +#
189 +
190 +test_output_expect_success "$_bisect_option l4 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l4 ^a2 ^a3 ^a4' <<EOF
191 +l3
192 +EOF
193 +
194 +test_output_expect_success "$_bisect_option l3 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l3 ^a2 ^a3 ^a4' <<EOF
195 +l3
196 +EOF
197 +
198 +# found!
199 +
200 +#
201 +# as another example, let's consider a4 to be the bad head, in which case
202 +#
203 +
204 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
205 +c2
206 +EOF
207 +
208 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2' <<EOF
209 +c3
210 +EOF
211 +
212 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3' <<EOF
213 +a4
214 +EOF
215 +
216 +# found!
217 +
218 +#
219 +# or consider c3 to be the bad head
220 +#
221 +
222 +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
223 +c2
224 +EOF
225 +
226 +test_output_expect_success "$_bisect_option c3 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option c3 ^a2 ^a3 ^b4 ^c2' <<EOF
227 +c3
228 +EOF
229 +
230 +# found!
231 +
232 +}
233 +
234 +test_sequence "--bisect"
235 +
236 +#
237 +#
238 +
239 +test_expect_success 'set up fake --bisect refs' '
240 + git update-ref refs/bisect/bad c3 &&
241 + good=$(git rev-parse b1) &&
242 + git update-ref refs/bisect/good-$good $good &&
243 + good=$(git rev-parse c1) &&
244 + git update-ref refs/bisect/good-$good $good
245 +'
246 +
247 +test_expect_success 'rev-list --bisect can default to good/bad refs' '
248 + # the only thing between c3 and c1 is c2
249 + git rev-parse c2 >expect &&
250 + git rev-list --bisect >actual &&
251 + test_cmp expect actual
252 +'
253 +
254 +test_expect_success 'rev-parse --bisect can default to good/bad refs' '
255 + git rev-parse c3 ^b1 ^c1 >expect &&
256 + git rev-parse --bisect >actual &&
257 +
258 + # output order depends on the refnames, which in turn depends on
259 + # the exact sha1s. We just want to make sure we have the same set
260 + # of lines in any order.
261 + sort <expect >expect.sorted &&
262 + sort <actual >actual.sorted &&
263 + test_cmp expect.sorted actual.sorted
264 +'
265 +
266 +test_output_expect_success '--bisect --first-parent' 'git rev-list --bisect --first-parent E ^F' <<EOF
267 +e4
268 +EOF
269 +
270 +test_output_expect_success '--first-parent' 'git rev-list --first-parent E ^F' <<EOF
271 +E
272 +e1
273 +e2
274 +e3
275 +e4
276 +e5
277 +e6
278 +e7
279 +e8
280 +EOF
281 +
282 +test_output_expect_success '--bisect-vars --first-parent' 'git rev-list --bisect-vars --first-parent E ^F' <<EOF
283 +bisect_rev='e5'
284 +bisect_nr=4
285 +bisect_good=4
286 +bisect_bad=3
287 +bisect_all=9
288 +bisect_steps=2
289 +EOF
290 +
291 +test_expect_success '--bisect-all --first-parent' '
292 + cat >expect.unsorted <<-EOF &&
293 + $(git rev-parse E) (tag: E, dist=0)
294 + $(git rev-parse e1) (tag: e1, dist=1)
295 + $(git rev-parse e2) (tag: e2, dist=2)
296 + $(git rev-parse e3) (tag: e3, dist=3)
297 + $(git rev-parse e4) (tag: e4, dist=4)
298 + $(git rev-parse e5) (tag: e5, dist=4)
299 + $(git rev-parse e6) (tag: e6, dist=3)
300 + $(git rev-parse e7) (tag: e7, dist=2)
301 + $(git rev-parse e8) (tag: e8, dist=1)
302 + EOF
303 +
304 + # expect results to be ordered by distance (descending),
305 + # commit hash (ascending)
306 + sort -k4,4r -k1,1 expect.unsorted >expect &&
307 + git rev-list --bisect-all --first-parent E ^F >actual &&
308 + test_cmp expect actual
309 +'
310 +
311 +test_expect_success '--bisect without any revisions' '
312 + git rev-list --bisect HEAD..HEAD >out &&
313 + test_must_be_empty out
314 +'
315 +
316 +test_done