diff-filter: be more careful when looking for negative bits

The `--diff-filter=<bits>` option allows to filter the diff by certain criteria, for example `R` to only show renamed files. It also supports negating a filter via a down-cased letter, i.e. `r` to show _everything but_ renamed files. However, the code is a bit overzealous when trying to figure out whether `git diff` should start with all diff-filters turned on because the user provided a lower-case letter: if the `--diff-filter` argument starts with an upper-case letter, we must not start with all bits turned on. Even worse, it is possible to specify the diff filters in multiple, separate options, e.g. `--diff-filter=AM [...] --diff-filter=m`. Let's accumulate the include/exclude filters independently, and only special-case the "only exclude filters were specified" case after parsing the options altogether. Note: The code replaced by this commit took pains to avoid setting any unused bits of `options->filter`. That was unnecessary, though, as all accesses happen via the `filter_bit_tst()` function using specific bits, and setting the unused bits has no effect. Therefore, we can simplify the code by using `~0` (or in this instance, `~<unwanted-bit>`). Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Jan 28, 2022 at 12:02 UTC 75408ca94980c125b3a800003c3e6b7cdcb27d44
3 files changed +21 -17
diff.c
+7 -16
@@ -4720,6 +4720,12 @@ void diff_setup_done(struct diff_options *options)
4720 if (!options->use_color || external_diff())
4721 options->color_moved = 0;
4722
4723 + if (options->filter_not) {
4724 + if (!options->filter)
4725 + options->filter = ~filter_bit[DIFF_STATUS_FILTER_AON];
4726 + options->filter &= ~options->filter_not;
4727 + }
4728 +
4729 FREE_AND_NULL(options->parseopts);
4730 }
4731
@@ -4820,21 +4826,6 @@ static int diff_opt_diff_filter(const struct option *option,
4826 BUG_ON_OPT_NEG(unset);
4827 prepare_filter_bits();
4828
4823 - /*
4824 - * If there is a negation e.g. 'd' in the input, and we haven't
4825 - * initialized the filter field with another --diff-filter, start
4826 - * from full set of bits, except for AON.
4827 - */
4828 - if (!opt->filter) {
4829 - for (i = 0; (optch = optarg[i]) != '\0'; i++) {
4830 - if (optch < 'a' || 'z' < optch)
4831 - continue;
4832 - opt->filter = (1 << (ARRAY_SIZE(diff_status_letters) - 1)) - 1;
4833 - opt->filter &= ~filter_bit[DIFF_STATUS_FILTER_AON];
4834 - break;
4835 - }
4836 - }
4837 -
4829 for (i = 0; (optch = optarg[i]) != '\0'; i++) {
4830 unsigned int bit;
4831 int negate;
@@ -4851,7 +4842,7 @@ static int diff_opt_diff_filter(const struct option *option,
4842 return error(_("unknown change class '%c' in --diff-filter=%s"),
4843 optarg[i], optarg);
4844 if (negate)
4854 - opt->filter &= ~bit;
4845 + opt->filter_not |= bit;
4846 else
4847 opt->filter |= bit;
4848 }
diff.h
+1 -1
@@ -283,7 +283,7 @@ struct diff_options {
283 struct diff_flags flags;
284
285 /* diff-filter bits */
286 - unsigned int filter;
286 + unsigned int filter, filter_not;
287
288 int use_color;
289
t/t4202-log.sh
+13
@@ -142,6 +142,19 @@ test_expect_success 'diff-filter=R' '
142
143 '
144
145 +test_expect_success 'multiple --diff-filter bits' '
146 +
147 + git log -M --pretty="format:%s" --diff-filter=R HEAD >expect &&
148 + git log -M --pretty="format:%s" --diff-filter=Ra HEAD >actual &&
149 + test_cmp expect actual &&
150 + git log -M --pretty="format:%s" --diff-filter=aR HEAD >actual &&
151 + test_cmp expect actual &&
152 + git log -M --pretty="format:%s" \
153 + --diff-filter=a --diff-filter=R HEAD >actual &&
154 + test_cmp expect actual
155 +
156 +'
157 +
158 test_expect_success 'diff-filter=C' '
159
160 git log -C -C --pretty="format:%s" --diff-filter=C HEAD >actual &&