Git development
 help / color / mirror / Atom feed
From: tnyman@openai.com
To: git@vger.kernel.org
Cc: Ted Nyman <tnyman@openai.com>, Derrick Stolee <stolee@gmail.com>,
	Taylor Blau <me@ttaylorr.com>, Jeff King <peff@peff.net>,
	Victoria Dye <vdye@github.com>
Subject: [PATCH 2/2] stash: avoid sparse-index expansion for in-cone paths
Date: Mon, 20 Jul 2026 15:31:21 -0700	[thread overview]
Message-ID: <20260720223118.62821-6-tnyman@openai.com> (raw)
In-Reply-To: <20260720223118.62821-4-tnyman@openai.com>

From: Ted Nyman <tnyman@openai.com>

`git stash push -- <pathspec>` expands a sparse index before checking
whether the pathspec matches any tracked paths. This is unnecessary
when the pathspec is wholly inside the sparse-checkout cone and makes
a path-limited stash proportional to the size of the full index.

Use `pathspec_needs_expanded_index()` to expand only when a pathspec
can match part of a sparse-directory entry, as `git rm` and `git
reset` already do. Keep the full-index behavior for pathspecs that
need it.

Add compatibility coverage for literal, prefixed, wildcard, file,
multiple, staged, and missing pathspecs. Add the corresponding
path-limited stash case to p2000.

On a cone-mode repository with 349,525 tracked paths and 49 sparse
index entries, the best of three runs changed from 18.87s to 0.06s.
Trace2 reported four index expansions before this change and none
after it.

Signed-off-by: Ted Nyman <tnyman@openai.com>
---
 builtin/stash.c                          |  4 +-
 t/perf/p2000-sparse-operations.sh        |  1 +
 t/t1092-sparse-checkout-compatibility.sh | 55 ++++++++++++++++++++++++
 3 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index c4809f299a313b..72c52571f8c06c 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1702,8 +1702,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
 	if (!include_untracked && ps->nr) {
 		char *ps_matched = xcalloc(ps->nr, 1);
 
-		/* TODO: audit for interaction with sparse-index. */
-		ensure_full_index(the_repository->index);
+		if (pathspec_needs_expanded_index(the_repository->index, ps))
+			ensure_full_index(the_repository->index);
 		for (size_t i = 0; i < the_repository->index->cache_nr; i++)
 			ce_path_match(the_repository->index, the_repository->index->cache[i], ps,
 				      ps_matched);
diff --git a/t/perf/p2000-sparse-operations.sh b/t/perf/p2000-sparse-operations.sh
index aadf22bc2f0bb2..548a61cd9064bc 100755
--- a/t/perf/p2000-sparse-operations.sh
+++ b/t/perf/p2000-sparse-operations.sh
@@ -108,6 +108,7 @@ test_perf_on_all () {
 
 test_perf_on_all git status
 test_perf_on_all 'git stash && git stash pop'
+test_perf_on_all "git stash push -- $SPARSE_CONE/a && git stash pop"
 test_perf_on_all 'echo >>new && git stash -u && git stash pop'
 test_perf_on_all git add -A
 test_perf_on_all git add .
diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
index d0b42371663f9d..4140c4d8ef2436 100755
--- a/t/t1092-sparse-checkout-compatibility.sh
+++ b/t/t1092-sparse-checkout-compatibility.sh
@@ -1598,6 +1598,61 @@ test_expect_success 'sparse-index is not expanded: stash' '
 	ensure_not_expanded stash pop
 '
 
+test_expect_success 'sparse-index is not expanded: stash in-cone pathspec' '
+	init_repos &&
+
+	echo unrelated >>sparse-index/deep/e &&
+	echo literal >>sparse-index/deep/a &&
+	ensure_not_expanded stash push -- deep/a &&
+	test_grep ! literal sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep literal sparse-index/deep/a &&
+
+	echo prefixed >>sparse-index/deep/a &&
+	ensure_not_expanded -C deep stash push -- a &&
+	test_grep ! prefixed sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep prefixed sparse-index/deep/a &&
+
+	echo wildcard >>sparse-index/deep/a &&
+	ensure_not_expanded stash push -- "deep/a*" &&
+	test_grep ! wildcard sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep wildcard sparse-index/deep/a &&
+
+	echo pathspec-file >>sparse-index/deep/a &&
+	echo deep/a >pathspec-file &&
+	ensure_not_expanded stash push --pathspec-from-file=../pathspec-file &&
+	test_grep ! pathspec-file sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep pathspec-file sparse-index/deep/a &&
+
+	echo multiple-a >>sparse-index/deep/a &&
+	echo multiple-e >>sparse-index/deep/e &&
+	ensure_not_expanded stash push -- deep/a deep/e &&
+	test_grep ! multiple-a sparse-index/deep/a &&
+	test_grep ! multiple-e sparse-index/deep/e &&
+	ensure_not_expanded stash pop &&
+	test_grep multiple-a sparse-index/deep/a &&
+	test_grep multiple-e sparse-index/deep/e &&
+
+	echo staged >>sparse-index/deep/a &&
+	git -C sparse-index add deep/a &&
+	ensure_not_expanded stash push --staged -- deep/a &&
+	test_grep ! staged sparse-index/deep/a &&
+	test_grep unrelated sparse-index/deep/e &&
+	ensure_not_expanded stash pop --index &&
+	test_grep staged sparse-index/deep/a &&
+	test_must_fail git -C sparse-index diff --cached --quiet -- deep/a &&
+
+	ensure_not_expanded ! stash push -- deep/does-not-exist &&
+	test_grep "did not match any file" sparse-index-error
+'
+
 test_expect_success 'describe tested on all' '
 	init_repos &&
 

      parent reply	other threads:[~2026-07-20 22:31 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-20 22:31 [PATCH 0/2] stash: avoid sparse-index expansion for in-cone paths tnyman
2026-07-20 22:31 ` [PATCH 1/2] pathspec: use match for sparse-index expansion checks tnyman
2026-07-20 22:31 ` tnyman [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260720223118.62821-6-tnyman@openai.com \
    --to=tnyman@openai.com \
    --cc=git@vger.kernel.org \
    --cc=me@ttaylorr.com \
    --cc=peff@peff.net \
    --cc=stolee@gmail.com \
    --cc=vdye@github.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox