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>
This commit is contained in:
Michael Montalbo
2026-08-01 10:41:47 -07:00
committed by Junio C Hamano
parent 42824db113
commit 55b052a198
12 changed files with 1146 additions and 26 deletions

View File

@@ -170,6 +170,8 @@ int cmd_diff_tree(int argc,
opt->diffopt.rotate_to_strict = 1;
diff_hunks_attach(&opt->diffopt);
/*
* NOTE! We expect "a..b" to expand to "^a b" but it is
* perfectly valid for revision range parser to yield "b ^a",
@@ -234,5 +236,6 @@ int cmd_diff_tree(int argc,
diff_free(&opt->diffopt);
}
diff_hunks_detach(&opt->diffopt);
return diff_result_code(opt);
}

View File

@@ -568,6 +568,15 @@ int cmd_diff(int argc,
}
}
/*
* The hunk store is keyed by blob pair, so any diff whose
* file pairs carry known blob object IDs (tree-to-tree,
* index-to-tree) can consult the same entries that
* "git log --stat" and "git blame" use; pairs without known
* blobs bypass it at lookup time.
*/
diff_hunks_attach(&rev.diffopt);
symdiff_prepare(&rev, &sdiff);
for (i = 0; i < rev.pending.nr; i++) {
struct object_array_entry *entry = &rev.pending.objects[i];
@@ -648,6 +657,7 @@ int cmd_diff(int argc,
result = diff_result_code(&rev);
if (1 < rev.diffopt.skip_stat_unmatch)
refresh_index_quietly();
diff_hunks_detach(&rev.diffopt);
release_revisions(&rev);
object_array_clear(&ent);
symdiff_release(&sdiff);

View File

@@ -693,8 +693,11 @@ int cmd_show(int argc,
opt.tweak = show_setup_revisions_tweak;
cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);
diff_hunks_attach(&rev.diffopt);
if (!rev.no_walk) {
ret = cmd_log_walk(&rev);
diff_hunks_detach(&rev.diffopt);
release_revisions(&rev);
log_config_release(&cfg);
return ret;
@@ -767,6 +770,7 @@ int cmd_show(int argc,
}
rev.diffopt.no_free = 0;
diff_hunks_detach(&rev.diffopt);
diff_free(&rev.diffopt);
release_revisions(&rev);
log_config_release(&cfg);
@@ -846,8 +850,11 @@ int cmd_log(int argc,
opt.tweak = log_setup_revisions_tweak;
cmd_log_init(argc, argv, prefix, &rev, &opt, &cfg);
diff_hunks_attach(&rev.diffopt);
ret = cmd_log_walk(&rev);
diff_hunks_detach(&rev.diffopt);
release_revisions(&rev);
log_config_release(&cfg);
return ret;

View File

@@ -651,6 +651,38 @@ int diff_hunks_writer_add(struct diff_hunks_writer *w,
return 1;
}
void diff_hunks_writer_record_stable(struct diff_hunks_writer *w,
const struct object_id *old_oid,
const struct object_id *new_oid,
int xdl_opts,
const struct precomputed_hunk *trimmed,
size_t nr_trimmed,
const struct precomputed_hunk *full,
size_t nr_full)
{
size_t i;
if (!w)
return;
/*
* Record only a trim-stable pair, one whose trimmed and
* untrimmed diffs are identical, so the single entry answers
* any consumer at any context (see the top of this file). A
* pair where the two diffs differ is never recorded and every
* consumer computes it.
*/
if (nr_trimmed != nr_full)
return;
for (i = 0; i < nr_trimmed; i++)
if (trimmed[i].old_start != full[i].old_start ||
trimmed[i].old_count != full[i].old_count ||
trimmed[i].new_start != full[i].new_start ||
trimmed[i].new_count != full[i].new_count)
return;
diff_hunks_writer_add(w, old_oid, new_oid, xdl_opts,
trimmed, nr_trimmed);
}
/*
* Seed the writer with fname's entries so a rewrite preserves them,
* setting *pruned when the rewrite will not carry the whole file

View File

@@ -94,7 +94,8 @@ struct diff_hunks_writer *diff_hunks_writer_maybe_new(struct repository *r);
* Record a blob pair's hunks as computed under xdl_opts; a later lookup
* with a matching key is served these hunks. The caller must have
* checked that the pair's trimmed and untrimmed diffs are identical
* (see the top of this file), so the entry answers at any context.
* (see the top of this file), so the entry answers at any context;
* diff_hunks_writer_record_stable() below performs that check.
* NULL-safe. Returns 1 when the entry was recorded, 0 when the writer
* refused it (no hunks, a null object id, or values the on-disk
* 32-bit fields cannot hold).
@@ -106,6 +107,21 @@ int diff_hunks_writer_add(struct diff_hunks_writer *w,
const struct precomputed_hunk *hunks,
size_t nr_hunks);
/*
* Record the pair only if it is trim-stable: the recording caller
* hands over both the trimmed (xdi_diff) and untrimmed (xdl_diff)
* zero-context hunk sequences it computed, and the entry is added
* only when the two are identical. NULL-safe.
*/
void diff_hunks_writer_record_stable(struct diff_hunks_writer *w,
const struct object_id *old_oid,
const struct object_id *new_oid,
int xdl_opts,
const struct precomputed_hunk *trimmed,
size_t nr_trimmed,
const struct precomputed_hunk *full,
size_t nr_full);
/* Flush the accumulated entries to the store and free the writer. NULL-safe. */
void diff_hunks_writer_finish(struct diff_hunks_writer *w);

210
diff.c
View File

@@ -16,6 +16,7 @@
#include "revision.h"
#include "quote.h"
#include "diff.h"
#include "diff-hunks.h"
#include "diffcore.h"
#include "delta.h"
#include "hex.h"
@@ -2929,6 +2930,72 @@ static struct diffstat_file *diffstat_add(struct diffstat_t *diffstat,
return x;
}
struct diffstat_hunk_cb_data {
struct precomputed_hunk **h;
size_t *nr, *alloc;
};
/*
* Hunk callback that appends each hunk's coordinates to a growable
* array, so one xdiff pass can both sum a diffstat and record hunks for
* the store.
*/
static int diffstat_hunk_cb(long start_a, long count_a,
long start_b, long count_b,
void *cb_data)
{
struct diffstat_hunk_cb_data *d = cb_data;
ALLOC_GROW(*d->h, *d->nr + 1, *d->alloc);
(*d->h)[*d->nr].old_start = start_a;
(*d->h)[*d->nr].old_count = count_a;
(*d->h)[*d->nr].new_start = start_b;
(*d->h)[*d->nr].new_count = count_b;
(*d->nr)++;
return 0;
}
/*
* Collect the hunks of the two files at zero context. diff_fn chooses
* whether trimming runs: xdi_diff applies trim_common_tail, yielding the
* zero-context hunks blame reads; xdl_diff does not, yielding the
* untrimmed hunks. Both run at zero context, so the untrimmed hunks are
* not grouped the way a nonzero context would group them; diffstat only
* sums their counts, which grouping does not change. Sets *ph (caller
* frees) and *ph_nr.
*/
typedef int (*xdiff_fn)(mmfile_t *, mmfile_t *, xpparam_t const *,
xdemitconf_t const *, xdemitcb_t *);
static int collect_hunks(xdiff_fn diff_fn, mmfile_t *mf1, mmfile_t *mf2,
xpparam_t *xpp, struct precomputed_hunk **ph,
size_t *ph_nr)
{
size_t ph_alloc = 0;
xdemitcb_t ecb = { 0 };
xdemitconf_t xecfg = { 0 };
struct diffstat_hunk_cb_data cd = { ph, ph_nr, &ph_alloc };
*ph = NULL;
*ph_nr = 0;
xecfg.hunk_func = diffstat_hunk_cb;
ecb.priv = &cd;
return diff_fn(mf1, mf2, xpp, &xecfg, &ecb);
}
void diff_hunks_attach(struct diff_options *o)
{
if (!(o->output_format &
(DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_NUMSTAT)))
return;
o->hunks_writer = diff_hunks_writer_maybe_new(o->repo);
}
void diff_hunks_detach(struct diff_options *o)
{
diff_hunks_writer_finish(o->hunks_writer);
o->hunks_writer = NULL;
}
static int diffstat_consume(void *priv, char *line, unsigned long len)
{
struct diffstat_t *diffstat = priv;
@@ -4253,6 +4320,87 @@ static const char *get_compact_summary(const struct diff_filepair *p, int is_ren
return NULL;
}
/*
* Fill data->added/deleted for a modified pair by collecting its hunk
* coordinates, and record them into the store. Runs only on a warming
* run; returns 1 when it produced the counts, 0 when the caller must
* compute the diffstat itself.
*
* --ignore-blank-lines is excluded: that flag is part of the store
* key, but it coalesces hunks differently between the emit and
* hunk-callback paths, so a recorded entry would not match a
* store-less run's --stat output. (--inter-hunk-context is not
* excluded: it only groups hunks, and diffstat sums their counts,
* which grouping does not change.) Recording requires both sides to
* be valid regular files whose blobs the key can name.
*/
static int diffstat_from_hunks(struct diff_options *o,
struct diff_filespec *one,
struct diff_filespec *two,
struct diffstat_file *data)
{
struct precomputed_hunk *ph_trim, *ph_full, *counts;
size_t n_trim, n_full, n_counts, k;
mmfile_t mf1, mf2;
xpparam_t xpp = { .flags = o->xdl_opts,
.ignore_regex = o->ignore_regex,
.ignore_regex_nr = o->ignore_regex_nr,
.anchors = o->anchors,
.anchors_nr = o->anchors_nr };
if (o->xdl_opts & XDF_IGNORE_BLANK_LINES)
return 0;
/* Not a warming run: the caller computes the diffstat. */
if (!o->hunks_writer)
return 0;
/*
* -I patterns, --anchored anchors, and break detection (-B)
* shape the diff outside the store key, so what they compute
* must not be recorded under it.
*/
if (o->ignore_regex_nr || o->anchors_nr || o->break_opt != -1)
return 0;
/* Recording needs blobs the key can name, on both sides. */
if (!one->oid_valid || !two->oid_valid ||
S_ISGITLINK(one->mode) || S_ISGITLINK(two->mode) ||
!DIFF_FILE_VALID(one) || !DIFF_FILE_VALID(two) ||
!S_ISREG(one->mode) || !S_ISREG(two->mode))
return 0;
if (fill_mmfile(o->repo, &mf1, one) < 0 ||
fill_mmfile(o->repo, &mf2, two) < 0)
die("unable to read files to diff");
/*
* Compute the zero-context trimmed diff (what blame reads) and the
* untrimmed diff (whose counts a nonzero-context stat matches).
* xdi_diff runs first: it enforces the size limit, so the xdl_diff
* call is already bounded.
*/
if (collect_hunks(xdi_diff, &mf1, &mf2, &xpp, &ph_trim, &n_trim) ||
collect_hunks(xdl_diff, &mf1, &mf2, &xpp, &ph_full, &n_full))
die("unable to generate diffstat for %s", one->path);
/*
* Match a store-less run: at zero context xdi_diff trims, so sum the
* trimmed diff; otherwise sum the untrimmed one.
*/
counts = o->context ? ph_full : ph_trim;
n_counts = o->context ? n_full : n_trim;
for (k = 0; k < n_counts; k++) {
data->added += counts[k].new_count;
data->deleted += counts[k].old_count;
}
diff_hunks_writer_record_stable(o->hunks_writer, &one->oid, &two->oid,
o->xdl_opts, ph_trim, n_trim,
ph_full, n_full);
free(ph_trim);
free(ph_full);
return 1;
}
static void builtin_diffstat(const char *name_a, const char *name_b,
struct diff_filespec *one,
struct diff_filespec *two,
@@ -4304,38 +4452,50 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
}
else if (may_differ) {
/* Crazy xdl interfaces.. */
xpparam_t xpp;
xdemitconf_t xecfg;
/*
* Record into the diff-hunks store on a warming run. A
* "log -L" range-scoped stat is not the whole-pair diff
* the store keys, so it does not record. Otherwise diff
* normally.
*/
if (p->line_ranges || !diffstat_from_hunks(o, one, two, data)) {
/* Crazy xdl interfaces.. */
xpparam_t xpp;
xdemitconf_t xecfg;
if (fill_mmfile(o->repo, &mf1, one) < 0 ||
fill_mmfile(o->repo, &mf2, two) < 0)
die("unable to read files to diff");
if (fill_mmfile(o->repo, &mf1, one) < 0 ||
fill_mmfile(o->repo, &mf2, two) < 0)
die("unable to read files to diff");
memset(&xpp, 0, sizeof(xpp));
memset(&xecfg, 0, sizeof(xecfg));
xpp.flags = o->xdl_opts;
xpp.ignore_regex = o->ignore_regex;
xpp.ignore_regex_nr = o->ignore_regex_nr;
xpp.anchors = o->anchors;
xpp.anchors_nr = o->anchors_nr;
xecfg.ctxlen = o->context;
xecfg.interhunkctxlen = o->interhunkcontext;
xecfg.flags = XDL_EMIT_NO_HUNK_HDR;
memset(&xpp, 0, sizeof(xpp));
memset(&xecfg, 0, sizeof(xecfg));
xpp.flags = o->xdl_opts;
xpp.ignore_regex = o->ignore_regex;
xpp.ignore_regex_nr = o->ignore_regex_nr;
xpp.anchors = o->anchors;
xpp.anchors_nr = o->anchors_nr;
xecfg.ctxlen = o->context;
xecfg.interhunkctxlen = o->interhunkcontext;
xecfg.flags = XDL_EMIT_NO_HUNK_HDR;
if (p->line_ranges) {
struct line_range_filter lr_filter;
if (p->line_ranges) {
struct line_range_filter lr_filter;
line_range_filter_init(&lr_filter, p->line_ranges,
diffstat_consume, diffstat);
line_range_filter_init(&lr_filter,
p->line_ranges,
diffstat_consume,
diffstat);
if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
&xpp, &xecfg))
if (line_range_filter_diff(&lr_filter, &mf1,
&mf2, &xpp, &xecfg))
die("unable to generate diffstat for %s",
one->path);
} else if (xdi_diff_outf(&mf1, &mf2, NULL,
diffstat_consume, diffstat,
&xpp, &xecfg))
die("unable to generate diffstat for %s",
one->path);
} else if (xdi_diff_outf(&mf1, &mf2, NULL,
diffstat_consume, diffstat, &xpp, &xecfg))
die("unable to generate diffstat for %s", one->path);
}
if (DIFF_FILE_VALID(one) && DIFF_FILE_VALID(two)) {
struct diffstat_file *file =

17
diff.h
View File

@@ -420,6 +420,13 @@ struct diff_options {
*/
int max_depth;
int max_depth_valid;
/*
* Precomputed diff hunks (see diff-hunks.h). When hunks_writer is
* set (a warming run), diffstat records the hunks it computes;
* the writer is attached only for the stat output formats.
*/
struct diff_hunks_writer *hunks_writer;
};
unsigned diff_filter_bit(char status);
@@ -668,6 +675,16 @@ void diffcore_fix_diff_index(void);
int diff_queue_is_empty(struct diff_options *o);
void diff_flush(struct diff_options*);
void diff_free(struct diff_options*);
/*
* Attach a diff-hunks writer to a diff producing a stat format, so a
* warming run records the hunks it computes; a no-op when writing is off
* or for other formats. Pair with diff_hunks_detach() once the diff is
* done.
*/
void diff_hunks_attach(struct diff_options *o);
void diff_hunks_detach(struct diff_options *o);
void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);
/* diff-raw status letters */

View File

@@ -583,6 +583,7 @@ integration_tests = [
't4216-log-bloom.sh',
't4217-log-limit.sh',
't4219-log-follow-merge.sh',
't4220-diff-hunks.sh',
't4252-am-options.sh',
't4253-am-keep-cr-dos.sh',
't4254-am-corrupt.sh',

184
t/t4220-diff-hunks.sh Executable file
View File

@@ -0,0 +1,184 @@
#!/bin/sh
test_description='precomputed diff hunks store (git diff-hunks)
The store maps an (old blob, new blob, diff settings) key to the hunks of
diffing the pair. It is a cache: reading is on by default
(core.diffHunks), while writing is
off by default and enabled per run by GIT_DIFF_HUNKS_WRITE (or the
diffHunks.write config), so a diff or log warms the store only when the
owner opts in. These tests check that a warmed store never changes
output, that lookups honor the diff settings, and that a corrupt store is
read as absent while verify reports the corruption.'
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
. ./test-lib.sh
STORE=.git/objects/info/diff-hunks
# Warm the store the way a repository owner would: a stat walk with
# writing enabled. A --stat walk records one entry per trim-stable blob
# pair, serving blame and the summary formats alike. Extra arguments
# (e.g. -c options) are passed to git before "log".
warm () {
GIT_DIFF_HUNKS_WRITE=1 git "$@" log --all --stat >/dev/null
}
# Run a command with the store disabled, for ground truth.
no_store () {
git -c core.diffhunks=false "$@"
}
test_expect_success 'setup' '
test_commit initial file.txt "line 1" &&
test_commit second file.txt "line 1
line 2" &&
test_commit third file.txt "line 1
line 2
line 3" &&
test_commit fourth file.txt "changed line 1
line 2
line 3
line 4"
'
test_expect_success 'ordinary commands do not create the store' '
git log --stat >/dev/null &&
git blame file.txt >/dev/null &&
git diff --stat second third >/dev/null &&
test_path_is_missing $STORE
'
test_expect_success 'writing is gated by env and config, env wins' '
test_when_finished "git diff-hunks clear" &&
# The diffHunks.write config enables writing.
git -c diffHunks.write=true log --all --stat >/dev/null &&
test_path_is_file $STORE &&
git diff-hunks clear &&
# GIT_DIFF_HUNKS_WRITE overrides the config: 0 disables it.
GIT_DIFF_HUNKS_WRITE=0 git -c diffHunks.write=true log --all --stat >/dev/null &&
test_path_is_missing $STORE &&
# and enables it without any config.
GIT_DIFF_HUNKS_WRITE=1 git log --all --stat >/dev/null &&
test_path_is_file $STORE
'
test_expect_success 'a warm builds a store that verifies' '
warm &&
test_path_is_file $STORE &&
git diff-hunks verify
'
test_expect_success 'a second warming run refreshes the store in place' '
warm &&
test_commit fifth file.txt "brand new line" &&
warm &&
git diff-hunks verify &&
no_store log --stat >expect &&
git log --stat >actual &&
test_cmp expect actual
'
# A warming run displays the diffstat it computes. At zero context xdi_diff
# trims, so the displayed counts must be the trimmed ones (what a store-less
# run shows), not the untrimmed ones the writer compares against when it
# decides whether the pair is stable enough to record.
test_expect_success 'warming --stat at zero context matches a store-less run' '
git init -q warm-u0 &&
(
cd warm-u0 &&
cp "$TEST_DIRECTORY/t4220/trim-divergent-old" div.sh &&
git add div.sh && git commit -q -m old &&
cp "$TEST_DIRECTORY/t4220/trim-divergent-new" div.sh &&
git add div.sh && git commit -q -m new &&
git -c core.diffhunks=false log -1 --format= -U0 --stat -- div.sh >expect &&
GIT_DIFF_HUNKS_WRITE=1 git log -1 --format= -U0 --stat -- div.sh >got &&
test_cmp expect got
)
'
test_expect_success 'show and diff-tree --stat use the store' '
test_when_finished "git diff-hunks clear" &&
# diff_hunks_attach() runs for show and diff-tree: a write-enabled
# --stat records into the store (without the attach there is no
# writer, so nothing is written).
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git show --stat fourth >/dev/null &&
test_path_is_file "$STORE" &&
git diff-hunks clear &&
GIT_DIFF_HUNKS_WRITE=1 git diff-tree --stat fourth >/dev/null &&
test_path_is_file "$STORE" &&
# Reading never changes their output.
git diff-hunks clear &&
no_store show --stat fourth >expect_show &&
no_store diff-tree --stat fourth >expect_dt &&
warm &&
git show --stat fourth >got_show &&
git diff-tree --stat fourth >got_dt &&
test_cmp expect_show got_show &&
test_cmp expect_dt got_dt
'
# Cover the pair shapes an object walk encounters: binary and
# mode-only changes produce no text hunks to record.
test_expect_success 'binary and mode-only changes do not break the writer' '
printf "\\000\\001\\002" >bin.dat &&
git add bin.dat &&
git commit -m binary-1 &&
printf "\\000\\001\\003\\004" >bin.dat &&
git add bin.dat &&
git commit -m binary-2 &&
echo "mode content" >mode.txt &&
git add mode.txt &&
git commit -m mode-1 &&
test_chmod +x mode.txt &&
git commit -m mode-2 &&
no_store log --stat >expect &&
warm &&
git log --stat >actual &&
test_cmp expect actual
'
test_expect_success 'verify succeeds on a valid store and on an absent one' '
warm &&
git diff-hunks verify &&
git diff-hunks clear &&
test_path_is_missing $STORE &&
git diff-hunks verify
'
test_expect_success 'verify detects a checksum mismatch' '
test_when_finished "git diff-hunks clear" &&
warm &&
fsize=$(test_file_size $STORE) &&
mid=$((fsize / 2)) &&
printf "\\377" | dd of=$STORE bs=1 seek=$mid count=1 conv=notrunc 2>/dev/null &&
test_must_fail git diff-hunks verify
'
test_expect_success 'a warm discards a corrupt store rather than seeding from it' '
test_when_finished "git diff-hunks clear" &&
warm &&
# Corrupt the checksum: the next warm must not carry the corrupt
# entries forward into a fresh checksum-valid file; it discards
# them (with a warning) and rewrites a store that verifies.
fsize=$(test_file_size $STORE) &&
printf "\\377" | dd of=$STORE bs=1 seek=$((fsize / 2)) count=1 conv=notrunc 2>/dev/null &&
warm 2>err &&
test_grep "failed its checksum" err &&
git diff-hunks verify &&
no_store log --stat >expect &&
git log --stat >actual &&
test_cmp expect actual
'
test_expect_success 'diff-hunks clear removes the store file' '
warm &&
test_path_is_file $STORE &&
git diff-hunks clear &&
test_path_is_missing $STORE
'
test_done

55
t/t4220/README Normal file
View File

@@ -0,0 +1,55 @@
t4220 diff-hunks test fixtures
==============================
trim-divergent-old, trim-divergent-new
--------------------------------------
Two revisions of a single real file, used by t4220-diff-hunks.sh to
exercise a "trim-divergent" blob pair: one whose diff hunk counts change
with the amount of context, so the trimmed and untrimmed results
disagree.
They are two versions of git's own t/t6002-rev-list-bisect.sh, taken from
git.git history around:
090af9957c ("t6002: fix use of `expr` with `set -e`",
Patrick Steinhardt, 2026-04-21)
which rewrites `$(expr ...)` arithmetic as `$((...))` and reformats a few
test_expect_success blocks.
trim-divergent-old = 090af9957c^:t/t6002-rev-list-bisect.sh (blob daa009c9a1)
trim-divergent-new = 090af9957c :t/t6002-rev-list-bisect.sh (blob f2de40b5ed)
To regenerate them from any git.git checkout:
git show 090af9957c^:t/t6002-rev-list-bisect.sh >trim-divergent-old
git show 090af9957c:t/t6002-rev-list-bisect.sh >trim-divergent-new
Why this pair
-------------
The diff-hunks store records only "trim-stable" pairs: those whose hunks
are identical whether or not xdiff trims the common head and tail (which
it does at zero context, in trim_common_tail). This pair is deliberately
NOT trim-stable:
diff -U0 reports 9 added / 6 deleted
diff -U3 reports 10 added / 7 deleted
Because the counts diverge with context, the writer must refuse to record
this pair and every command must recompute it from the blobs. t4220 uses
it to prove that the displayed counts stay correct at each context, and
that a divergent pair is never served from the store. See
t4220-diff-hunks.sh ("a trim-divergent file is correct at each context"
and the store-poison test).
Why not a synthesized fixture
-----------------------------
The divergence needs real content that makes xdiff's common-tail trimming
shift a hunk boundary while the added/deleted balance stays equal. A
minimal hand-written file that reliably triggers the -U0 vs -U3 count
disagreement has not been found yet; until one is, this real pair is kept
verbatim. If you synthesize a smaller equivalent, replace these two files
and delete this note.

319
t/t4220/trim-divergent-new Normal file
View File

@@ -0,0 +1,319 @@
#!/bin/sh
#
# Copyright (c) 2005 Jon Seymour
#
test_description='Tests git rev-list --bisect functionality'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-t6000.sh # t6xxx specific functions
# usage: test_bisection max-diff bisect-option head ^prune...
#
# e.g. test_bisection 1 --bisect l1 ^l0
#
test_bisection_diff()
{
_max_diff=$1
_bisect_option=$2
shift 2
_bisection=$(git rev-list $_bisect_option "$@")
_list_size=$(git rev-list "$@" | wc -l)
_head=$1
shift 1
_bisection_size=$(git rev-list $_bisection "$@" | wc -l)
[ -n "$_list_size" -a -n "$_bisection_size" ] ||
error "test_bisection_diff failed"
# Test if bisection size is close to half of list size within
# tolerance.
#
_bisect_err=$(($_list_size - $_bisection_size * 2))
if test "$_bisect_err" -lt 0
then
_bisect_err=$((0 - $_bisect_err))
fi
_bisect_err=$(($_bisect_err / 2)) ; # floor
test_expect_success "bisection diff $_bisect_option $_head $* <= $_max_diff" '
test $_bisect_err -le $_max_diff
'
}
date >path0
git update-index --add path0
save_tag tree git write-tree
on_committer_date "00:00" hide_error save_tag root unique_commit root tree
on_committer_date "00:01" save_tag l0 unique_commit l0 tree -p root
on_committer_date "00:02" save_tag l1 unique_commit l1 tree -p l0
on_committer_date "00:03" save_tag l2 unique_commit l2 tree -p l1
on_committer_date "00:04" save_tag a0 unique_commit a0 tree -p l2
on_committer_date "00:05" save_tag a1 unique_commit a1 tree -p a0
on_committer_date "00:06" save_tag b1 unique_commit b1 tree -p a0
on_committer_date "00:07" save_tag c1 unique_commit c1 tree -p b1
on_committer_date "00:08" save_tag b2 unique_commit b2 tree -p b1
on_committer_date "00:09" save_tag b3 unique_commit b2 tree -p b2
on_committer_date "00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2
on_committer_date "00:11" save_tag c3 unique_commit c3 tree -p c2
on_committer_date "00:12" save_tag a2 unique_commit a2 tree -p a1
on_committer_date "00:13" save_tag a3 unique_commit a3 tree -p a2
on_committer_date "00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3
on_committer_date "00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3
on_committer_date "00:16" save_tag l3 unique_commit l3 tree -p a4
on_committer_date "00:17" save_tag l4 unique_commit l4 tree -p l3
on_committer_date "00:18" save_tag l5 unique_commit l5 tree -p l4
git update-ref HEAD $(tag l5)
# E
# / \
# e1 |
# | |
# e2 |
# | |
# e3 |
# | |
# e4 |
# | |
# | f1
# | |
# | f2
# | |
# | f3
# | |
# | f4
# | |
# e5 |
# | |
# e6 |
# | |
# e7 |
# | |
# e8 |
# \ /
# F
on_committer_date "00:00" hide_error save_tag F unique_commit F tree
on_committer_date "00:01" save_tag e8 unique_commit e8 tree -p F
on_committer_date "00:02" save_tag e7 unique_commit e7 tree -p e8
on_committer_date "00:03" save_tag e6 unique_commit e6 tree -p e7
on_committer_date "00:04" save_tag e5 unique_commit e5 tree -p e6
on_committer_date "00:05" save_tag f4 unique_commit f4 tree -p F
on_committer_date "00:06" save_tag f3 unique_commit f3 tree -p f4
on_committer_date "00:07" save_tag f2 unique_commit f2 tree -p f3
on_committer_date "00:08" save_tag f1 unique_commit f1 tree -p f2
on_committer_date "00:09" save_tag e4 unique_commit e4 tree -p e5
on_committer_date "00:10" save_tag e3 unique_commit e3 tree -p e4
on_committer_date "00:11" save_tag e2 unique_commit e2 tree -p e3
on_committer_date "00:12" save_tag e1 unique_commit e1 tree -p e2
on_committer_date "00:13" save_tag E unique_commit E tree -p e1 -p f1
on_committer_date "00:00" hide_error save_tag U unique_commit U tree
on_committer_date "00:01" save_tag u0 unique_commit u0 tree -p U
on_committer_date "00:01" save_tag u1 unique_commit u1 tree -p u0
on_committer_date "00:02" save_tag u2 unique_commit u2 tree -p u0
on_committer_date "00:03" save_tag u3 unique_commit u3 tree -p u0
on_committer_date "00:04" save_tag u4 unique_commit u4 tree -p u0
on_committer_date "00:05" save_tag u5 unique_commit u5 tree -p u0
on_committer_date "00:06" save_tag V unique_commit V tree -p u1 -p u2 -p u3 -p u4 -p u5
test_sequence()
{
_bisect_option=$1
test_bisection_diff 0 $_bisect_option l0 ^root
test_bisection_diff 0 $_bisect_option l1 ^root
test_bisection_diff 0 $_bisect_option l2 ^root
test_bisection_diff 0 $_bisect_option a0 ^root
test_bisection_diff 0 $_bisect_option a1 ^root
test_bisection_diff 0 $_bisect_option a2 ^root
test_bisection_diff 0 $_bisect_option a3 ^root
test_bisection_diff 0 $_bisect_option b1 ^root
test_bisection_diff 0 $_bisect_option b2 ^root
test_bisection_diff 0 $_bisect_option b3 ^root
test_bisection_diff 0 $_bisect_option c1 ^root
test_bisection_diff 0 $_bisect_option c2 ^root
test_bisection_diff 0 $_bisect_option c3 ^root
test_bisection_diff 0 $_bisect_option E ^F
test_bisection_diff 0 $_bisect_option e1 ^F
test_bisection_diff 0 $_bisect_option e2 ^F
test_bisection_diff 0 $_bisect_option e3 ^F
test_bisection_diff 0 $_bisect_option e4 ^F
test_bisection_diff 0 $_bisect_option e5 ^F
test_bisection_diff 0 $_bisect_option e6 ^F
test_bisection_diff 0 $_bisect_option e7 ^F
test_bisection_diff 0 $_bisect_option f1 ^F
test_bisection_diff 0 $_bisect_option f2 ^F
test_bisection_diff 0 $_bisect_option f3 ^F
test_bisection_diff 0 $_bisect_option f4 ^F
test_bisection_diff 0 $_bisect_option E ^F
test_bisection_diff 1 $_bisect_option V ^U
test_bisection_diff 0 $_bisect_option V ^U ^u1 ^u2 ^u3
test_bisection_diff 0 $_bisect_option u1 ^U
test_bisection_diff 0 $_bisect_option u2 ^U
test_bisection_diff 0 $_bisect_option u3 ^U
test_bisection_diff 0 $_bisect_option u4 ^U
test_bisection_diff 0 $_bisect_option u5 ^U
#
# the following illustrates Linus' binary bug blatt idea.
#
# assume the bug is actually at l3, but you don't know that - all you know is that l3 is broken
# and it wasn't broken before
#
# keep bisecting the list, advancing the "bad" head and accumulating "good" heads until
# the bisection point is the head - this is the bad point.
#
test_output_expect_success "$_bisect_option l5 ^root" 'git rev-list $_bisect_option l5 ^root' <<EOF
c3
EOF
test_output_expect_success "$_bisect_option l5 ^root ^c3" 'git rev-list $_bisect_option l5 ^root ^c3' <<EOF
b4
EOF
test_output_expect_success "$_bisect_option l5 ^root ^c3 ^b4" 'git rev-list $_bisect_option l5 ^c3 ^b4' <<EOF
l3
EOF
test_output_expect_success "$_bisect_option l3 ^root ^c3 ^b4" 'git rev-list $_bisect_option l3 ^root ^c3 ^b4' <<EOF
a4
EOF
test_output_expect_success "$_bisect_option l5 ^b3 ^a3 ^b4 ^a4" 'git rev-list $_bisect_option l3 ^b3 ^a3 ^a4' <<EOF
l3
EOF
#
# if l3 is bad, then l4 is bad too - so advance the bad pointer by making b4 the known bad head
#
test_output_expect_success "$_bisect_option l4 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l4 ^a2 ^a3 ^a4' <<EOF
l3
EOF
test_output_expect_success "$_bisect_option l3 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l3 ^a2 ^a3 ^a4' <<EOF
l3
EOF
# found!
#
# as another example, let's consider a4 to be the bad head, in which case
#
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
c2
EOF
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2' <<EOF
c3
EOF
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3' <<EOF
a4
EOF
# found!
#
# or consider c3 to be the bad head
#
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
c2
EOF
test_output_expect_success "$_bisect_option c3 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option c3 ^a2 ^a3 ^b4 ^c2' <<EOF
c3
EOF
# found!
}
test_sequence "--bisect"
#
#
test_expect_success 'set up fake --bisect refs' '
git update-ref refs/bisect/bad c3 &&
good=$(git rev-parse b1) &&
git update-ref refs/bisect/good-$good $good &&
good=$(git rev-parse c1) &&
git update-ref refs/bisect/good-$good $good
'
test_expect_success 'rev-list --bisect can default to good/bad refs' '
# the only thing between c3 and c1 is c2
git rev-parse c2 >expect &&
git rev-list --bisect >actual &&
test_cmp expect actual
'
test_expect_success 'rev-parse --bisect can default to good/bad refs' '
git rev-parse c3 ^b1 ^c1 >expect &&
git rev-parse --bisect >actual &&
# output order depends on the refnames, which in turn depends on
# the exact sha1s. We just want to make sure we have the same set
# of lines in any order.
sort <expect >expect.sorted &&
sort <actual >actual.sorted &&
test_cmp expect.sorted actual.sorted
'
test_output_expect_success '--bisect --first-parent' 'git rev-list --bisect --first-parent E ^F' <<EOF
e4
EOF
test_output_expect_success '--first-parent' 'git rev-list --first-parent E ^F' <<EOF
E
e1
e2
e3
e4
e5
e6
e7
e8
EOF
test_output_expect_success '--bisect-vars --first-parent' 'git rev-list --bisect-vars --first-parent E ^F' <<EOF
bisect_rev='e5'
bisect_nr=4
bisect_good=4
bisect_bad=3
bisect_all=9
bisect_steps=2
EOF
test_expect_success '--bisect-all --first-parent' '
cat >expect.unsorted <<-EOF &&
$(git rev-parse E) (tag: E, dist=0)
$(git rev-parse e1) (tag: e1, dist=1)
$(git rev-parse e2) (tag: e2, dist=2)
$(git rev-parse e3) (tag: e3, dist=3)
$(git rev-parse e4) (tag: e4, dist=4)
$(git rev-parse e5) (tag: e5, dist=4)
$(git rev-parse e6) (tag: e6, dist=3)
$(git rev-parse e7) (tag: e7, dist=2)
$(git rev-parse e8) (tag: e8, dist=1)
EOF
# expect results to be ordered by distance (descending),
# commit hash (ascending)
sort -k4,4r -k1,1 expect.unsorted >expect &&
git rev-list --bisect-all --first-parent E ^F >actual &&
test_cmp expect actual
'
test_expect_success '--bisect without any revisions' '
git rev-list --bisect HEAD..HEAD >out &&
test_must_be_empty out
'
test_done

316
t/t4220/trim-divergent-old Normal file
View File

@@ -0,0 +1,316 @@
#!/bin/sh
#
# Copyright (c) 2005 Jon Seymour
#
test_description='Tests git rev-list --bisect functionality'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-t6000.sh # t6xxx specific functions
# usage: test_bisection max-diff bisect-option head ^prune...
#
# e.g. test_bisection 1 --bisect l1 ^l0
#
test_bisection_diff()
{
_max_diff=$1
_bisect_option=$2
shift 2
_bisection=$(git rev-list $_bisect_option "$@")
_list_size=$(git rev-list "$@" | wc -l)
_head=$1
shift 1
_bisection_size=$(git rev-list $_bisection "$@" | wc -l)
[ -n "$_list_size" -a -n "$_bisection_size" ] ||
error "test_bisection_diff failed"
# Test if bisection size is close to half of list size within
# tolerance.
#
_bisect_err=$(expr $_list_size - $_bisection_size \* 2)
test "$_bisect_err" -lt 0 && _bisect_err=$(expr 0 - $_bisect_err)
_bisect_err=$(expr $_bisect_err / 2) ; # floor
test_expect_success \
"bisection diff $_bisect_option $_head $* <= $_max_diff" \
'test $_bisect_err -le $_max_diff'
}
date >path0
git update-index --add path0
save_tag tree git write-tree
on_committer_date "00:00" hide_error save_tag root unique_commit root tree
on_committer_date "00:01" save_tag l0 unique_commit l0 tree -p root
on_committer_date "00:02" save_tag l1 unique_commit l1 tree -p l0
on_committer_date "00:03" save_tag l2 unique_commit l2 tree -p l1
on_committer_date "00:04" save_tag a0 unique_commit a0 tree -p l2
on_committer_date "00:05" save_tag a1 unique_commit a1 tree -p a0
on_committer_date "00:06" save_tag b1 unique_commit b1 tree -p a0
on_committer_date "00:07" save_tag c1 unique_commit c1 tree -p b1
on_committer_date "00:08" save_tag b2 unique_commit b2 tree -p b1
on_committer_date "00:09" save_tag b3 unique_commit b2 tree -p b2
on_committer_date "00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2
on_committer_date "00:11" save_tag c3 unique_commit c3 tree -p c2
on_committer_date "00:12" save_tag a2 unique_commit a2 tree -p a1
on_committer_date "00:13" save_tag a3 unique_commit a3 tree -p a2
on_committer_date "00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3
on_committer_date "00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3
on_committer_date "00:16" save_tag l3 unique_commit l3 tree -p a4
on_committer_date "00:17" save_tag l4 unique_commit l4 tree -p l3
on_committer_date "00:18" save_tag l5 unique_commit l5 tree -p l4
git update-ref HEAD $(tag l5)
# E
# / \
# e1 |
# | |
# e2 |
# | |
# e3 |
# | |
# e4 |
# | |
# | f1
# | |
# | f2
# | |
# | f3
# | |
# | f4
# | |
# e5 |
# | |
# e6 |
# | |
# e7 |
# | |
# e8 |
# \ /
# F
on_committer_date "00:00" hide_error save_tag F unique_commit F tree
on_committer_date "00:01" save_tag e8 unique_commit e8 tree -p F
on_committer_date "00:02" save_tag e7 unique_commit e7 tree -p e8
on_committer_date "00:03" save_tag e6 unique_commit e6 tree -p e7
on_committer_date "00:04" save_tag e5 unique_commit e5 tree -p e6
on_committer_date "00:05" save_tag f4 unique_commit f4 tree -p F
on_committer_date "00:06" save_tag f3 unique_commit f3 tree -p f4
on_committer_date "00:07" save_tag f2 unique_commit f2 tree -p f3
on_committer_date "00:08" save_tag f1 unique_commit f1 tree -p f2
on_committer_date "00:09" save_tag e4 unique_commit e4 tree -p e5
on_committer_date "00:10" save_tag e3 unique_commit e3 tree -p e4
on_committer_date "00:11" save_tag e2 unique_commit e2 tree -p e3
on_committer_date "00:12" save_tag e1 unique_commit e1 tree -p e2
on_committer_date "00:13" save_tag E unique_commit E tree -p e1 -p f1
on_committer_date "00:00" hide_error save_tag U unique_commit U tree
on_committer_date "00:01" save_tag u0 unique_commit u0 tree -p U
on_committer_date "00:01" save_tag u1 unique_commit u1 tree -p u0
on_committer_date "00:02" save_tag u2 unique_commit u2 tree -p u0
on_committer_date "00:03" save_tag u3 unique_commit u3 tree -p u0
on_committer_date "00:04" save_tag u4 unique_commit u4 tree -p u0
on_committer_date "00:05" save_tag u5 unique_commit u5 tree -p u0
on_committer_date "00:06" save_tag V unique_commit V tree -p u1 -p u2 -p u3 -p u4 -p u5
test_sequence()
{
_bisect_option=$1
test_bisection_diff 0 $_bisect_option l0 ^root
test_bisection_diff 0 $_bisect_option l1 ^root
test_bisection_diff 0 $_bisect_option l2 ^root
test_bisection_diff 0 $_bisect_option a0 ^root
test_bisection_diff 0 $_bisect_option a1 ^root
test_bisection_diff 0 $_bisect_option a2 ^root
test_bisection_diff 0 $_bisect_option a3 ^root
test_bisection_diff 0 $_bisect_option b1 ^root
test_bisection_diff 0 $_bisect_option b2 ^root
test_bisection_diff 0 $_bisect_option b3 ^root
test_bisection_diff 0 $_bisect_option c1 ^root
test_bisection_diff 0 $_bisect_option c2 ^root
test_bisection_diff 0 $_bisect_option c3 ^root
test_bisection_diff 0 $_bisect_option E ^F
test_bisection_diff 0 $_bisect_option e1 ^F
test_bisection_diff 0 $_bisect_option e2 ^F
test_bisection_diff 0 $_bisect_option e3 ^F
test_bisection_diff 0 $_bisect_option e4 ^F
test_bisection_diff 0 $_bisect_option e5 ^F
test_bisection_diff 0 $_bisect_option e6 ^F
test_bisection_diff 0 $_bisect_option e7 ^F
test_bisection_diff 0 $_bisect_option f1 ^F
test_bisection_diff 0 $_bisect_option f2 ^F
test_bisection_diff 0 $_bisect_option f3 ^F
test_bisection_diff 0 $_bisect_option f4 ^F
test_bisection_diff 0 $_bisect_option E ^F
test_bisection_diff 1 $_bisect_option V ^U
test_bisection_diff 0 $_bisect_option V ^U ^u1 ^u2 ^u3
test_bisection_diff 0 $_bisect_option u1 ^U
test_bisection_diff 0 $_bisect_option u2 ^U
test_bisection_diff 0 $_bisect_option u3 ^U
test_bisection_diff 0 $_bisect_option u4 ^U
test_bisection_diff 0 $_bisect_option u5 ^U
#
# the following illustrates Linus' binary bug blatt idea.
#
# assume the bug is actually at l3, but you don't know that - all you know is that l3 is broken
# and it wasn't broken before
#
# keep bisecting the list, advancing the "bad" head and accumulating "good" heads until
# the bisection point is the head - this is the bad point.
#
test_output_expect_success "$_bisect_option l5 ^root" 'git rev-list $_bisect_option l5 ^root' <<EOF
c3
EOF
test_output_expect_success "$_bisect_option l5 ^root ^c3" 'git rev-list $_bisect_option l5 ^root ^c3' <<EOF
b4
EOF
test_output_expect_success "$_bisect_option l5 ^root ^c3 ^b4" 'git rev-list $_bisect_option l5 ^c3 ^b4' <<EOF
l3
EOF
test_output_expect_success "$_bisect_option l3 ^root ^c3 ^b4" 'git rev-list $_bisect_option l3 ^root ^c3 ^b4' <<EOF
a4
EOF
test_output_expect_success "$_bisect_option l5 ^b3 ^a3 ^b4 ^a4" 'git rev-list $_bisect_option l3 ^b3 ^a3 ^a4' <<EOF
l3
EOF
#
# if l3 is bad, then l4 is bad too - so advance the bad pointer by making b4 the known bad head
#
test_output_expect_success "$_bisect_option l4 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l4 ^a2 ^a3 ^a4' <<EOF
l3
EOF
test_output_expect_success "$_bisect_option l3 ^a2 ^a3 ^b ^a4" 'git rev-list $_bisect_option l3 ^a2 ^a3 ^a4' <<EOF
l3
EOF
# found!
#
# as another example, let's consider a4 to be the bad head, in which case
#
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
c2
EOF
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2' <<EOF
c3
EOF
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3' <<EOF
a4
EOF
# found!
#
# or consider c3 to be the bad head
#
test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF
c2
EOF
test_output_expect_success "$_bisect_option c3 ^a2 ^a3 ^b4 ^c2" 'git rev-list $_bisect_option c3 ^a2 ^a3 ^b4 ^c2' <<EOF
c3
EOF
# found!
}
test_sequence "--bisect"
#
#
test_expect_success 'set up fake --bisect refs' '
git update-ref refs/bisect/bad c3 &&
good=$(git rev-parse b1) &&
git update-ref refs/bisect/good-$good $good &&
good=$(git rev-parse c1) &&
git update-ref refs/bisect/good-$good $good
'
test_expect_success 'rev-list --bisect can default to good/bad refs' '
# the only thing between c3 and c1 is c2
git rev-parse c2 >expect &&
git rev-list --bisect >actual &&
test_cmp expect actual
'
test_expect_success 'rev-parse --bisect can default to good/bad refs' '
git rev-parse c3 ^b1 ^c1 >expect &&
git rev-parse --bisect >actual &&
# output order depends on the refnames, which in turn depends on
# the exact sha1s. We just want to make sure we have the same set
# of lines in any order.
sort <expect >expect.sorted &&
sort <actual >actual.sorted &&
test_cmp expect.sorted actual.sorted
'
test_output_expect_success '--bisect --first-parent' 'git rev-list --bisect --first-parent E ^F' <<EOF
e4
EOF
test_output_expect_success '--first-parent' 'git rev-list --first-parent E ^F' <<EOF
E
e1
e2
e3
e4
e5
e6
e7
e8
EOF
test_output_expect_success '--bisect-vars --first-parent' 'git rev-list --bisect-vars --first-parent E ^F' <<EOF
bisect_rev='e5'
bisect_nr=4
bisect_good=4
bisect_bad=3
bisect_all=9
bisect_steps=2
EOF
test_expect_success '--bisect-all --first-parent' '
cat >expect.unsorted <<-EOF &&
$(git rev-parse E) (tag: E, dist=0)
$(git rev-parse e1) (tag: e1, dist=1)
$(git rev-parse e2) (tag: e2, dist=2)
$(git rev-parse e3) (tag: e3, dist=3)
$(git rev-parse e4) (tag: e4, dist=4)
$(git rev-parse e5) (tag: e5, dist=4)
$(git rev-parse e6) (tag: e6, dist=3)
$(git rev-parse e7) (tag: e7, dist=2)
$(git rev-parse e8) (tag: e8, dist=1)
EOF
# expect results to be ordered by distance (descending),
# commit hash (ascending)
sort -k4,4r -k1,1 expect.unsorted >expect &&
git rev-list --bisect-all --first-parent E ^F >actual &&
test_cmp expect actual
'
test_expect_success '--bisect without any revisions' '
git rev-list --bisect HEAD..HEAD >out &&
test_must_be_empty out
'
test_done