restore: avoid sparse index expansion

Teach update_some() to handle sparse directory entries at the tree level rather than expanding the entire sparse index. When iterating a source tree during checkout/restore operations: - If a directory matches a sparse directory entry with the same OID, skip it entirely (no change needed). - If the OID differs and we are in non-overlay mode (e.g., restore --staged), update the sparse directory entry's OID in place. This is semantically correct because non-overlay mode removes paths not in the source tree anyway. - In overlay mode (e.g., checkout <tree> -- .), fall through to recursive descent so individual file entries are preserved correctly. Also switch from index_name_pos() to index_name_pos_sparse() for individual file lookups to avoid triggering ensure_full_index() when the file is already individually tracked in the index. Update the test expectation in t1092 to assert that 'restore --staged' no longer expands the sparse index. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed May 26, 2026 at 20:26 UTC 105aacd072e41c55948d016b46f86f75db2487b3
2 files changed +63 -10
builtin/checkout.c
+59 -6
@@ -31,6 +31,7 @@
31 #include "revision.h"
32 #include "sequencer.h"
33 #include "setup.h"
34 +#include "sparse-index.h"
35 #include "strvec.h"
36 #include "submodule.h"
37 #include "symlinks.h"
@@ -141,15 +142,65 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm
142 return run_hooks_opt(the_repository, "post-checkout", &opt);
143 }
144
145 +/*
146 + * Handle a tree object and determine if we need to recurse into the
147 + * tree (READ_TREE_RECURSIVE) or skip it (0).
148 + */
149 +static int try_update_sparse_directory(const struct object_id *oid,
150 + struct strbuf *base,
151 + const char *pathname,
152 + int overlay_mode)
153 +{
154 + struct strbuf dirpath = STRBUF_INIT;
155 + struct cache_entry *old;
156 + int pos, result = READ_TREE_RECURSIVE;
157 +
158 + if (!the_repository->index->sparse_index)
159 + return result;
160 +
161 + strbuf_addbuf(&dirpath, base);
162 + strbuf_addstr(&dirpath, pathname);
163 + strbuf_addch(&dirpath, '/');
164 +
165 + pos = index_name_pos_sparse(the_repository->index,
166 + dirpath.buf, dirpath.len);
167 + if (pos < 0)
168 + goto cleanup;
169 +
170 + old = the_repository->index->cache[pos];
171 + if (!S_ISSPARSEDIR(old->ce_mode))
172 + goto cleanup;
173 +
174 + if (oideq(oid, &old->oid)) {
175 + /* Tree content already matches; no need to descend. */
176 + result = 0;
177 + } else if (!overlay_mode) {
178 + /*
179 + * In non-overlay mode (e.g., restore --staged), replace the
180 + * sparse directory OID directly since files not present in
181 + * the source tree should be removed anyway.
182 + */
183 + oidcpy(&old->oid, oid);
184 + old->ce_flags |= CE_UPDATE;
185 + result = 0;
186 + }
187 +
188 +cleanup:
189 + strbuf_release(&dirpath);
190 + return result;
191 +}
192 +
193 static int update_some(const struct object_id *oid, struct strbuf *base,
145 - const char *pathname, unsigned mode, void *context UNUSED)
194 + const char *pathname, unsigned mode, void *context)
195 {
196 int len;
197 struct cache_entry *ce;
198 int pos;
199 + int overlay_mode = context ? *((int *)context) : 1;
200
201 if (S_ISDIR(mode))
152 - return READ_TREE_RECURSIVE;
202 + return try_update_sparse_directory(oid, base, pathname,
203 + overlay_mode);
204
205 len = base->len + strlen(pathname);
206 ce = make_empty_cache_entry(the_repository->index, len);
@@ -165,7 +216,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
216 * entry in place. Whether it is UPTODATE or not, checkout_entry will
217 * do the right thing.
218 */
168 - pos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen);
219 + pos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen);
220 if (pos >= 0) {
221 struct cache_entry *old = the_repository->index->cache[pos];
222 if (ce->ce_mode == old->ce_mode &&
@@ -182,10 +233,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
233 return 0;
234 }
235
185 -static int read_tree_some(struct tree *tree, const struct pathspec *pathspec)
236 +static int read_tree_some(struct tree *tree, const struct pathspec *pathspec,
237 + int overlay_mode)
238 {
239 read_tree(the_repository, tree,
188 - pathspec, update_some, NULL);
240 + pathspec, update_some, &overlay_mode);
241
242 /* update the index with the given tree's info
243 * for all args, expanding wildcards, and exit
@@ -580,7 +632,8 @@ static int checkout_paths(const struct checkout_opts *opts,
632 return error(_("index file corrupt"));
633
634 if (opts->source_tree)
583 - read_tree_some(opts->source_tree, &opts->pathspec);
635 + read_tree_some(opts->source_tree, &opts->pathspec,
636 + opts->overlay_mode);
637 if (opts->merge)
638 unmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED);
639
t/t1092-sparse-checkout-compatibility.sh
+4 -4
@@ -2608,19 +2608,19 @@ test_expect_success 'restore --staged with wildcards' '
2608 test_all_match git diff --cached
2609 '
2610
2611 -test_expect_success 'sparse-index is expanded: restore --staged' '
2611 +test_expect_success 'sparse-index is not expanded: restore --staged' '
2612 init_repos &&
2613
2614 git -C sparse-index checkout -b restore-staged-exp base &&
2615 git -C sparse-index reset --soft update-folder1 &&
2616 - ensure_expanded restore --staged .
2616 + ensure_not_expanded restore --staged .
2617 '
2618
2619 -test_expect_success 'sparse-index is expanded: restore --source --staged' '
2619 +test_expect_success 'sparse-index is not expanded: restore --source --staged' '
2620 init_repos &&
2621
2622 git -C sparse-index checkout -b restore-source-staged base &&
2623 - ensure_expanded restore --source update-folder1 --staged .
2623 + ensure_not_expanded restore --source update-folder1 --staged .
2624 '
2625
2626 test_done