From 9d6c580d01800defbd9497dbe6a694dc31179dce Mon Sep 17 00:00:00 2001 From: Jeff King Date: Sun, 26 Oct 2025 11:41:46 -0400 Subject: [PATCH 1/2] match_pathname(): reorder prefix-match check As an optimization, we use fspathncmp() to match a prefix of the pattern that does not contain any wildcards, and then pass the remainder to fnmatch(). If it has matched the whole thing, we can return early. Let's shift this early-return check to before we tweak the pattern and name strings. That will gives us more flexibility with that tweaking. It might also save a few instructions, but I couldn't measure any improvement in doing so (and I wouldn't be surprised if an optimizing compiler could figure that out itself). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- dir.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dir.c b/dir.c index dfb4d40103..130fa98766 100644 --- a/dir.c +++ b/dir.c @@ -1360,18 +1360,19 @@ int match_pathname(const char *pathname, int pathlen, if (fspathncmp(pattern, name, prefix)) return 0; - pattern += prefix; - patternlen -= prefix; - name += prefix; - namelen -= prefix; /* * If the whole pattern did not have a wildcard, * then our prefix match is all we need; we * do not need to call fnmatch at all. */ - if (!patternlen && !namelen) + if (patternlen == prefix && namelen == prefix) return 1; + + pattern += prefix; + patternlen -= prefix; + name += prefix; + namelen -= prefix; } return fnmatch_icase_mem(pattern, patternlen, From 1940a02dc1122d15706a7051ee47e73f329fb4f7 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Sun, 26 Oct 2025 11:42:22 -0400 Subject: [PATCH 2/2] match_pathname(): give fnmatch one char of prefix context 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 Signed-off-by: Junio C Hamano --- dir.c | 6 ++++++ t/t0008-ignores.sh | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/dir.c b/dir.c index 130fa98766..465c22ff68 100644 --- 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; diff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh index 273d71411f..db8bde280e 100755 --- a/t/t0008-ignores.sh +++ b/t/t0008-ignores.sh @@ -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