All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.