diff: ensure consistent diff behavior with ignore options

In git-diff, options like `-w` and `-I<regex>`, two files are considered equivalent under the specified "ignore" rules, even when they are not bit-for-bit identical. For options like `--raw`, `--name-status`, and `--name-only`, git-diff deliberately compares only the SHA values to determine whether two files are equivalent, for performance reasons. As a result, a file shown in `git diff --name-status` may not appear in `git diff --patch`. To quickly determine whether two files are equivalent, add a helper function diff_flush_patch_quietly() in diff.c. Add `.dry_run` field in `struct diff_options`. When `.dry_run` is true, builtin_diff() returns immediately upon finding any change. Call diff_flush_patch_quietly() to determine if we should flush `--raw`, `--name-only` or `--name-status` output. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Lidong Yan <yldhome2d2@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Lidong Yan committed Aug 8, 2025 at 11:30 UTC b55e6d36ebce69136559add8fffd1a65df231518
5 files changed +70 -23
diff.c
+50 -14
@@ -2444,6 +2444,15 @@ static int fn_out_consume(void *priv, char *line, unsigned long len)
2444 return 0;
2445 }
2446
2447 +static int quick_consume(void *priv, char *line UNUSED, unsigned long len UNUSED)
2448 +{
2449 + struct emit_callback *ecbdata = priv;
2450 + struct diff_options *o = ecbdata->opt;
2451 +
2452 + o->found_changes = 1;
2453 + return 1;
2454 +}
2455 +
2456 static void pprint_rename(struct strbuf *name, const char *a, const char *b)
2457 {
2458 const char *old_name = a;
@@ -3759,8 +3768,21 @@ static void builtin_diff(const char *name_a,
3768
3769 if (o->word_diff)
3770 init_diff_words_data(&ecbdata, o, one, two);
3762 - if (xdi_diff_outf(&mf1, &mf2, NULL, fn_out_consume,
3763 - &ecbdata, &xpp, &xecfg))
3771 + if (o->dry_run) {
3772 + /*
3773 + * Unlike the !dry_run case, we need to ignore the
3774 + * return value from xdi_diff_outf() here, because
3775 + * xdi_diff_outf() takes non-zero return from its
3776 + * callback function as a sign of error and returns
3777 + * early (which is why we return non-zero from our
3778 + * callback, quick_consume()). Unfortunately,
3779 + * xdi_diff_outf() signals an error by returning
3780 + * non-zero.
3781 + */
3782 + xdi_diff_outf(&mf1, &mf2, NULL, quick_consume,
3783 + &ecbdata, &xpp, &xecfg);
3784 + } else if (xdi_diff_outf(&mf1, &mf2, NULL, fn_out_consume,
3785 + &ecbdata, &xpp, &xecfg))
3786 die("unable to generate diff for %s", one->path);
3787 if (o->word_diff)
3788 free_diff_words_data(&ecbdata);
@@ -6150,6 +6172,22 @@ static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)
6172 run_diff(p, o);
6173 }
6174
6175 +/* return 1 if any change is found; otherwise, return 0 */
6176 +static int diff_flush_patch_quietly(struct diff_filepair *p, struct diff_options *o)
6177 +{
6178 + int saved_dry_run = o->dry_run;
6179 + int saved_found_changes = o->found_changes;
6180 + int ret;
6181 +
6182 + o->dry_run = 1;
6183 + o->found_changes = 0;
6184 + diff_flush_patch(p, o);
6185 + ret = o->found_changes;
6186 + o->dry_run = saved_dry_run;
6187 + o->found_changes |= saved_found_changes;
6188 + return ret;
6189 +}
6190 +
6191 static void diff_flush_stat(struct diff_filepair *p, struct diff_options *o,
6192 struct diffstat_t *diffstat)
6193 {
@@ -6778,8 +6816,15 @@ void diff_flush(struct diff_options *options)
6816 DIFF_FORMAT_CHECKDIFF)) {
6817 for (i = 0; i < q->nr; i++) {
6818 struct diff_filepair *p = q->queue[i];
6781 - if (check_pair_status(p))
6782 - flush_one_pair(p, options);
6819 +
6820 + if (!check_pair_status(p))
6821 + continue;
6822 +
6823 + if (options->flags.diff_from_contents &&
6824 + !diff_flush_patch_quietly(p, options))
6825 + continue;
6826 +
6827 + flush_one_pair(p, options);
6828 }
6829 separator++;
6830 }
@@ -6831,19 +6876,10 @@ void diff_flush(struct diff_options *options)
6876 if (output_format & DIFF_FORMAT_NO_OUTPUT &&
6877 options->flags.exit_with_status &&
6878 options->flags.diff_from_contents) {
6834 - /*
6835 - * run diff_flush_patch for the exit status. setting
6836 - * options->file to /dev/null should be safe, because we
6837 - * aren't supposed to produce any output anyway.
6838 - */
6839 - diff_free_file(options);
6840 - options->file = xfopen("/dev/null", "w");
6841 - options->close_file = 1;
6842 - options->color_moved = 0;
6879 for (i = 0; i < q->nr; i++) {
6880 struct diff_filepair *p = q->queue[i];
6881 if (check_pair_status(p))
6846 - diff_flush_patch(p, options);
6882 + diff_flush_patch_quietly(p, options);
6883 if (options->found_changes)
6884 break;
6885 }
diff.h
+2
@@ -400,6 +400,8 @@ struct diff_options {
400 #define COLOR_MOVED_WS_ERROR (1<<0)
401 unsigned color_moved_ws_handling;
402
403 + bool dry_run;
404 +
405 struct repository *repo;
406 struct strmap *additional_path_headers;
407
t/t4013-diff-various.sh
+13
@@ -648,6 +648,19 @@ test_expect_success 'diff -I<regex>: detect malformed regex' '
648 test_grep "invalid regex given to -I: " error
649 '
650
651 +test_expect_success 'diff -I<regex>: ignore matching file' '
652 + test_when_finished "git rm -f file1" &&
653 + test_seq 50 >file1 &&
654 + git add file1 &&
655 + test_seq 50 | sed -e "s/13/ten and three/" -e "s/^[124-9].*/& /" >file1 &&
656 +
657 + : >actual &&
658 + git diff --raw --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >>actual &&
659 + git diff --name-only --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >>actual &&
660 + git diff --name-status --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >>actual &&
661 + test_grep ! "file1" actual
662 +'
663 +
664 # check_prefix <patch> <src> <dst>
665 # check only lines with paths to avoid dependency on exact oid/contents
666 check_prefix () {
t/t4015-diff-whitespace.sh
+2 -6
@@ -11,12 +11,8 @@ test_description='Test special whitespace in diff engine.
11 . "$TEST_DIRECTORY"/lib-diff.sh
12
13 for opt_res in --patch --quiet -s --stat --shortstat --dirstat=lines \
14 - --raw! --name-only! --name-status!
14 + --raw --name-only --name-status
15 do
16 - opts=${opt_res%!} expect_failure=
17 - test "$opts" = "$opt_res" ||
18 - expect_failure="test_expect_code 1"
19 -
16 test_expect_success "status with $opts (different)" '
17 echo foo >x &&
18 git add x &&
@@ -43,7 +39,7 @@ do
39 echo foo >x &&
40 git add x &&
41 echo " foo" >x &&
46 - $expect_failure git diff -w $opts --exit-code x
42 + git diff -w $opts --exit-code x
43 '
44 done
45
xdiff-interface.h
+3 -3
@@ -28,9 +28,9 @@
28 * from an error internal to xdiff, xdiff itself will see that
29 * non-zero return and translate it to -1.
30 *
31 - * See "diff_grep" in diffcore-pickaxe.c for a trick to work around
32 - * this, i.e. using the "consume_callback_data" to note the desired
33 - * early return.
31 + * See "diff_grep" in diffcore-pickaxe.c and "quick_consume" in diff.c
32 + * for a trick to work around this, i.e. using the "consume_callback_data"
33 + * to note the desired early return.
34 */
35 typedef int (*xdiff_emit_line_fn)(void *, char *, unsigned long);
36 typedef void (*xdiff_emit_hunk_fn)(void *data,