Git development
 help / color / mirror / Atom feed
* [PATCH] dir: find common prefix among positive pathspecs
@ 2026-09-02 13:04 Yannik Tausch
  2026-09-02 17:07 ` Junio C Hamano
  0 siblings, 1 reply; 25+ messages in thread
From: Yannik Tausch @ 2026-09-02 13:04 UTC (permalink / raw)
  To: git

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(-)

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

^ permalink raw reply related	[flat|nested] 25+ messages in thread

end of thread, other threads:[~2026-09-05 16:14 UTC | newest]

Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-02 13:04 [PATCH] dir: find common prefix among positive pathspecs Yannik Tausch
2026-09-02 17:07 ` Junio C Hamano
2026-09-03  9:59   ` Yannik Tausch
2026-09-03 10:02     ` [PATCH v2 0/2] dir: fix pathspec prefixes with exclusions Yannik Tausch
2026-09-03 10:03       ` [PATCH v2 1/2] dir: do not apply prefix to negative pathspecs Yannik Tausch
2026-09-04  5:00         ` Elijah Newren
2026-09-04 14:21           ` Junio C Hamano
2026-09-03 10:04       ` [PATCH v2 2/2] dir: find common prefix among non-exclude pathspec items Yannik Tausch
     [not found]         ` <CY5PR17MB6144A1A7BF2E101FE26A6A85B1B62@CY5PR17MB6144.namprd17.prod.outlook.com>
2026-09-03 11:49           ` Yannik Tausch
2026-09-03 18:11         ` Junio C Hamano
2026-09-03 18:13           ` pathspec: match and original in pathspec_item are const Junio C Hamano
2026-09-03 18:37             ` Yannik Tausch
2026-09-03 18:51               ` Junio C Hamano
2026-09-03 18:57                 ` Yannik Tausch
2026-09-03 21:05                   ` Junio C Hamano
2026-09-03 21:13                     ` Yannik Tausch
2026-09-04  5:02         ` [PATCH v2 2/2] dir: find common prefix among non-exclude pathspec items Elijah Newren
2026-09-04 16:43           ` Junio C Hamano
2026-09-04 19:19             ` Elijah Newren
2026-09-05 16:14               ` Junio C Hamano
2026-09-03 18:06       ` [PATCH v2 0/2] dir: fix pathspec prefixes with exclusions Yannik Tausch
2026-09-03 18:43       ` [PATCH v3 0/3] " Yannik Tausch
2026-09-03 18:44         ` [PATCH v3 1/3] pathspec: match and original in pathspec_item are const Yannik Tausch
2026-09-03 18:45         ` [PATCH v3 2/3] dir: do not apply prefix to negative pathspecs Yannik Tausch
2026-09-03 18:45         ` [PATCH v3 3/3] dir: find common prefix among non-exclude pathspec items Yannik Tausch

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox