diff --git a/builtin/checkout.c b/builtin/checkout.c index 1345e8574a79c8..67f03dea102b4d 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -31,6 +31,7 @@ #include "revision.h" #include "sequencer.h" #include "setup.h" +#include "sparse-index.h" #include "strvec.h" #include "submodule.h" #include "symlinks.h" @@ -142,14 +143,56 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm } static int update_some(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, void *context UNUSED) + const char *pathname, unsigned mode, void *context) { int len; struct cache_entry *ce; int pos; + int overlay_mode = context ? *((int *)context) : 1; - if (S_ISDIR(mode)) + if (S_ISDIR(mode)) { + /* + * If this directory exists as a sparse directory entry in + * the index, we can handle it at the tree level without + * descending into individual files. + */ + if (the_repository->index->sparse_index) { + struct strbuf dirpath = STRBUF_INIT; + + strbuf_addbuf(&dirpath, base); + strbuf_addstr(&dirpath, pathname); + strbuf_addch(&dirpath, '/'); + + pos = index_name_pos_sparse(the_repository->index, + dirpath.buf, dirpath.len); + if (pos >= 0) { + struct cache_entry *old = + the_repository->index->cache[pos]; + if (S_ISSPARSEDIR(old->ce_mode)) { + if (oideq(oid, &old->oid)) { + strbuf_release(&dirpath); + return 0; + } + if (!overlay_mode) { + /* + * In non-overlay mode (e.g., + * restore --staged), we can + * replace the sparse dir OID + * directly since files not in + * the source tree should be + * removed anyway. + */ + oidcpy(&old->oid, oid); + old->ce_flags |= CE_UPDATE; + strbuf_release(&dirpath); + return 0; + } + } + } + strbuf_release(&dirpath); + } return READ_TREE_RECURSIVE; + } len = base->len + strlen(pathname); ce = make_empty_cache_entry(the_repository->index, len); @@ -165,7 +208,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base, * entry in place. Whether it is UPTODATE or not, checkout_entry will * do the right thing. */ - pos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen); + pos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen); if (pos >= 0) { struct cache_entry *old = the_repository->index->cache[pos]; if (ce->ce_mode == old->ce_mode && @@ -182,10 +225,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base, return 0; } -static int read_tree_some(struct tree *tree, const struct pathspec *pathspec) +static int read_tree_some(struct tree *tree, const struct pathspec *pathspec, + int overlay_mode) { read_tree(the_repository, tree, - pathspec, update_some, NULL); + pathspec, update_some, &overlay_mode); /* update the index with the given tree's info * for all args, expanding wildcards, and exit @@ -580,7 +624,8 @@ static int checkout_paths(const struct checkout_opts *opts, return error(_("index file corrupt")); if (opts->source_tree) - read_tree_some(opts->source_tree, &opts->pathspec); + read_tree_some(opts->source_tree, &opts->pathspec, + opts->overlay_mode); if (opts->merge) unmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED); diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index d98cb4ac113c67..8186da5c887c56 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -2573,4 +2573,54 @@ test_expect_success 'sparse-index is not expanded: merge-ours' ' ensure_not_expanded merge -s ours merge-right ' +test_expect_success 'restore --staged with sparse definition' ' + init_repos && + + # Stage changes within the sparse definition + test_all_match git checkout -b restore-staged-1 base && + test_all_match git reset --soft update-deep && + test_all_match git restore --staged . && + test_all_match git status --porcelain=v2 && + test_all_match git diff --cached +' + +test_expect_success 'restore --staged with outside sparse definition' ' + init_repos && + + # Stage changes that include paths outside the sparse definition. + # Although the working tree differs between full and sparse checkouts + # after restore, the state of the index should be the same. + test_all_match git checkout -b restore-staged-2 base && + test_all_match git reset --soft update-folder1 && + test_sparse_match git restore --staged . && + git -C full-checkout restore --staged . && + test_all_match git ls-files -s -- folder1 && + test_all_match git diff --cached -- folder1 +' + +test_expect_success 'restore --staged with wildcards' ' + init_repos && + + test_all_match git checkout -b restore-staged-3 base && + test_all_match git reset --soft update-deep && + test_all_match git restore --staged "deep/*" && + test_all_match git status --porcelain=v2 && + test_all_match git diff --cached +' + +test_expect_success 'sparse-index is not expanded: restore --staged' ' + init_repos && + + git -C sparse-index checkout -b restore-staged-exp base && + git -C sparse-index reset --soft update-folder1 && + ensure_not_expanded restore --staged . +' + +test_expect_success 'sparse-index is not expanded: restore --source --staged' ' + init_repos && + + git -C sparse-index checkout -b restore-source-staged base && + ensure_not_expanded restore --source update-folder1 --staged . +' + test_done