Merge branch 'tn/stash-avoid-sparse-index-expansion' into jch

The 'git stash push' command has been optimized to avoid unnecessary
sparse index expansion when pathspecs are wholly inside the
sparse-checkout cone.  Also, a potential out-of-bounds read in the
sparse-index expansion check helper pathspec_needs_expanded_index()
has been fixed by consistently using the parsed, prefixed path.

* tn/stash-avoid-sparse-index-expansion:
  stash: avoid sparse-index expansion for in-cone paths
  pathspec: use match for sparse-index expansion checks
jch
Junio C Hamano 2026-07-24 14:40:39 -07:00
commit bf5a557747
4 changed files with 71 additions and 8 deletions

View File

@ -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);

View File

@ -847,9 +847,9 @@ int pathspec_needs_expanded_index(struct index_state *istate,
* - not-in-cone/bar*: may need expanded index
* - **.c: may need expanded index
*/
if (strspn(item.original + item.nowildcard_len, "*") ==
if (strspn(item.match + item.nowildcard_len, "*") ==
(unsigned int)(item.len - item.nowildcard_len) &&
path_in_cone_mode_sparse_checkout(item.original, istate))
path_in_cone_mode_sparse_checkout(item.match, istate))
continue;

for (pos = 0; pos < istate->cache_nr; pos++) {
@ -865,7 +865,7 @@ int pathspec_needs_expanded_index(struct index_state *istate,
*/
if ((unsigned int)item.nowildcard_len >
ce_namelen(ce) &&
!strncmp(item.original, ce->name,
!strncmp(item.match, ce->name,
ce_namelen(ce))) {
res = 1;
break;
@ -876,13 +876,13 @@ int pathspec_needs_expanded_index(struct index_state *istate,
* directory and the pathspec does not match the whole
* directory, need to expand the index.
*/
if (!strncmp(item.original, ce->name, item.nowildcard_len) &&
wildmatch(item.original, ce->name, 0)) {
if (!strncmp(item.match, ce->name, item.nowildcard_len) &&
wildmatch(item.match, ce->name, 0)) {
res = 1;
break;
}
}
} else if (!path_in_cone_mode_sparse_checkout(item.original, istate) &&
} else if (!path_in_cone_mode_sparse_checkout(item.match, istate) &&
!matches_skip_worktree(pathspec, i, &skip_worktree_seen))
res = 1;


View File

@ -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 .

View File

@ -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 &&

@ -2119,6 +2174,13 @@ test_expect_success 'sparse index is not expanded: rm' '
ensure_not_expanded rm -r deep
'

test_expect_success 'sparse index is not expanded: prefixed wildcard pathspec' '
init_repos &&

ensure_not_expanded -C deep rm --dry-run -- "a*" &&
ensure_not_expanded -C deep reset base -- "a*"
'

test_expect_success 'grep with and --cached' '
init_repos &&