git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v2 2/2] refs.c: stop matching non-directory prefixes in exclude patterns

From
Elijah Newren <newren@gmail.com>
Date
Mar 7, 2025, 21:31 UTC
Message-ID
<CABPp-BFn0wht71rM1bE1ABpa6Rn6QRrcVrbN0fhcwprbm+T39Q@mail.gmail.com>
In-Reply-To
<67c8c5f797833a9a35f4805059d7e759020f54bd.1741275245.git.me@ttaylorr.com>
On Thu, Mar 6, 2025 at 7:34 AM Taylor Blau <me@ttaylorr.com> wrote:
Show 29 quoted lines
>
> In the packed-refs backend, our implementation of '--exclude' (dating
> back to 59c35fac54 (refs/packed-backend.c: implement jump lists to avoid
> excluded pattern(s), 2023-07-10)) considers, for example:
>
>     $ git for-each-ref --exclude=refs/heads/ba
>
> to exclude "refs/heads/bar", "refs/heads/baz", and so on.
>
> The files backend, which does not implement '--exclude' (and relies on
> the caller to cull out results that don't match) naturally will
> enumerate "refs/heads/bar" and so on.
>
> So in the above example, 'for-each-ref' will try and see if
> "refs/heads/ba" matches "refs/heads/bar" (since the files backend simply
> enumerated every loose reference), and, realizing that it does not
> match, output the reference as expected. (A caller that did want to
> exclude "refs/heads/bar" and "refs/heads/baz" might instead run "git
> for-each-ref --exclude='refs/heads/ba*'").
>
> This can lead to strange behavior, like seeing a different set of
> references advertised via 'upload-pack' depending on what set of
> references were loose versus packed.
>
> So there is a subtle bug with '--exclude' which is that in the
> packed-refs backend we will consider "refs/heads/bar" to be a pattern
> match against "refs/heads/ba" when we shouldn't. Likewise, the reftable
> backend (which in this case is bug-compatible with the packed backend)
> exhibits the same broken behavior.
Yuck; nice to see this being addressed.
Show 32 quoted lines
> There are a few ways to fix this. One is to tighten the rules in
> cmp_record_to_refname(), which is used to determine the start/end-points
> of the jump list used by the packed backend. In this new "strict" mode,
> the comparison function would handle the case where we've reached the
> end of the pattern by introducing a new check like so:
>
>     while (1) {
>         if (*r1 == '\n')
>             return *r2 ? -1 : 0;
>         if (!*r2)
>             if (strict && *r1 != '/')        /* <- here */
>                 return 1;
>             return start ? 1 : -1;
>         if (*r1 != *r2)
>             return (unsigned char)*r1 < (unsigned char)*r2 ? -1 : +1;
>         r1++;
>         r2++;
>     }
>
> (eliding out the rest of cmp_record_to_refname()). Equivalently, we
> could teach refs/packed-backend::populate_excluded_jump_list() to append
> a trailing '/' if one does not already exist, forcing an exclude pattern
> like "refs/heads/ba" to only match "refs/heads/ba/abc" and so forth.
>
> But since the same problem exists in reftable, we can fix both at once
> by performing this pre-processing step one layer up in refs.c at the
> common entrypoint for the two, which is 'refs_ref_iterator_begin()'.
>
> Since that solution is both the simplest and only requires modification
> in one spot, let's normalize exclude patterns so that they end with a
> trailing slash. This causes us to unify the behavior between all three
> backends.
:-)
Show 12 quoted lines
> There is some minor test fallout in the "overlapping excluded regions"
> test, which happens to use 'refs/ba' as an exclude pattern, and expects
> references under the "refs/heads/bar/*" and "refs/heads/baz/*"
> hierarchies to be excluded from the results.
>
> But that test fallout is expected, because the test was codifying the
> buggy behavior to begin with, and should have never been written that
> way. Split that into its own test (since the range is no longer
> overlapping under the stricter interpretation of --exclude patterns
> presented here). Create a new test which does have overlapping
> regions by using a refs/heads/bar/4/... hierarchy and excluding both
> "refs/heads/bar" and "refs/heads/bar/4".
Always nice to see tests corrected.
Show 22 quoted lines
> Reported-by: SURA <surak8806@gmail.com>
> Helped-by: Jeff King <peff@peff.net>
> Signed-off-by: Taylor Blau <me@ttaylorr.com>
> ---
>  refs.c                  |  6 +++++-
>  t/t1419-exclude-refs.sh | 16 ++++++++++++++--
>  2 files changed, 19 insertions(+), 3 deletions(-)
>
> diff --git a/refs.c b/refs.c
> index 17d3840aff..2d9a1b51f4 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -1708,7 +1708,11 @@ struct ref_iterator *refs_ref_iterator_begin(
>                         if (!len)
>                                 continue;
>
> -                       strvec_push(&normalized_exclude_patterns, pattern);
> +                       if (pattern[len - 1] == '/')
> +                               strvec_push(&normalized_exclude_patterns, pattern);
> +                       else
> +                               strvec_pushf(&normalized_exclude_patterns, "%s/",
> +                                            pattern);

Doesn't this mean that if the user requested to exclude "refs/heads/bar" and "refs/heads/bar" exists, that we won't exclude it because it doesn't have a trailing slash?

From reading other comments in this thread, I guess that ends up being okay, because we only promise to filter out what we can cheaply filter, and we rely on our caller to double-check everything and do the real filtering.

...but it gives me some ugly dir.c vibes, reminding me of 95c11ecc73f2 (Fix error-prone fill_directory() API; make it only return matches, 2020-04-01) and a slew of related bugs preceding it. Granted, dir.c had this tri-state to deal with (tracked, untracked-but-ignored, untracked-and-not-ignored) and simplifying of whole directories, which don't apply here, so maybe the similarity of "fast-filtering-only-and-rely-on-caller" won't be a problem since the upper level filtering is so much more straightforward.

Should this at least be called out in the commit message, though?
Show 40 quoted lines
>                 }
>
>                 exclude_patterns = normalized_exclude_patterns.v;
> diff --git a/t/t1419-exclude-refs.sh b/t/t1419-exclude-refs.sh
> index fd58260a24..04797aee59 100755
> --- a/t/t1419-exclude-refs.sh
> +++ b/t/t1419-exclude-refs.sh
> @@ -46,6 +46,10 @@ test_expect_success 'setup' '
>                         echo "create refs/heads/$name/$i $base" || return 1
>                 done || return 1
>         done >in &&
> +       for i in 5 6 7
> +       do
> +               echo "create refs/heads/bar/4/$i $base" || return 1
> +       done >>in &&
>         echo "delete refs/heads/main" >>in &&
>
>         git update-ref --stdin <in &&
> @@ -99,9 +103,17 @@ test_expect_success 'adjacent, non-overlapping excluded regions' '
>         esac
>  '
>
> -test_expect_success 'overlapping excluded regions' '
> +test_expect_success 'non-directory excluded regions' '
>         for_each_ref__exclude refs/heads refs/heads/ba refs/heads/baz >actual 2>perf &&
> -       for_each_ref refs/heads/foo refs/heads/quux >expect &&
> +       for_each_ref refs/heads/bar refs/heads/foo refs/heads/quux >expect &&
> +
> +       test_cmp expect actual &&
> +       assert_jumps 1 perf
> +'
> +
> +test_expect_success 'overlapping excluded regions' '
> +       for_each_ref__exclude refs/heads refs/heads/bar refs/heads/bar/4 >actual 2>perf &&
> +       for_each_ref refs/heads/baz refs/heads/foo refs/heads/quux >expect &&
>
>         test_cmp expect actual &&
>         assert_jumps 1 perf
> --
> 2.49.0.rc1.2.g67c8c5f7978
Other than the one surprise noted above, looks good to me.
Previous: Taylor BlauNext: Taylor Blau
Message 16 of 17 in “refs: a couple of --exclude fixes”
  1. 0/2 refs: a couple of --exclude fixesTaylor Blau, Mar 6, 2025
  2. 1/2 refs.c: remove empty '--exclude' patternsTaylor Blau, Mar 6, 2025
  3. 2/2 refs.c: unify '--exclude' behavior between files and packed backendsTaylor Blau, Mar 6, 2025
  4. Patrick SteinhardtMar 6, 2025
  5. Taylor BlauMar 6, 2025
  6. 0/2 refs: a couple of --exclude fixesTaylor Blau, Mar 6, 2025
  7. 1/2 refs.c: remove empty '--exclude' patternsTaylor Blau, Mar 6, 2025
  8. Elijah NewrenMar 7, 2025
  9. Taylor BlauMar 7, 2025
  10. Elijah NewrenMar 7, 2025
  11. 2/2 refs.c: stop matching non-directory prefixes in exclude patternsTaylor Blau, Mar 6, 2025
  12. Junio C HamanoMar 6, 2025
  13. Patrick SteinhardtMar 7, 2025
  14. Junio C HamanoMar 7, 2025
  15. Taylor BlauMar 7, 2025
  16. Elijah NewrenMar 7, 2025
  17. Taylor BlauMar 7, 2025

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.