From: Lidong Yan <yldhome2d2@gmail.com>
To: gitster@pobox.com
Cc: git@vger.kernel.org, hi@arnes.space, michal@isc.org,
peff@peff.net, yldhome2d2@gmail.com
Subject: [PATCH v2] diff: ensure consistent diff behavior with -I<regex> across output formats
Date: Sun, 3 Aug 2025 22:51:55 +0800 [thread overview]
Message-ID: <20250803145155.57894-1-yldhome2d2@gmail.com> (raw)
In-Reply-To: <xmqqcy9io73j.fsf@gitster.g>
`git diff -I<regex>` option is inconsistently applied across various
output formats. In some cases, files would appear in the `--name-only`
output but not in the accompanying `--stat` or `-p` outputs, despite
the user explicitly requesting to ignore certain changes using
`-I<regex>`. Not only for `-I<regex>`, but this inconsistency also
exists for other output formats that have `.diff_from_content` set
(e.g. `-w`, `--ignore-space-at-eol` and `--ignore-space-change`).
To provide this consistency, introduces diffcore_ignore() to filter
out file pairs which only contains changes marked for ignoring before
flush diff output. Add test cases in t4015 and t4013 to verify that
files ignored by each ignore option do not appear in the output when
using the `--name-only` and `--name-status` options.
Signed-off-by: Lidong Yan <yldhome2d2@gmail.com>
---
Makefile | 1 +
diff.c | 2 +
diffcore-ignore.c | 151 +++++++++++++++++++++++++++++++++++++
diffcore.h | 1 +
meson.build | 1 +
t/t4013-diff-various.sh | 53 +++++++++++++
t/t4015-diff-whitespace.sh | 10 ++-
7 files changed, 217 insertions(+), 2 deletions(-)
create mode 100644 diffcore-ignore.c
diff --git a/Makefile b/Makefile
index 5f7dd79dfa..2cdbd5d3fb 100644
--- a/Makefile
+++ b/Makefile
@@ -1014,6 +1014,7 @@ LIB_OBJS += diff-no-index.o
LIB_OBJS += diff.o
LIB_OBJS += diffcore-break.o
LIB_OBJS += diffcore-delta.o
+LIB_OBJS += diffcore-ignore.o
LIB_OBJS += diffcore-order.o
LIB_OBJS += diffcore-pickaxe.o
LIB_OBJS += diffcore-rename.o
diff --git a/diff.c b/diff.c
index dca87e164f..1f905b5354 100644
--- a/diff.c
+++ b/diff.c
@@ -7088,6 +7088,8 @@ void diffcore_std(struct diff_options *options)
}
if (options->pickaxe_opts & DIFF_PICKAXE_KINDS_MASK)
diffcore_pickaxe(options);
+ if (options->flags.diff_from_contents)
+ diffcore_ignore(options);
if (options->orderfile)
diffcore_order(options->orderfile);
if (options->rotate_to)
diff --git a/diffcore-ignore.c b/diffcore-ignore.c
new file mode 100644
index 0000000000..2b80911449
--- /dev/null
+++ b/diffcore-ignore.c
@@ -0,0 +1,151 @@
+#include "git-compat-util.h"
+#include "diff.h"
+#include "diffcore.h"
+#include "xdiff-interface.h"
+
+#define KEEP 0
+#define IGNORE 1
+
+struct diffignore_cb {
+ regex_t **regex;
+ size_t sz;
+ int miss;
+};
+
+/*
+ * Check for any added or deleted line that does not match the ignore pattern.
+ * If found, mark data->miss=1 and return early.
+ */
+static int diffignore_consume(void *priv, char *line, unsigned long len)
+{
+ struct diffignore_cb *data = priv;
+
+ if (line[0] != '+' && line[0] != '-')
+ return 0;
+ if (data->miss)
+ BUG("Already matched in diffignore_consume! Broken xdiff_emit_line_fn?");
+
+ /* check if any of the regexes match */
+ data->miss = 1;
+ for (size_t nr = 0; nr < data->sz; nr++) {
+ regmatch_t regmatch;
+ regex_t *regex = data->regex[nr];
+
+ if (!regexec_buf(regex, line + 1, len - 1, 1,
+ ®match, 0)) {
+ data->miss = 0;
+ break;
+ }
+ }
+
+ /* stop looking for miss */
+ if (data->miss)
+ return 1;
+ return 0;
+}
+
+/* return IGNORE to ignore this change, return KEEP to keep it. */
+static int diff_ignore(mmfile_t *one, mmfile_t *two, struct diff_options *o)
+{
+ struct diffignore_cb ecbdata;
+ xpparam_t xpp;
+ xdemitconf_t xecfg;
+ int ret;
+
+ /*
+ * We have both sides; need to run textual diff and check if
+ * there are any unmatched (non-ignored) added or deleted lines.
+ */
+ memset(&xpp, 0, sizeof(xpp));
+ memset(&xecfg, 0, sizeof(xecfg));
+ ecbdata.regex = o->ignore_regex;
+ ecbdata.sz = o->ignore_regex_nr;
+ ecbdata.miss = 0;
+ xecfg.flags = XDL_EMIT_NO_HUNK_HDR;
+ xecfg.ctxlen = 0;
+ xecfg.interhunkctxlen = 0;
+ xpp.flags = o->xdl_opts & XDF_WHITESPACE_FLAGS;
+
+ /*
+ * An xdiff error might be our "data->miss" from above. See the
+ * comment for xdiff_emit_line_fn in xdiff-interface.h
+ */
+ ret = xdi_diff_outf(one, two, NULL, diffignore_consume,
+ &ecbdata, &xpp, &xecfg);
+
+ /* error happened, keep this change */
+ if (ret)
+ return KEEP;
+ /* If no regex matches, keep this change */
+ return ecbdata.miss ? KEEP : IGNORE;
+}
+
+/* return IGNORE to ignore this change, return KEEP to keep it. */
+static int ignore_match(struct diff_filepair *p, struct diff_options *o)
+{
+ struct userdiff_driver *textconv_one = NULL;
+ struct userdiff_driver *textconv_two = NULL;
+ mmfile_t mf1, mf2;
+ int ret;
+
+ /* keep unmerged */
+ if (DIFF_PAIR_UNMERGED(p))
+ return KEEP;
+
+ /* keep add/delete */
+ if (!DIFF_FILE_VALID(p->one) || !DIFF_FILE_VALID(p->two))
+ return KEEP;
+
+ /* keep mode changed pair */
+ if (DIFF_PAIR_MODE_CHANGED(p))
+ return KEEP;
+
+ if (o->flags.allow_textconv) {
+ textconv_one = get_textconv(o->repo, p->one);
+ textconv_two = get_textconv(o->repo, p->two);
+ }
+
+ /* unmodified pair will be ignored anyway */
+ if (textconv_one == textconv_two && diff_unmodified_pair(p))
+ return IGNORE;
+
+ /* keep binary files if diff cannot be performed */
+ if (!o->flags.text &&
+ ((!textconv_one && diff_filespec_is_binary(o->repo, p->one)) ||
+ (!textconv_two && diff_filespec_is_binary(o->repo, p->two))))
+ return KEEP;
+
+ mf1.size = fill_textconv(o->repo, textconv_one, p->one, &mf1.ptr);
+ mf2.size = fill_textconv(o->repo, textconv_two, p->two, &mf2.ptr);
+
+ ret = diff_ignore(&mf1, &mf2, o);
+
+ if (textconv_one)
+ free(mf1.ptr);
+ if (textconv_two)
+ free(mf2.ptr);
+ diff_free_filespec_data(p->one);
+ diff_free_filespec_data(p->two);
+
+ return ret;
+}
+
+void diffcore_ignore(struct diff_options *o)
+{
+ struct diff_queue_struct *q = &diff_queued_diff;
+ struct diff_queue_struct outq = DIFF_QUEUE_INIT;
+
+ if (!(o->output_format & (DIFF_FORMAT_NAME | DIFF_FORMAT_NAME_STATUS)))
+ return;
+
+ for (int i = 0; i < q->nr; i++) {
+ struct diff_filepair *p = q->queue[i];
+ if (ignore_match(p, o))
+ diff_free_filepair(p);
+ else
+ diff_q(&outq, p);
+ }
+
+ free(q->queue);
+ *q = outq;
+}
diff --git a/diffcore.h b/diffcore.h
index 9c0a0e7aaf..97e6e3553b 100644
--- a/diffcore.h
+++ b/diffcore.h
@@ -189,6 +189,7 @@ void diffcore_rename_extended(struct diff_options *options,
struct strmap *cached_pairs);
void diffcore_merge_broken(void);
void diffcore_pickaxe(struct diff_options *);
+void diffcore_ignore(struct diff_options *);
void diffcore_order(const char *orderfile);
void diffcore_rotate(struct diff_options *);
diff --git a/meson.build b/meson.build
index 92d62695e3..c95c9b92bd 100644
--- a/meson.build
+++ b/meson.build
@@ -329,6 +329,7 @@ libgit_sources = [
'diffcore-delta.c',
'diffcore-order.c',
'diffcore-pickaxe.c',
+ 'diffcore-ignore.c',
'diffcore-rename.c',
'diffcore-rotate.c',
'dir-iterator.c',
diff --git a/t/t4013-diff-various.sh b/t/t4013-diff-various.sh
index 8ebd170451..72eb7ae703 100755
--- a/t/t4013-diff-various.sh
+++ b/t/t4013-diff-various.sh
@@ -643,6 +643,59 @@ test_expect_success 'diff -I<regex> --stat' '
test_cmp expect actual
'
+test_expect_success 'diff -I<regex>: setup file1' '
+ test_seq 50 >file1 &&
+ git add file1 &&
+ test_seq 50 | sed -e "s/13/ten and three/" -e "s/^[124-9].*/& /" >file1
+'
+
+test_expect_success 'diff -I<regex>: ignore file1' '
+ git diff --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >actual &&
+ cat >expect <<-\EOF &&
+ diff --git a/file0 b/file0
+ --- a/file0
+ +++ b/file0
+ @@ -34,7 +31,6 @@
+ 34
+ 35
+ 36
+ -37
+ 38
+ 39
+ 40
+ EOF
+ compare_diff_patch expect actual &&
+
+ git diff --stat --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >actual &&
+ cat >expect <<-\EOF &&
+ file0 | 1 -
+ 1 file changed, 1 deletion(-)
+ EOF
+ test_cmp expect actual &&
+
+ git diff --name-status --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >actual &&
+ cat >expect <<-\EOF &&
+ M file0
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'diff -I<regex> --raw: --raw ignores -I<regex>' '
+ git diff --raw >expect &&
+ git diff --raw --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'diff -I<regex> --check: --check ignores -I<regex>' '
+ test_must_fail git diff --check 2>&1 >expect &&
+ test_must_fail git diff --check --ignore-blank-lines -I"ten.*e" -I"^[124-9]" 2>&1 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'diff -I<regex>: rm file1' '
+ git rm -f file1
+'
+
test_expect_success 'diff -I<regex>: detect malformed regex' '
test_expect_code 129 git diff --ignore-matching-lines="^[124-9" 2>error &&
test_grep "invalid regex given to -I: " error
diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh
index 52e3e476ff..6e46a070e5 100755
--- a/t/t4015-diff-whitespace.sh
+++ b/t/t4015-diff-whitespace.sh
@@ -11,7 +11,7 @@ test_description='Test special whitespace in diff engine.
. "$TEST_DIRECTORY"/lib-diff.sh
for opt_res in --patch --quiet -s --stat --shortstat --dirstat=lines \
- --raw! --name-only! --name-status!
+ --name-only --name-status --raw!
do
opts=${opt_res%!} expect_failure=
test "$opts" = "$opt_res" ||
@@ -43,7 +43,13 @@ do
echo foo >x &&
git add x &&
echo " foo" >x &&
- $expect_failure git diff -w $opts --exit-code x
+ $expect_failure git diff -w $opts --exit-code x &&
+ echo "foo " >x &&
+ $expect_failure git diff --ignore-space-at-eol $opts --exit-code x &&
+ echo "fo o" >x &&
+ git add x &&
+ echo "fo o " >x &&
+ $expect_failure git diff --ignore-space-change $opts --exit-code x
'
done
--
2.39.5 (Apple Git-154)
next prev parent reply other threads:[~2025-08-03 14:52 UTC|newest]
Thread overview: 35+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-07-23 5:47 git-diff: --ignore-matching-lines has no effect on the output when --name-only is used hi
2025-07-23 8:00 ` Lidong Yan
2025-07-23 17:09 ` Junio C Hamano
2025-07-24 1:56 ` Lidong Yan
2025-07-24 2:16 ` Eric Sunshine
2025-07-24 3:38 ` Lidong Yan
2025-07-25 6:00 ` hi
2025-07-25 6:06 ` hi
2025-07-25 6:46 ` Lidong Yan
2025-07-25 8:08 ` hi
2025-07-25 11:11 ` Jeff King
2025-07-25 15:20 ` Junio C Hamano
2025-07-29 8:18 ` [PATCH] diff: ensure consistent diff behavior with -I<regex> across output formats Lidong Yan
2025-07-30 0:28 ` Junio C Hamano
2025-08-02 10:22 ` Jeff King
2025-08-03 8:42 ` Lidong Yan
2025-08-03 15:43 ` Junio C Hamano
2025-08-04 4:39 ` Junio C Hamano
2025-08-04 12:42 ` Jeff King
2025-08-03 14:51 ` Lidong Yan [this message]
2025-08-04 0:39 ` [PATCH v2] " Junio C Hamano
2025-08-04 1:56 ` Lidong Yan
2025-08-04 4:36 ` Junio C Hamano
2025-08-05 9:23 ` Lidong Yan
2025-08-05 16:11 ` Junio C Hamano
2025-08-06 12:33 ` [PATCH v3] diff: ensure consistent diff behavior with ignore options Lidong Yan
2025-08-06 17:35 ` Junio C Hamano
2025-08-07 1:23 ` Lidong Yan
2025-08-06 20:56 ` Junio C Hamano
2025-08-07 1:39 ` Lidong Yan
2025-08-07 2:06 ` [PATCH v4] " Lidong Yan
2025-08-07 21:27 ` Junio C Hamano
2025-08-08 1:46 ` Lidong Yan
2025-08-08 3:30 ` [PATCH v5] " Lidong Yan
2025-10-16 14:55 ` Johannes Schindelin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250803145155.57894-1-yldhome2d2@gmail.com \
--to=yldhome2d2@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=hi@arnes.space \
--cc=michal@isc.org \
--cc=peff@peff.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).