diff: add -I<regex> that ignores matching changes

Add a new diff option that enables ignoring changes whose all lines (changed, removed, and added) match a given regular expression. This is similar to the -I/--ignore-matching-lines option in standalone diff utilities and can be used e.g. to ignore changes which only affect code comments or to look for unrelated changes in commits containing a large number of automatically applied modifications (e.g. a tree-wide string replacement). The difference between -G/-S and the new -I option is that the latter filters output on a per-change basis. Use the 'ignore' field of xdchange_t for marking a change as ignored or not. Since the same field is used by --ignore-blank-lines, identical hunk emitting rules apply for --ignore-blank-lines and -I. These two options can also be used together in the same git invocation (they are complementary to each other). Rename xdl_mark_ignorable() to xdl_mark_ignorable_lines(), to indicate that it is logically a "sibling" of xdl_mark_ignorable_regex() rather than its "parent". Signed-off-by: Michał Kępień <michal@isc.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michał Kępień committed Oct 20, 2020 at 08:48 UTC 296d4a94e7231a1d57356889f51bff57a1a3c5a1
7 files changed +221 -2
Documentation/diff-options.txt
+5
@@ -681,6 +681,11 @@ endif::git-format-patch[]
681 --ignore-blank-lines::
682 Ignore changes whose lines are all blank.
683
684 +-I<regex>::
685 +--ignore-matching-lines=<regex>::
686 + Ignore changes whose all lines match <regex>. This option may
687 + be specified more than once.
688 +
689 --inter-hunk-context=<lines>::
690 Show the context between diff hunks, up to the specified number
691 of lines, thereby fusing hunks that are close to each other.
diff.c
+23
@@ -3584,6 +3584,8 @@ static void builtin_diff(const char *name_a,
3584 if (header.len && !o->flags.suppress_diff_headers)
3585 ecbdata.header = &header;
3586 xpp.flags = o->xdl_opts;
3587 + xpp.ignore_regex = o->ignore_regex;
3588 + xpp.ignore_regex_nr = o->ignore_regex_nr;
3589 xpp.anchors = o->anchors;
3590 xpp.anchors_nr = o->anchors_nr;
3591 xecfg.ctxlen = o->context;
@@ -3711,6 +3713,8 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
3713 memset(&xpp, 0, sizeof(xpp));
3714 memset(&xecfg, 0, sizeof(xecfg));
3715 xpp.flags = o->xdl_opts;
3716 + xpp.ignore_regex = o->ignore_regex;
3717 + xpp.ignore_regex_nr = o->ignore_regex_nr;
3718 xpp.anchors = o->anchors;
3719 xpp.anchors_nr = o->anchors_nr;
3720 xecfg.ctxlen = o->context;
@@ -5174,6 +5178,22 @@ static int diff_opt_patience(const struct option *opt,
5178 return 0;
5179 }
5180
5181 +static int diff_opt_ignore_regex(const struct option *opt,
5182 + const char *arg, int unset)
5183 +{
5184 + struct diff_options *options = opt->value;
5185 + regex_t *regex;
5186 +
5187 + BUG_ON_OPT_NEG(unset);
5188 + regex = xmalloc(sizeof(*regex));
5189 + if (regcomp(regex, arg, REG_EXTENDED | REG_NEWLINE))
5190 + return error(_("invalid regex given to -I: '%s'"), arg);
5191 + ALLOC_GROW(options->ignore_regex, options->ignore_regex_nr + 1,
5192 + options->ignore_regex_alloc);
5193 + options->ignore_regex[options->ignore_regex_nr++] = regex;
5194 + return 0;
5195 +}
5196 +
5197 static int diff_opt_pickaxe_regex(const struct option *opt,
5198 const char *arg, int unset)
5199 {
@@ -5462,6 +5482,9 @@ static void prep_parse_options(struct diff_options *options)
5482 OPT_BIT_F(0, "ignore-blank-lines", &options->xdl_opts,
5483 N_("ignore changes whose lines are all blank"),
5484 XDF_IGNORE_BLANK_LINES, PARSE_OPT_NONEG),
5485 + OPT_CALLBACK_F('I', "ignore-matching-lines", options, N_("<regex>"),
5486 + N_("ignore changes whose all lines match <regex>"),
5487 + 0, diff_opt_ignore_regex),
5488 OPT_BIT(0, "indent-heuristic", &options->xdl_opts,
5489 N_("heuristic to shift diff hunk boundaries for easy reading"),
5490 XDF_INDENT_HEURISTIC),
diff.h
+4
@@ -234,6 +234,10 @@ struct diff_options {
234 */
235 const char *pickaxe;
236
237 + /* -I<regex> */
238 + regex_t **ignore_regex;
239 + size_t ignore_regex_nr, ignore_regex_alloc;
240 +
241 const char *single_follow;
242 const char *a_prefix, *b_prefix;
243 const char *line_prefix;
t/t4013-diff-various.sh
+41
@@ -6,6 +6,7 @@
6 test_description='Various diff formatting options'
7
8 . ./test-lib.sh
9 +. "$TEST_DIRECTORY"/diff-lib.sh
10
11 test_expect_success setup '
12
@@ -309,6 +310,7 @@ log -SF master --max-count=2
310 log -GF master
311 log -GF -p master
312 log -GF -p --pickaxe-all master
313 +log -IA -IB -I1 -I2 -p master
314 log --decorate --all
315 log --decorate=full --all
316
@@ -449,4 +451,43 @@ test_expect_success 'diff-tree --stdin with log formatting' '
451 test_cmp expect actual
452 '
453
454 +test_expect_success 'diff -I<regex>: setup' '
455 + git checkout master &&
456 + test_seq 50 >file0 &&
457 + git commit -m "Set up -I<regex> test file" file0 &&
458 + test_seq 50 | sed -e "s/13/ten and three/" -e "/7\$/d" >file0 &&
459 + echo >>file0
460 +'
461 +test_expect_success 'diff -I<regex>' '
462 + git diff --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >actual &&
463 + cat >expect <<-\EOF &&
464 + diff --git a/file0 b/file0
465 + --- a/file0
466 + +++ b/file0
467 + @@ -34,7 +31,6 @@
468 + 34
469 + 35
470 + 36
471 + -37
472 + 38
473 + 39
474 + 40
475 + EOF
476 + compare_diff_patch expect actual
477 +'
478 +
479 +test_expect_success 'diff -I<regex> --stat' '
480 + git diff --stat --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >actual &&
481 + cat >expect <<-\EOF &&
482 + file0 | 1 -
483 + 1 file changed, 1 deletion(-)
484 + EOF
485 + test_cmp expect actual
486 +'
487 +
488 +test_expect_success 'diff -I<regex>: detect malformed regex' '
489 + test_expect_code 129 git diff --ignore-matching-lines="^[124-9" 2>error &&
490 + test_i18ngrep "invalid regex given to -I: " error
491 +'
492 +
493 test_done
t/t4013/diff.log_-IA_-IB_-I1_-I2_-p_master new
+99
@@ -0,0 +1,99 @@
1 +$ git log -IA -IB -I1 -I2 -p master
2 +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64
3 +Merge: 9a6d494 c7a2ab9
4 +Author: A U Thor <author@example.com>
5 +Date: Mon Jun 26 00:04:00 2006 +0000
6 +
7 + Merge branch 'side'
8 +
9 +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a
10 +Author: A U Thor <author@example.com>
11 +Date: Mon Jun 26 00:03:00 2006 +0000
12 +
13 + Side
14 +
15 +diff --git a/file0 b/file0
16 +index 01e79c3..f4615da 100644
17 +--- a/file0
18 ++++ b/file0
19 +@@ -1,3 +1,6 @@
20 + 1
21 + 2
22 + 3
23 ++A
24 ++B
25 ++C
26 +diff --git a/file3 b/file3
27 +new file mode 100644
28 +index 0000000..7289e35
29 +
30 +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0
31 +Author: A U Thor <author@example.com>
32 +Date: Mon Jun 26 00:02:00 2006 +0000
33 +
34 + Third
35 +
36 +diff --git a/dir/sub b/dir/sub
37 +index 8422d40..cead32e 100644
38 +--- a/dir/sub
39 ++++ b/dir/sub
40 +@@ -2,3 +2,5 @@ A
41 + B
42 + C
43 + D
44 ++E
45 ++F
46 +diff --git a/file1 b/file1
47 +new file mode 100644
48 +index 0000000..b1e6722
49 +--- /dev/null
50 ++++ b/file1
51 +@@ -0,0 +1,3 @@
52 ++A
53 ++B
54 ++C
55 +
56 +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44
57 +Author: A U Thor <author@example.com>
58 +Date: Mon Jun 26 00:01:00 2006 +0000
59 +
60 + Second
61 +
62 + This is the second commit.
63 +
64 +diff --git a/dir/sub b/dir/sub
65 +index 35d242b..8422d40 100644
66 +--- a/dir/sub
67 ++++ b/dir/sub
68 +@@ -1,2 +1,4 @@
69 + A
70 + B
71 ++C
72 ++D
73 +diff --git a/file0 b/file0
74 +index 01e79c3..b414108 100644
75 +--- a/file0
76 ++++ b/file0
77 +@@ -1,3 +1,6 @@
78 + 1
79 + 2
80 + 3
81 ++4
82 ++5
83 ++6
84 +diff --git a/file2 b/file2
85 +deleted file mode 100644
86 +index 01e79c3..0000000
87 +--- a/file2
88 ++++ /dev/null
89 +@@ -1,3 +0,0 @@
90 +-1
91 +-2
92 +-3
93 +
94 +commit 444ac553ac7612cc88969031b02b3767fb8a353a
95 +Author: A U Thor <author@example.com>
96 +Date: Mon Jun 26 00:00:00 2006 +0000
97 +
98 + Initial
99 +$
xdiff/xdiff.h
+4
@@ -79,6 +79,10 @@ typedef struct s_mmbuffer {
79 typedef struct s_xpparam {
80 unsigned long flags;
81
82 + /* -I<regex> */
83 + regex_t **ignore_regex;
84 + size_t ignore_regex_nr;
85 +
86 /* See Documentation/diff-options.txt. */
87 char **anchors;
88 size_t anchors_nr;
xdiff/xdiffi.c
+45 -2
@@ -998,7 +998,7 @@ static int xdl_call_hunk_func(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb,
998 return 0;
999 }
1000
1001 -static void xdl_mark_ignorable(xdchange_t *xscr, xdfenv_t *xe, long flags)
1001 +static void xdl_mark_ignorable_lines(xdchange_t *xscr, xdfenv_t *xe, long flags)
1002 {
1003 xdchange_t *xch;
1004
@@ -1019,6 +1019,46 @@ static void xdl_mark_ignorable(xdchange_t *xscr, xdfenv_t *xe, long flags)
1019 }
1020 }
1021
1022 +static int record_matches_regex(xrecord_t *rec, xpparam_t const *xpp) {
1023 + regmatch_t regmatch;
1024 + int i;
1025 +
1026 + for (i = 0; i < xpp->ignore_regex_nr; i++)
1027 + if (!regexec_buf(xpp->ignore_regex[i], rec->ptr, rec->size, 1,
1028 + &regmatch, 0))
1029 + return 1;
1030 +
1031 + return 0;
1032 +}
1033 +
1034 +static void xdl_mark_ignorable_regex(xdchange_t *xscr, const xdfenv_t *xe,
1035 + xpparam_t const *xpp)
1036 +{
1037 + xdchange_t *xch;
1038 +
1039 + for (xch = xscr; xch; xch = xch->next) {
1040 + xrecord_t **rec;
1041 + int ignore = 1;
1042 + long i;
1043 +
1044 + /*
1045 + * Do not override --ignore-blank-lines.
1046 + */
1047 + if (xch->ignore)
1048 + continue;
1049 +
1050 + rec = &xe->xdf1.recs[xch->i1];
1051 + for (i = 0; i < xch->chg1 && ignore; i++)
1052 + ignore = record_matches_regex(rec[i], xpp);
1053 +
1054 + rec = &xe->xdf2.recs[xch->i2];
1055 + for (i = 0; i < xch->chg2 && ignore; i++)
1056 + ignore = record_matches_regex(rec[i], xpp);
1057 +
1058 + xch->ignore = ignore;
1059 + }
1060 +}
1061 +
1062 int xdl_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
1063 xdemitconf_t const *xecfg, xdemitcb_t *ecb) {
1064 xdchange_t *xscr;
@@ -1038,7 +1078,10 @@ int xdl_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp,
1078 }
1079 if (xscr) {
1080 if (xpp->flags & XDF_IGNORE_BLANK_LINES)
1041 - xdl_mark_ignorable(xscr, &xe, xpp->flags);
1081 + xdl_mark_ignorable_lines(xscr, &xe, xpp->flags);
1082 +
1083 + if (xpp->ignore_regex)
1084 + xdl_mark_ignorable_regex(xscr, &xe, xpp);
1085
1086 if (ef(&xe, xscr, ecb, xecfg) < 0) {
1087