]> git.ipfire.org Git - thirdparty/git.git/commitdiff
match_pathname(): give fnmatch one char of prefix context
authorJeff King <peff@peff.net>
Sun, 26 Oct 2025 15:42:22 +0000 (11:42 -0400)
committerJunio C Hamano <gitster@pobox.com>
Sun, 26 Oct 2025 23:32:43 +0000 (16:32 -0700)
In match_pathname(), which we use for matching .gitignore and
.gitattribute patterns, we are comparing paths with fnmatch patterns
(actually our extended wildmatch, which will be important).  There's an
extra optimization there: we pre-compute the number of non-wildcard
characters at the beginning of the pattern and do an fspathncmp() on
that prefix.

That lets us avoid fnmatch entirely on patterns without wildcards, and
shrinks the amount of work we hand off to fnmatch. For a pattern like
"foo*.txt" and a path "foobar.txt", we'd cut away the matching "foo"
prefix and just pass "*.txt" and "bar.txt" to fnmatch().

But this misses a subtle corner case. In fnmatch(), we'll think
"bar.txt" is the start of the path, but it's not. This doesn't matter
for the pattern above, but consider the wildmatch pattern "foo**/bar"
and the path "foobar". These two should not match, because there is no
file named "bar", and the "**" applies only to the containing directory
name. But after removing the "foo" prefix, fnmatch will get "**/bar" and
"bar", which it does consider a match, because "**/" can match zero
directories.

We can solve this by giving fnmatch a bit more context. As long as it
has one byte of the matched prefix, then it will know that "bar" is not
the start of the path. In this example it would get "o**/bar" and
"obar", and realize that they cannot match.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
dir.c
t/t0008-ignores.sh

diff --git a/dir.c b/dir.c
index 130fa987660a01cbefb373bb06b6379ad4d01e1d..465c22ff68f14fd25ae8f3585cc774a3c148ddd1 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -1369,6 +1369,12 @@ int match_pathname(const char *pathname, int pathlen,
                if (patternlen == prefix && namelen == prefix)
                        return 1;
 
+               /*
+                * Retain one character of the prefix to
+                * pass to fnmatch, which lets it distinguish
+                * the start of a directory component correctly.
+                */
+               prefix--;
                pattern += prefix;
                patternlen -= prefix;
                name    += prefix;
index 273d71411fe05d132467437498463a0d765da205..db8bde280ecfc9b3d3cc9dff0e1d794471342586 100755 (executable)
@@ -847,6 +847,17 @@ test_expect_success 'directories and ** matches' '
        test_cmp expect actual
 '
 
+test_expect_success '** not confused by matching leading prefix' '
+       cat >.gitignore <<-\EOF &&
+       foo**/bar
+       EOF
+       git check-ignore foobar foo/bar >actual &&
+       cat >expect <<-\EOF &&
+       foo/bar
+       EOF
+       test_cmp expect actual
+'
+
 ############################################################################
 #
 # test whitespace handling