common_prefix_len() skips exclude pathspec items, but uses n == 0 to identify the initial item and items[0] as the comparison source. When an exclude item comes first, the function returns zero even when all positive pathspecs share a directory.
Track the first positive item explicitly. Return its match and the common prefix length together so that common_prefix() and fill_directory() use the correct string. Add a unit test with an unrelated exclude before two positive pathspecs that share a directory.
Signed-off-by: Yannik Tausch <dev@ytausch.de> ---
This patch is based on https://lore.kernel.org/git/0CA8678D-0540-4A2E-B314-B9BEB04E2BF5@ytausch.de/T/#u.
dir.c | 51 +++++++++++++++++++++++++++----------------- t/unit-tests/u-dir.c | 28 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-)
Show changes to 2 files +60 −19
dir.c, t/unit-tests/u-dir.c
diff --git a/dir.c b/dir.c index 7072715389..441c1795a1 100644 --- a/dir.c +++ b/dir.c @@ -212,9 +212,19 @@ static int fnmatch_icase_mem(const char *pattern, int patternlen, return match_status; } -static size_t common_prefix_len(const struct pathspec *pathspec) +struct pathspec_prefix { + const char *match; + size_t len; +}; + +/* + * Find the common prefix of positive pathspec items. The returned match + * points into the first positive item and is not NUL-terminated at len. + */ +static struct pathspec_prefix find_common_prefix(const struct pathspec *pathspec) { - int n; + struct pathspec_prefix prefix = { 0 }; + int n, first = -1; size_t max = 0; /* @@ -237,44 +247,47 @@ static size_t common_prefix_len(const struct pathspec *pathspec) size_t i = 0, len = 0, item_len; if (pathspec->items[n].magic & PATHSPEC_EXCLUDE) continue; + if (first < 0) + first = n; if (pathspec->items[n].magic & PATHSPEC_ICASE) item_len = pathspec->items[n].prefix; else item_len = pathspec->items[n].nowildcard_len; - while (i < item_len && (n == 0 || i < max)) { + while (i < item_len && (n == first || i < max)) { char c = pathspec->items[n].match[i]; - if (c != pathspec->items[0].match[i]) + if (c != pathspec->items[first].match[i]) break; if (c == '/') len = i + 1; i++; } - if (n == 0 || len < max) { + if (n == first || len < max) { max = len; if (!max) break; } } - return max; + prefix.match = first < 0 ? NULL : pathspec->items[first].match; + prefix.len = max; + return prefix; } /* - * Returns a copy of the longest leading path common among all + * Returns a copy of the longest leading path common among all positive * pathspecs. */ char *common_prefix(const struct pathspec *pathspec) { - unsigned long len = common_prefix_len(pathspec); + struct pathspec_prefix prefix = find_common_prefix(pathspec); - return len ? xmemdupz(pathspec->items[0].match, len) : NULL; + return prefix.len ? xmemdupz(prefix.match, prefix.len) : NULL; } int fill_directory(struct dir_struct *dir, struct index_state *istate, const struct pathspec *pathspec) { - const char *prefix; - size_t prefix_len; + struct pathspec_prefix prefix; unsigned exclusive_flags = DIR_SHOW_IGNORED | DIR_SHOW_IGNORED_TOO; if ((dir->flags & exclusive_flags) == exclusive_flags) @@ -284,13 +297,13 @@ int fill_directory(struct dir_struct *dir, * Calculate common prefix for the pathspec, and * use that to optimize the directory walk */ - prefix_len = common_prefix_len(pathspec); - prefix = prefix_len ? pathspec->items[0].match : ""; + prefix = find_common_prefix(pathspec); /* Read the directory and prune it */ - read_directory(dir, istate, prefix, prefix_len, pathspec); + read_directory(dir, istate, prefix.len ? prefix.match : "", + prefix.len, pathspec); - return prefix_len; + return prefix.len; } int within_depth(const char *name, int namelen, @@ -394,7 +407,7 @@ static int match_pathspec_item(struct index_state *istate, /* * The normal call pattern is: - * 1. prefix = common_prefix_len(ps); + * 1. prefix = find_common_prefix(ps).len; * 2. prune something, or fill_directory * 3. match_pathspec() * @@ -411,11 +424,11 @@ static int match_pathspec_item(struct index_state *istate, * prefix part when :(icase) is involved. We do exact * comparison ourselves. * - * Normally the caller (common_prefix_len() in fact) does + * Normally the caller (find_common_prefix() in fact) does * _exact_ matching on name[-prefix+1..-1] and we do not need * to check that part. Be defensive and check it anyway, in - * case common_prefix_len is changed, or a new caller is - * introduced that does not use common_prefix_len. + * case find_common_prefix() is changed, or a new caller is + * introduced that does not use find_common_prefix(). * * If the penalty turns out too high when prefix is really * long, maybe change it to diff --git a/t/unit-tests/u-dir.c b/t/unit-tests/u-dir.c index 2d0adaa39e..8b558e0391 100644 --- a/t/unit-tests/u-dir.c +++ b/t/unit-tests/u-dir.c @@ -45,3 +45,31 @@ void test_dir__within_depth(void) } + +void test_dir__common_prefix_skips_excluded_pathspecs(void) +{ + struct pathspec_item items[] = { + { + .match = "unrelated/path", + .magic = PATHSPEC_EXCLUDE, + .nowildcard_len = 14, + }, + { + .match = "foo/bar", + .nowildcard_len = 7, + }, + { + .match = "foo/baz", + .nowildcard_len = 7, + }, + }; + struct pathspec pathspec = { + .nr = ARRAY_SIZE(items), + .magic = PATHSPEC_EXCLUDE, + .items = items, + }; + char *prefix = common_prefix(&pathspec); + + cl_assert_equal_s(prefix, "foo/"); + free(prefix); +} base-commit: 1630431f326e15fcde608827b5ff38422528eb59 prerequisite-patch-id: 256750f07ff447732869d1aadde2f1050e7bb169
-- 2.55.0