{"thread":{"id":"65683","subject":"[PATCH 0/2] restore: better integrate with sparse index","startedAt":"2026-05-24T17:40:16Z","lastAt":"2026-05-26T20:26:41Z","messageCount":9,"participants":["Derrick Stolee via GitGitGadget","Junio C Hamano","Derrick Stolee"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"544002","messageId":"pull.2121.git.1779644412.gitgitgadget@gmail.com","threadId":"65683","inReplyTo":null,"subject":"[PATCH 0/2] restore: better integrate with sparse index","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-24T17:40:10Z","receivedAt":"2026-05-24T17:40:16Z","isPatch":true,"body":"There's still a long tail of situations where Git expands a sparse index\nin-memory in order to operate on blob path entries instead of intelligently\nhandling trees. I was recently alerted to one such case with git restore\n--staged -- ..\n\nThe basic idea here is that the pathspec . signals that all paths matter,\nbut what we want to do across those pathspecs will ignore the expanded blob\npaths with the SKIP_WORKTREE bit, so we should avoid expanding the tree when\nwe can.\n\nThis series has two patches: first a test to demonstrate the baseline\nbehavior of git restore across different sparsity cases as well as\ndemonstrate that the index is currently expanded. The second patch includes\nthe fix and maintains the same end-to-end behavior with the only change\nbeing the performance improvement from not expanding the sparse index.\n\nThanks, -Stolee\n\nDerrick Stolee (2):\n  t1092: test 'git restore' with sparse index\n  restore: avoid sparse index expansion\n\n builtin/checkout.c                       | 57 +++++++++++++++++++++---\n t/t1092-sparse-checkout-compatibility.sh | 50 +++++++++++++++++++++\n 2 files changed, 101 insertions(+), 6 deletions(-)\n\n\nbase-commit: aec3f587505a472db67e9462d0702e7d463a449d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2121%2Fderrickstolee%2Frestore-sparse-index-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2121/derrickstolee/restore-sparse-index-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2121\n-- \ngitgitgadget\n"},{"id":"544003","messageId":"7c56d038307d54929d9eaa9b8cb3cf26af181702.1779644412.git.gitgitgadget@gmail.com","threadId":"65683","inReplyTo":"pull.2121.git.1779644412.gitgitgadget@gmail.com","subject":"[PATCH 1/2] t1092: test 'git restore' with sparse index","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-24T17:40:11Z","receivedAt":"2026-05-24T17:40:18Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nA user reported that 'git restore --staged .' causes the sparse index to\nexpand. This is somewhat natural because the '.' pathspec means 'check\nevery path'. However, the restore will not update paths marked with the\nSKIP_WORKTREE bit, so we shouldn't need to process such entries.\n\nFor now, establish the current behavior, including the sparse index\nexpansion, in the t1092 test case as a baseline.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++++++++\n 1 file changed, 50 insertions(+)\n\ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex d98cb4ac11..d69434e7ab 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -2573,4 +2573,54 @@ test_expect_success 'sparse-index is not expanded: merge-ours' '\n \tensure_not_expanded merge -s ours merge-right\n '\n \n+test_expect_success 'restore --staged with sparse definition' '\n+\tinit_repos &&\n+\n+\t# Stage changes within the sparse definition\n+\ttest_all_match git checkout -b restore-staged-1 base &&\n+\ttest_all_match git reset --soft update-deep &&\n+\ttest_all_match git restore --staged . &&\n+\ttest_all_match git status --porcelain=v2 &&\n+\ttest_all_match git diff --cached\n+'\n+\n+test_expect_success 'restore --staged with outside sparse definition' '\n+\tinit_repos &&\n+\n+\t# Stage changes that include paths outside the sparse definition.\n+\t# Although the working tree differs between full and sparse checkouts\n+\t# after restore, the state of the index should be the same.\n+\ttest_all_match git checkout -b restore-staged-2 base &&\n+\ttest_all_match git reset --soft update-folder1 &&\n+\ttest_sparse_match git restore --staged . &&\n+\tgit -C full-checkout restore --staged . &&\n+\ttest_all_match git ls-files -s -- folder1 &&\n+\ttest_all_match git diff --cached -- folder1\n+'\n+\n+test_expect_success 'restore --staged with wildcards' '\n+\tinit_repos &&\n+\n+\ttest_all_match git checkout -b restore-staged-3 base &&\n+\ttest_all_match git reset --soft update-deep &&\n+\ttest_all_match git restore --staged \"deep/*\" &&\n+\ttest_all_match git status --porcelain=v2 &&\n+\ttest_all_match git diff --cached\n+'\n+\n+test_expect_success 'sparse-index is expanded: restore --staged' '\n+\tinit_repos &&\n+\n+\tgit -C sparse-index checkout -b restore-staged-exp base &&\n+\tgit -C sparse-index reset --soft update-folder1 &&\n+\tensure_expanded restore --staged .\n+'\n+\n+test_expect_success 'sparse-index is expanded: restore --source --staged' '\n+\tinit_repos &&\n+\n+\tgit -C sparse-index checkout -b restore-source-staged base &&\n+\tensure_expanded restore --source update-folder1 --staged .\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"544004","messageId":"47542cbd42eb13b63d0d852fb2f5bf967952b318.1779644412.git.gitgitgadget@gmail.com","threadId":"65683","inReplyTo":"pull.2121.git.1779644412.gitgitgadget@gmail.com","subject":"[PATCH 2/2] restore: avoid sparse index expansion","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-24T17:40:12Z","receivedAt":"2026-05-24T17:40:20Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nTeach update_some() to handle sparse directory entries at the tree\nlevel rather than expanding the entire sparse index. When iterating a\nsource tree during checkout/restore operations:\n\n - If a directory matches a sparse directory entry with the same OID,\n   skip it entirely (no change needed).\n\n - If the OID differs and we are in non-overlay mode (e.g., restore\n   --staged), update the sparse directory entry's OID in place. This\n   is semantically correct because non-overlay mode removes paths not\n   in the source tree anyway.\n\n - In overlay mode (e.g., checkout <tree> -- .), fall through to\n   recursive descent so individual file entries are preserved\n   correctly.\n\nAlso switch from index_name_pos() to index_name_pos_sparse() for\nindividual file lookups to avoid triggering ensure_full_index() when\nthe file is already individually tracked in the index.\n\nUpdate the test expectation in t1092 to assert that 'restore --staged'\nno longer expands the sparse index.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/checkout.c                       | 57 +++++++++++++++++++++---\n t/t1092-sparse-checkout-compatibility.sh |  8 ++--\n 2 files changed, 55 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 1345e8574a..67f03dea10 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -31,6 +31,7 @@\n #include \"revision.h\"\n #include \"sequencer.h\"\n #include \"setup.h\"\n+#include \"sparse-index.h\"\n #include \"strvec.h\"\n #include \"submodule.h\"\n #include \"symlinks.h\"\n@@ -142,14 +143,56 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\n-\t\t       const char *pathname, unsigned mode, void *context UNUSED)\n+\t\t       const char *pathname, unsigned mode, void *context)\n {\n \tint len;\n \tstruct cache_entry *ce;\n \tint pos;\n+\tint overlay_mode = context ? *((int *)context) : 1;\n \n-\tif (S_ISDIR(mode))\n+\tif (S_ISDIR(mode)) {\n+\t\t/*\n+\t\t * If this directory exists as a sparse directory entry in\n+\t\t * the index, we can handle it at the tree level without\n+\t\t * descending into individual files.\n+\t\t */\n+\t\tif (the_repository->index->sparse_index) {\n+\t\t\tstruct strbuf dirpath = STRBUF_INIT;\n+\n+\t\t\tstrbuf_addbuf(&dirpath, base);\n+\t\t\tstrbuf_addstr(&dirpath, pathname);\n+\t\t\tstrbuf_addch(&dirpath, '/');\n+\n+\t\t\tpos = index_name_pos_sparse(the_repository->index,\n+\t\t\t\t\t\t    dirpath.buf, dirpath.len);\n+\t\t\tif (pos >= 0) {\n+\t\t\t\tstruct cache_entry *old =\n+\t\t\t\t\tthe_repository->index->cache[pos];\n+\t\t\t\tif (S_ISSPARSEDIR(old->ce_mode)) {\n+\t\t\t\t\tif (oideq(oid, &old->oid)) {\n+\t\t\t\t\t\tstrbuf_release(&dirpath);\n+\t\t\t\t\t\treturn 0;\n+\t\t\t\t\t}\n+\t\t\t\t\tif (!overlay_mode) {\n+\t\t\t\t\t\t/*\n+\t\t\t\t\t\t * In non-overlay mode (e.g.,\n+\t\t\t\t\t\t * restore --staged), we can\n+\t\t\t\t\t\t * replace the sparse dir OID\n+\t\t\t\t\t\t * directly since files not in\n+\t\t\t\t\t\t * the source tree should be\n+\t\t\t\t\t\t * removed anyway.\n+\t\t\t\t\t\t */\n+\t\t\t\t\t\toidcpy(&old->oid, oid);\n+\t\t\t\t\t\told->ce_flags |= CE_UPDATE;\n+\t\t\t\t\t\tstrbuf_release(&dirpath);\n+\t\t\t\t\t\treturn 0;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tstrbuf_release(&dirpath);\n+\t\t}\n \t\treturn READ_TREE_RECURSIVE;\n+\t}\n \n \tlen = base->len + strlen(pathname);\n \tce = make_empty_cache_entry(the_repository->index, len);\n@@ -165,7 +208,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base,\n \t * entry in place. Whether it is UPTODATE or not, checkout_entry will\n \t * do the right thing.\n \t */\n-\tpos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen);\n+\tpos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen);\n \tif (pos >= 0) {\n \t\tstruct cache_entry *old = the_repository->index->cache[pos];\n \t\tif (ce->ce_mode == old->ce_mode &&\n@@ -182,10 +225,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base,\n \treturn 0;\n }\n \n-static int read_tree_some(struct tree *tree, const struct pathspec *pathspec)\n+static int read_tree_some(struct tree *tree, const struct pathspec *pathspec,\n+\t\t\t  int overlay_mode)\n {\n \tread_tree(the_repository, tree,\n-\t\t  pathspec, update_some, NULL);\n+\t\t  pathspec, update_some, &overlay_mode);\n \n \t/* update the index with the given tree's info\n \t * for all args, expanding wildcards, and exit\n@@ -580,7 +624,8 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\treturn error(_(\"index file corrupt\"));\n \n \tif (opts->source_tree)\n-\t\tread_tree_some(opts->source_tree, &opts->pathspec);\n+\t\tread_tree_some(opts->source_tree, &opts->pathspec,\n+\t\t\t       opts->overlay_mode);\n \tif (opts->merge)\n \t\tunmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED);\n \ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex d69434e7ab..8186da5c88 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -2608,19 +2608,19 @@ test_expect_success 'restore --staged with wildcards' '\n \ttest_all_match git diff --cached\n '\n \n-test_expect_success 'sparse-index is expanded: restore --staged' '\n+test_expect_success 'sparse-index is not expanded: restore --staged' '\n \tinit_repos &&\n \n \tgit -C sparse-index checkout -b restore-staged-exp base &&\n \tgit -C sparse-index reset --soft update-folder1 &&\n-\tensure_expanded restore --staged .\n+\tensure_not_expanded restore --staged .\n '\n \n-test_expect_success 'sparse-index is expanded: restore --source --staged' '\n+test_expect_success 'sparse-index is not expanded: restore --source --staged' '\n \tinit_repos &&\n \n \tgit -C sparse-index checkout -b restore-source-staged base &&\n-\tensure_expanded restore --source update-folder1 --staged .\n+\tensure_not_expanded restore --source update-folder1 --staged .\n '\n \n test_done\n-- \ngitgitgadget\n"},{"id":"544013","messageId":"xmqqzf1oh158.fsf@gitster.g","threadId":"65683","inReplyTo":"7c56d038307d54929d9eaa9b8cb3cf26af181702.1779644412.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/2] t1092: test 'git restore' with sparse index","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-24T22:51:31Z","receivedAt":"2026-05-24T22:51:34Z","isPatch":true,"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> A user reported that 'git restore --staged .' causes the sparse index to\n> expand. This is somewhat natural because the '.' pathspec means 'check\n> every path'. However, the restore will not update paths marked with the\n> SKIP_WORKTREE bit, so we shouldn't need to process such entries.\n\nInteresting.  So, ideally we should be able to say \"we are doing\neverything because the user gave us '.' from the top level of the\nworking tree, so let's see each entry and decide what to do.  Ah we\nhave this tree entry in this sparse index, and that is outside the\ndirectories we are dealing with in this working tree that is\nsparsely checked out, so we would skip\", and for that we have no\nneed to expand the index.  But in reality, what happens is \"OK, '.'\nso we need to deal with everything. Let's expand.\", which would\nbreak the contents of such a \"skipped\" tree out to constituent\npaths, all of which inherits the SKIP_WORKTREE bit to tell us that\nthese paths are outside the directories we are dealing with\".\n\nThe end result in the working tree should be the same, but we\nunnecessarily expand the index.  Correctness wins with a room for\nimprovement in the performance, which is what we want to see and\nthen improve ;-)  Nice.\n\n> For now, establish the current behavior, including the sparse index\n> expansion, in the t1092 test case as a baseline.\n>\n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++++++++\n>  1 file changed, 50 insertions(+)\n>\n> diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\n> index d98cb4ac11..d69434e7ab 100755\n> --- a/t/t1092-sparse-checkout-compatibility.sh\n> +++ b/t/t1092-sparse-checkout-compatibility.sh\n> @@ -2573,4 +2573,54 @@ test_expect_success 'sparse-index is not expanded: merge-ours' '\n>  \tensure_not_expanded merge -s ours merge-right\n>  '\n>  \n> +test_expect_success 'restore --staged with sparse definition' '\n> +\tinit_repos &&\n> +\n> +\t# Stage changes within the sparse definition\n> +\ttest_all_match git checkout -b restore-staged-1 base &&\n> +\ttest_all_match git reset --soft update-deep &&\n> +\ttest_all_match git restore --staged . &&\n> +\ttest_all_match git status --porcelain=v2 &&\n> +\ttest_all_match git diff --cached\n> +'\n> +\n> +test_expect_success 'restore --staged with outside sparse definition' '\n> +\tinit_repos &&\n> +\n> +\t# Stage changes that include paths outside the sparse definition.\n> +\t# Although the working tree differs between full and sparse checkouts\n> +\t# after restore, the state of the index should be the same.\n> +\ttest_all_match git checkout -b restore-staged-2 base &&\n> +\ttest_all_match git reset --soft update-folder1 &&\n> +\ttest_sparse_match git restore --staged . &&\n> +\tgit -C full-checkout restore --staged . &&\n> +\ttest_all_match git ls-files -s -- folder1 &&\n> +\ttest_all_match git diff --cached -- folder1\n> +'\n> +\n> +test_expect_success 'restore --staged with wildcards' '\n> +\tinit_repos &&\n> +\n> +\ttest_all_match git checkout -b restore-staged-3 base &&\n> +\ttest_all_match git reset --soft update-deep &&\n> +\ttest_all_match git restore --staged \"deep/*\" &&\n> +\ttest_all_match git status --porcelain=v2 &&\n> +\ttest_all_match git diff --cached\n> +'\n> +\n> +test_expect_success 'sparse-index is expanded: restore --staged' '\n> +\tinit_repos &&\n> +\n> +\tgit -C sparse-index checkout -b restore-staged-exp base &&\n> +\tgit -C sparse-index reset --soft update-folder1 &&\n> +\tensure_expanded restore --staged .\n> +'\n> +\n> +test_expect_success 'sparse-index is expanded: restore --source --staged' '\n> +\tinit_repos &&\n> +\n> +\tgit -C sparse-index checkout -b restore-source-staged base &&\n> +\tensure_expanded restore --source update-folder1 --staged .\n> +'\n> +\n>  test_done\n"},{"id":"544014","messageId":"xmqqtsrwh0hx.fsf@gitster.g","threadId":"65683","inReplyTo":"47542cbd42eb13b63d0d852fb2f5bf967952b318.1779644412.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] restore: avoid sparse index expansion","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-24T23:05:30Z","receivedAt":"2026-05-24T23:05:32Z","isPatch":true,"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> Teach update_some() to handle sparse directory entries at the tree\n> level rather than expanding the entire sparse index. When iterating a\n> source tree during checkout/restore operations:\n>\n>  - If a directory matches a sparse directory entry with the same OID,\n>    skip it entirely (no change needed).\n>\n>  - If the OID differs and we are in non-overlay mode (e.g., restore\n>    --staged), update the sparse directory entry's OID in place. This\n>    is semantically correct because non-overlay mode removes paths not\n>    in the source tree anyway.\n>\n>  - In overlay mode (e.g., checkout <tree> -- .), fall through to\n>    recursive descent so individual file entries are preserved\n>    correctly.\n>\n> Also switch from index_name_pos() to index_name_pos_sparse() for\n> individual file lookups to avoid triggering ensure_full_index() when\n> the file is already individually tracked in the index.\n>\n> Update the test expectation in t1092 to assert that 'restore --staged'\n> no longer expands the sparse index.\n>\n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  builtin/checkout.c                       | 57 +++++++++++++++++++++---\n>  t/t1092-sparse-checkout-compatibility.sh |  8 ++--\n>  2 files changed, 55 insertions(+), 10 deletions(-)\n>\n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index 1345e8574a..67f03dea10 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -31,6 +31,7 @@\n>  #include \"revision.h\"\n>  #include \"sequencer.h\"\n>  #include \"setup.h\"\n> +#include \"sparse-index.h\"\n>  #include \"strvec.h\"\n>  #include \"submodule.h\"\n>  #include \"symlinks.h\"\n> @@ -142,14 +143,56 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm\n>  }\n>  \n>  static int update_some(const struct object_id *oid, struct strbuf *base,\n> -\t\t       const char *pathname, unsigned mode, void *context UNUSED)\n> +\t\t       const char *pathname, unsigned mode, void *context)\n>  {\n>  \tint len;\n>  \tstruct cache_entry *ce;\n>  \tint pos;\n> +\tint overlay_mode = context ? *((int *)context) : 1;\n>  \n> -\tif (S_ISDIR(mode))\n> +\tif (S_ISDIR(mode)) {\n> +\t\t/*\n> +\t\t * If this directory exists as a sparse directory entry in\n> +\t\t * the index, we can handle it at the tree level without\n> +\t\t * descending into individual files.\n> +\t\t */\n> +\t\tif (the_repository->index->sparse_index) {\n\nI wonder if this deep nesting is a sign that the newly added code\nfrom here to ...\n\n> +\t\t\tstruct strbuf dirpath = STRBUF_INIT;\n> +\n> +\t\t\tstrbuf_addbuf(&dirpath, base);\n> +\t\t\tstrbuf_addstr(&dirpath, pathname);\n> +\t\t\tstrbuf_addch(&dirpath, '/');\n> +\n> +\t\t\tpos = index_name_pos_sparse(the_repository->index,\n> +\t\t\t\t\t\t    dirpath.buf, dirpath.len);\n> +\t\t\tif (pos >= 0) {\n> +\t\t\t\tstruct cache_entry *old =\n> +\t\t\t\t\tthe_repository->index->cache[pos];\n> +\t\t\t\tif (S_ISSPARSEDIR(old->ce_mode)) {\n> +\t\t\t\t\tif (oideq(oid, &old->oid)) {\n> +\t\t\t\t\t\tstrbuf_release(&dirpath);\n> +\t\t\t\t\t\treturn 0;\n> +\t\t\t\t\t}\n> +\t\t\t\t\tif (!overlay_mode) {\n> +\t\t\t\t\t\t/*\n> +\t\t\t\t\t\t * In non-overlay mode (e.g.,\n> +\t\t\t\t\t\t * restore --staged), we can\n> +\t\t\t\t\t\t * replace the sparse dir OID\n> +\t\t\t\t\t\t * directly since files not in\n> +\t\t\t\t\t\t * the source tree should be\n> +\t\t\t\t\t\t * removed anyway.\n> +\t\t\t\t\t\t */\n> +\t\t\t\t\t\toidcpy(&old->oid, oid);\n> +\t\t\t\t\t\told->ce_flags |= CE_UPDATE;\n> +\t\t\t\t\t\tstrbuf_release(&dirpath);\n> +\t\t\t\t\t\treturn 0;\n> +\t\t\t\t\t}\n> +\t\t\t\t}\n> +\t\t\t}\n> +\t\t\tstrbuf_release(&dirpath);\n> +\t\t}\n\n... here may become easier to understand if it is made into a small\nhelper function with a descriptive name.\n\n>  \t\treturn READ_TREE_RECURSIVE;\n> +\t}\n>  \n>  \tlen = base->len + strlen(pathname);\n>  \tce = make_empty_cache_entry(the_repository->index, len);\n> @@ -165,7 +208,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base,\n>  \t * entry in place. Whether it is UPTODATE or not, checkout_entry will\n>  \t * do the right thing.\n>  \t */\n> -\tpos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen);\n> +\tpos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen);\n>  \tif (pos >= 0) {\n>  \t\tstruct cache_entry *old = the_repository->index->cache[pos];\n>  \t\tif (ce->ce_mode == old->ce_mode &&\n> @@ -182,10 +225,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base,\n>  \treturn 0;\n>  }\n>  \n> -static int read_tree_some(struct tree *tree, const struct pathspec *pathspec)\n> +static int read_tree_some(struct tree *tree, const struct pathspec *pathspec,\n> +\t\t\t  int overlay_mode)\n>  {\n>  \tread_tree(the_repository, tree,\n> -\t\t  pathspec, update_some, NULL);\n> +\t\t  pathspec, update_some, &overlay_mode);\n>  \n>  \t/* update the index with the given tree's info\n>  \t * for all args, expanding wildcards, and exit\n> @@ -580,7 +624,8 @@ static int checkout_paths(const struct checkout_opts *opts,\n>  \t\treturn error(_(\"index file corrupt\"));\n>  \n>  \tif (opts->source_tree)\n> -\t\tread_tree_some(opts->source_tree, &opts->pathspec);\n> +\t\tread_tree_some(opts->source_tree, &opts->pathspec,\n> +\t\t\t       opts->overlay_mode);\n>  \tif (opts->merge)\n>  \t\tunmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED);\n>  \n> diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\n> index d69434e7ab..8186da5c88 100755\n> --- a/t/t1092-sparse-checkout-compatibility.sh\n> +++ b/t/t1092-sparse-checkout-compatibility.sh\n> @@ -2608,19 +2608,19 @@ test_expect_success 'restore --staged with wildcards' '\n>  \ttest_all_match git diff --cached\n>  '\n>  \n> -test_expect_success 'sparse-index is expanded: restore --staged' '\n> +test_expect_success 'sparse-index is not expanded: restore --staged' '\n>  \tinit_repos &&\n>  \n>  \tgit -C sparse-index checkout -b restore-staged-exp base &&\n>  \tgit -C sparse-index reset --soft update-folder1 &&\n> -\tensure_expanded restore --staged .\n> +\tensure_not_expanded restore --staged .\n>  '\n>  \n> -test_expect_success 'sparse-index is expanded: restore --source --staged' '\n> +test_expect_success 'sparse-index is not expanded: restore --source --staged' '\n>  \tinit_repos &&\n>  \n>  \tgit -C sparse-index checkout -b restore-source-staged base &&\n> -\tensure_expanded restore --source update-folder1 --staged .\n> +\tensure_not_expanded restore --source update-folder1 --staged .\n>  '\n\nVery nice.\n"},{"id":"544090","messageId":"7b8d12c0-21bc-4bc1-9e0e-81fbd4b3a2bd@gmail.com","threadId":"65683","inReplyTo":"xmqqtsrwh0hx.fsf@gitster.g","subject":"Re: [PATCH 2/2] restore: avoid sparse index expansion","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-26T02:54:49Z","receivedAt":"2026-05-26T02:54:51Z","isPatch":true,"body":"On 5/24/26 7:05 PM, Junio C Hamano wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n\n>> -\tif (S_ISDIR(mode))\n>> +\tif (S_ISDIR(mode)) {\n>> +\t\t/*\n>> +\t\t * If this directory exists as a sparse directory entry in\n>> +\t\t * the index, we can handle it at the tree level without\n>> +\t\t * descending into individual files.\n>> +\t\t */\n>> +\t\tif (the_repository->index->sparse_index) {\n> \n> I wonder if this deep nesting is a sign that the newly added code\n> from here to ...\n> \n>> +\t\t\tstruct strbuf dirpath = STRBUF_INIT;\n>> +\n>> +\t\t\tstrbuf_addbuf(&dirpath, base);\n>> +\t\t\tstrbuf_addstr(&dirpath, pathname);\n>> +\t\t\tstrbuf_addch(&dirpath, '/');\n>> +\n>> +\t\t\tpos = index_name_pos_sparse(the_repository->index,\n>> +\t\t\t\t\t\t    dirpath.buf, dirpath.len);\n>> +\t\t\tif (pos >= 0) {\n>> +\t\t\t\tstruct cache_entry *old =\n>> +\t\t\t\t\tthe_repository->index->cache[pos];\n>> +\t\t\t\tif (S_ISSPARSEDIR(old->ce_mode)) {\n>> +\t\t\t\t\tif (oideq(oid, &old->oid)) {\n>> +\t\t\t\t\t\tstrbuf_release(&dirpath);\n>> +\t\t\t\t\t\treturn 0;\n>> +\t\t\t\t\t}\n>> +\t\t\t\t\tif (!overlay_mode) {\n>> +\t\t\t\t\t\t/*\n>> +\t\t\t\t\t\t * In non-overlay mode (e.g.,\n>> +\t\t\t\t\t\t * restore --staged), we can\n>> +\t\t\t\t\t\t * replace the sparse dir OID\n>> +\t\t\t\t\t\t * directly since files not in\n>> +\t\t\t\t\t\t * the source tree should be\n>> +\t\t\t\t\t\t * removed anyway.\n>> +\t\t\t\t\t\t */\n>> +\t\t\t\t\t\toidcpy(&old->oid, oid);\n>> +\t\t\t\t\t\told->ce_flags |= CE_UPDATE;\n>> +\t\t\t\t\t\tstrbuf_release(&dirpath);\n>> +\t\t\t\t\t\treturn 0;\n>> +\t\t\t\t\t}\n>> +\t\t\t\t}\n>> +\t\t\t}\n>> +\t\t\tstrbuf_release(&dirpath);\n>> +\t\t}\n> \n> ... here may become easier to understand if it is made into a small\n> helper function with a descriptive name.\n\nGood idea. I'll try that and send a v2.\n\nThanks,\n-Stolee\n\n"},{"id":"544122","messageId":"pull.2121.v2.git.1779827195.gitgitgadget@gmail.com","threadId":"65683","inReplyTo":"pull.2121.git.1779644412.gitgitgadget@gmail.com","subject":"[PATCH v2 0/2] restore: better integrate with sparse index","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-26T20:26:32Z","receivedAt":"2026-05-26T20:26:38Z","isPatch":true,"body":"There's still a long tail of situations where Git expands a sparse index\nin-memory in order to operate on blob path entries instead of intelligently\nhandling trees. I was recently alerted to one such case with git restore\n--staged -- ..\n\nThe basic idea here is that the pathspec . signals that all paths matter,\nbut what we want to do across those pathspecs will ignore the expanded blob\npaths with the SKIP_WORKTREE bit, so we should avoid expanding the tree when\nwe can.\n\nThis series has two patches: first a test to demonstrate the baseline\nbehavior of git restore across different sparsity cases as well as\ndemonstrate that the index is currently expanded. The second patch includes\nthe fix and maintains the same end-to-end behavior with the only change\nbeing the performance improvement from not expanding the sparse index.\n\n\nUpdate in v2\n============\n\nThe logic around handling a tree entry is extracted to a helper method,\nmaking the diff easier to read.\n\nThanks, -Stolee\n\nDerrick Stolee (2):\n  t1092: test 'git restore' with sparse index\n  restore: avoid sparse index expansion\n\n builtin/checkout.c                       | 65 +++++++++++++++++++++---\n t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++\n 2 files changed, 109 insertions(+), 6 deletions(-)\n\n\nbase-commit: aec3f587505a472db67e9462d0702e7d463a449d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2121%2Fderrickstolee%2Frestore-sparse-index-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2121/derrickstolee/restore-sparse-index-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2121\n\nRange-diff vs v1:\n\n 1:  7c56d03830 = 1:  7c56d03830 t1092: test 'git restore' with sparse index\n 2:  47542cbd42 ! 2:  88f5d26a33 restore: avoid sparse index expansion\n     @@ builtin/checkout.c\n       #include \"submodule.h\"\n       #include \"symlinks.h\"\n      @@ builtin/checkout.c: static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm\n     + \treturn run_hooks_opt(the_repository, \"post-checkout\", &opt);\n       }\n       \n     ++/*\n     ++ * Handle a tree object and determine if we need to recurse into the\n     ++ * tree (READ_TREE_RECURSIVE) or skip it (0).\n     ++ */\n     ++static int try_update_sparse_directory(const struct object_id *oid,\n     ++\t\t\t\t       struct strbuf *base,\n     ++\t\t\t\t       const char *pathname,\n     ++\t\t\t\t       int overlay_mode)\n     ++{\n     ++\tstruct strbuf dirpath = STRBUF_INIT;\n     ++\tstruct cache_entry *old;\n     ++\tint pos, result = READ_TREE_RECURSIVE;\n     ++\n     ++\tif (!the_repository->index->sparse_index)\n     ++\t\treturn result;\n     ++\n     ++\tstrbuf_addbuf(&dirpath, base);\n     ++\tstrbuf_addstr(&dirpath, pathname);\n     ++\tstrbuf_addch(&dirpath, '/');\n     ++\n     ++\tpos = index_name_pos_sparse(the_repository->index,\n     ++\t\t\t\t    dirpath.buf, dirpath.len);\n     ++\tif (pos < 0)\n     ++\t\tgoto cleanup;\n     ++\n     ++\told = the_repository->index->cache[pos];\n     ++\tif (!S_ISSPARSEDIR(old->ce_mode))\n     ++\t\tgoto cleanup;\n     ++\n     ++\tif (oideq(oid, &old->oid)) {\n     ++\t\t/* Tree content already matches; no need to descend. */\n     ++\t\tresult = 0;\n     ++\t} else if (!overlay_mode) {\n     ++\t\t/*\n     ++\t\t * In non-overlay mode (e.g., restore --staged), replace the\n     ++\t\t * sparse directory OID directly since files not present in\n     ++\t\t * the source tree should be removed anyway.\n     ++\t\t */\n     ++\t\toidcpy(&old->oid, oid);\n     ++\t\told->ce_flags |= CE_UPDATE;\n     ++\t\tresult = 0;\n     ++\t}\n     ++\n     ++cleanup:\n     ++\tstrbuf_release(&dirpath);\n     ++\treturn result;\n     ++}\n     ++\n       static int update_some(const struct object_id *oid, struct strbuf *base,\n      -\t\t       const char *pathname, unsigned mode, void *context UNUSED)\n      +\t\t       const char *pathname, unsigned mode, void *context)\n     @@ builtin/checkout.c: static int post_checkout_hook(struct commit *old_commit, str\n       \tint pos;\n      +\tint overlay_mode = context ? *((int *)context) : 1;\n       \n     --\tif (S_ISDIR(mode))\n     -+\tif (S_ISDIR(mode)) {\n     -+\t\t/*\n     -+\t\t * If this directory exists as a sparse directory entry in\n     -+\t\t * the index, we can handle it at the tree level without\n     -+\t\t * descending into individual files.\n     -+\t\t */\n     -+\t\tif (the_repository->index->sparse_index) {\n     -+\t\t\tstruct strbuf dirpath = STRBUF_INIT;\n     -+\n     -+\t\t\tstrbuf_addbuf(&dirpath, base);\n     -+\t\t\tstrbuf_addstr(&dirpath, pathname);\n     -+\t\t\tstrbuf_addch(&dirpath, '/');\n     -+\n     -+\t\t\tpos = index_name_pos_sparse(the_repository->index,\n     -+\t\t\t\t\t\t    dirpath.buf, dirpath.len);\n     -+\t\t\tif (pos >= 0) {\n     -+\t\t\t\tstruct cache_entry *old =\n     -+\t\t\t\t\tthe_repository->index->cache[pos];\n     -+\t\t\t\tif (S_ISSPARSEDIR(old->ce_mode)) {\n     -+\t\t\t\t\tif (oideq(oid, &old->oid)) {\n     -+\t\t\t\t\t\tstrbuf_release(&dirpath);\n     -+\t\t\t\t\t\treturn 0;\n     -+\t\t\t\t\t}\n     -+\t\t\t\t\tif (!overlay_mode) {\n     -+\t\t\t\t\t\t/*\n     -+\t\t\t\t\t\t * In non-overlay mode (e.g.,\n     -+\t\t\t\t\t\t * restore --staged), we can\n     -+\t\t\t\t\t\t * replace the sparse dir OID\n     -+\t\t\t\t\t\t * directly since files not in\n     -+\t\t\t\t\t\t * the source tree should be\n     -+\t\t\t\t\t\t * removed anyway.\n     -+\t\t\t\t\t\t */\n     -+\t\t\t\t\t\toidcpy(&old->oid, oid);\n     -+\t\t\t\t\t\told->ce_flags |= CE_UPDATE;\n     -+\t\t\t\t\t\tstrbuf_release(&dirpath);\n     -+\t\t\t\t\t\treturn 0;\n     -+\t\t\t\t\t}\n     -+\t\t\t\t}\n     -+\t\t\t}\n     -+\t\t\tstrbuf_release(&dirpath);\n     -+\t\t}\n     - \t\treturn READ_TREE_RECURSIVE;\n     -+\t}\n     + \tif (S_ISDIR(mode))\n     +-\t\treturn READ_TREE_RECURSIVE;\n     ++\t\treturn try_update_sparse_directory(oid, base, pathname,\n     ++\t\t\t\t\t\t   overlay_mode);\n       \n       \tlen = base->len + strlen(pathname);\n       \tce = make_empty_cache_entry(the_repository->index, len);\n\n-- \ngitgitgadget\n"},{"id":"544123","messageId":"7c56d038307d54929d9eaa9b8cb3cf26af181702.1779827195.git.gitgitgadget@gmail.com","threadId":"65683","inReplyTo":"pull.2121.v2.git.1779827195.gitgitgadget@gmail.com","subject":"[PATCH v2 1/2] t1092: test 'git restore' with sparse index","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-26T20:26:33Z","receivedAt":"2026-05-26T20:26:40Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nA user reported that 'git restore --staged .' causes the sparse index to\nexpand. This is somewhat natural because the '.' pathspec means 'check\nevery path'. However, the restore will not update paths marked with the\nSKIP_WORKTREE bit, so we shouldn't need to process such entries.\n\nFor now, establish the current behavior, including the sparse index\nexpansion, in the t1092 test case as a baseline.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++++++++\n 1 file changed, 50 insertions(+)\n\ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex d98cb4ac11..d69434e7ab 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -2573,4 +2573,54 @@ test_expect_success 'sparse-index is not expanded: merge-ours' '\n \tensure_not_expanded merge -s ours merge-right\n '\n \n+test_expect_success 'restore --staged with sparse definition' '\n+\tinit_repos &&\n+\n+\t# Stage changes within the sparse definition\n+\ttest_all_match git checkout -b restore-staged-1 base &&\n+\ttest_all_match git reset --soft update-deep &&\n+\ttest_all_match git restore --staged . &&\n+\ttest_all_match git status --porcelain=v2 &&\n+\ttest_all_match git diff --cached\n+'\n+\n+test_expect_success 'restore --staged with outside sparse definition' '\n+\tinit_repos &&\n+\n+\t# Stage changes that include paths outside the sparse definition.\n+\t# Although the working tree differs between full and sparse checkouts\n+\t# after restore, the state of the index should be the same.\n+\ttest_all_match git checkout -b restore-staged-2 base &&\n+\ttest_all_match git reset --soft update-folder1 &&\n+\ttest_sparse_match git restore --staged . &&\n+\tgit -C full-checkout restore --staged . &&\n+\ttest_all_match git ls-files -s -- folder1 &&\n+\ttest_all_match git diff --cached -- folder1\n+'\n+\n+test_expect_success 'restore --staged with wildcards' '\n+\tinit_repos &&\n+\n+\ttest_all_match git checkout -b restore-staged-3 base &&\n+\ttest_all_match git reset --soft update-deep &&\n+\ttest_all_match git restore --staged \"deep/*\" &&\n+\ttest_all_match git status --porcelain=v2 &&\n+\ttest_all_match git diff --cached\n+'\n+\n+test_expect_success 'sparse-index is expanded: restore --staged' '\n+\tinit_repos &&\n+\n+\tgit -C sparse-index checkout -b restore-staged-exp base &&\n+\tgit -C sparse-index reset --soft update-folder1 &&\n+\tensure_expanded restore --staged .\n+'\n+\n+test_expect_success 'sparse-index is expanded: restore --source --staged' '\n+\tinit_repos &&\n+\n+\tgit -C sparse-index checkout -b restore-source-staged base &&\n+\tensure_expanded restore --source update-folder1 --staged .\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"544124","messageId":"88f5d26a3345bf524bdef872027704c8fb955e74.1779827195.git.gitgitgadget@gmail.com","threadId":"65683","inReplyTo":"pull.2121.v2.git.1779827195.gitgitgadget@gmail.com","subject":"[PATCH v2 2/2] restore: avoid sparse index expansion","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-26T20:26:34Z","receivedAt":"2026-05-26T20:26:41Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nTeach update_some() to handle sparse directory entries at the tree\nlevel rather than expanding the entire sparse index. When iterating a\nsource tree during checkout/restore operations:\n\n - If a directory matches a sparse directory entry with the same OID,\n   skip it entirely (no change needed).\n\n - If the OID differs and we are in non-overlay mode (e.g., restore\n   --staged), update the sparse directory entry's OID in place. This\n   is semantically correct because non-overlay mode removes paths not\n   in the source tree anyway.\n\n - In overlay mode (e.g., checkout <tree> -- .), fall through to\n   recursive descent so individual file entries are preserved\n   correctly.\n\nAlso switch from index_name_pos() to index_name_pos_sparse() for\nindividual file lookups to avoid triggering ensure_full_index() when\nthe file is already individually tracked in the index.\n\nUpdate the test expectation in t1092 to assert that 'restore --staged'\nno longer expands the sparse index.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/checkout.c                       | 65 +++++++++++++++++++++---\n t/t1092-sparse-checkout-compatibility.sh |  8 +--\n 2 files changed, 63 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 1345e8574a..86e23a07b1 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -31,6 +31,7 @@\n #include \"revision.h\"\n #include \"sequencer.h\"\n #include \"setup.h\"\n+#include \"sparse-index.h\"\n #include \"strvec.h\"\n #include \"submodule.h\"\n #include \"symlinks.h\"\n@@ -141,15 +142,65 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm\n \treturn run_hooks_opt(the_repository, \"post-checkout\", &opt);\n }\n \n+/*\n+ * Handle a tree object and determine if we need to recurse into the\n+ * tree (READ_TREE_RECURSIVE) or skip it (0).\n+ */\n+static int try_update_sparse_directory(const struct object_id *oid,\n+\t\t\t\t       struct strbuf *base,\n+\t\t\t\t       const char *pathname,\n+\t\t\t\t       int overlay_mode)\n+{\n+\tstruct strbuf dirpath = STRBUF_INIT;\n+\tstruct cache_entry *old;\n+\tint pos, result = READ_TREE_RECURSIVE;\n+\n+\tif (!the_repository->index->sparse_index)\n+\t\treturn result;\n+\n+\tstrbuf_addbuf(&dirpath, base);\n+\tstrbuf_addstr(&dirpath, pathname);\n+\tstrbuf_addch(&dirpath, '/');\n+\n+\tpos = index_name_pos_sparse(the_repository->index,\n+\t\t\t\t    dirpath.buf, dirpath.len);\n+\tif (pos < 0)\n+\t\tgoto cleanup;\n+\n+\told = the_repository->index->cache[pos];\n+\tif (!S_ISSPARSEDIR(old->ce_mode))\n+\t\tgoto cleanup;\n+\n+\tif (oideq(oid, &old->oid)) {\n+\t\t/* Tree content already matches; no need to descend. */\n+\t\tresult = 0;\n+\t} else if (!overlay_mode) {\n+\t\t/*\n+\t\t * In non-overlay mode (e.g., restore --staged), replace the\n+\t\t * sparse directory OID directly since files not present in\n+\t\t * the source tree should be removed anyway.\n+\t\t */\n+\t\toidcpy(&old->oid, oid);\n+\t\told->ce_flags |= CE_UPDATE;\n+\t\tresult = 0;\n+\t}\n+\n+cleanup:\n+\tstrbuf_release(&dirpath);\n+\treturn result;\n+}\n+\n static int update_some(const struct object_id *oid, struct strbuf *base,\n-\t\t       const char *pathname, unsigned mode, void *context UNUSED)\n+\t\t       const char *pathname, unsigned mode, void *context)\n {\n \tint len;\n \tstruct cache_entry *ce;\n \tint pos;\n+\tint overlay_mode = context ? *((int *)context) : 1;\n \n \tif (S_ISDIR(mode))\n-\t\treturn READ_TREE_RECURSIVE;\n+\t\treturn try_update_sparse_directory(oid, base, pathname,\n+\t\t\t\t\t\t   overlay_mode);\n \n \tlen = base->len + strlen(pathname);\n \tce = make_empty_cache_entry(the_repository->index, len);\n@@ -165,7 +216,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base,\n \t * entry in place. Whether it is UPTODATE or not, checkout_entry will\n \t * do the right thing.\n \t */\n-\tpos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen);\n+\tpos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen);\n \tif (pos >= 0) {\n \t\tstruct cache_entry *old = the_repository->index->cache[pos];\n \t\tif (ce->ce_mode == old->ce_mode &&\n@@ -182,10 +233,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base,\n \treturn 0;\n }\n \n-static int read_tree_some(struct tree *tree, const struct pathspec *pathspec)\n+static int read_tree_some(struct tree *tree, const struct pathspec *pathspec,\n+\t\t\t  int overlay_mode)\n {\n \tread_tree(the_repository, tree,\n-\t\t  pathspec, update_some, NULL);\n+\t\t  pathspec, update_some, &overlay_mode);\n \n \t/* update the index with the given tree's info\n \t * for all args, expanding wildcards, and exit\n@@ -580,7 +632,8 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\treturn error(_(\"index file corrupt\"));\n \n \tif (opts->source_tree)\n-\t\tread_tree_some(opts->source_tree, &opts->pathspec);\n+\t\tread_tree_some(opts->source_tree, &opts->pathspec,\n+\t\t\t       opts->overlay_mode);\n \tif (opts->merge)\n \t\tunmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED);\n \ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex d69434e7ab..8186da5c88 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -2608,19 +2608,19 @@ test_expect_success 'restore --staged with wildcards' '\n \ttest_all_match git diff --cached\n '\n \n-test_expect_success 'sparse-index is expanded: restore --staged' '\n+test_expect_success 'sparse-index is not expanded: restore --staged' '\n \tinit_repos &&\n \n \tgit -C sparse-index checkout -b restore-staged-exp base &&\n \tgit -C sparse-index reset --soft update-folder1 &&\n-\tensure_expanded restore --staged .\n+\tensure_not_expanded restore --staged .\n '\n \n-test_expect_success 'sparse-index is expanded: restore --source --staged' '\n+test_expect_success 'sparse-index is not expanded: restore --source --staged' '\n \tinit_repos &&\n \n \tgit -C sparse-index checkout -b restore-source-staged base &&\n-\tensure_expanded restore --source update-folder1 --staged .\n+\tensure_not_expanded restore --source update-folder1 --staged .\n '\n \n test_done\n-- \ngitgitgadget\n"}]}