Skip to content

Commit f28fc01

Browse files
authored
Merge pull request #417 from vdye/sparse-index/git-reset
Sparse index: `git reset`
2 parents b8a803e + e9331c4 commit f28fc01

File tree

3 files changed

+200
-9
lines changed

3 files changed

+200
-9
lines changed

builtin/reset.c

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,43 @@ static int read_from_tree(const struct pathspec *pathspec,
195195
int intent_to_add)
196196
{
197197
struct diff_options opt;
198+
unsigned int i;
199+
char *skip_worktree_seen = NULL;
198200

199201
memset(&opt, 0, sizeof(opt));
200202
copy_pathspec(&opt.pathspec, pathspec);
201203
opt.output_format = DIFF_FORMAT_CALLBACK;
202204
opt.format_callback = update_index_from_diff;
203205
opt.format_callback_data = &intent_to_add;
204206
opt.flags.override_submodule_config = 1;
207+
opt.flags.recursive = 1;
205208
opt.repo = the_repository;
209+
opt.change = diff_change;
210+
opt.add_remove = diff_addremove;
211+
212+
/*
213+
* When pathspec is given for resetting a cone-mode sparse checkout, it may
214+
* identify entries that are nested in sparse directories, in which case the
215+
* index should be expanded. For the sake of efficiency, this check is
216+
* overly-cautious: anything with a wildcard or a magic prefix requires
217+
* expansion, as well as literal paths that aren't in the sparse checkout
218+
* definition AND don't match any directory in the index.
219+
*/
220+
if (pathspec->nr && the_index.sparse_index) {
221+
if (pathspec->magic || pathspec->has_wildcard) {
222+
ensure_full_index(&the_index);
223+
} else {
224+
for (i = 0; i < pathspec->nr; i++) {
225+
if (!path_in_cone_modesparse_checkout(pathspec->items[i].original, &the_index) &&
226+
!matches_skip_worktree(pathspec, i, &skip_worktree_seen)) {
227+
ensure_full_index(&the_index);
228+
break;
229+
}
230+
}
231+
}
232+
}
233+
234+
free(skip_worktree_seen);
206235

207236
if (do_diff_cache(tree_oid, &opt))
208237
return 1;
@@ -281,9 +310,6 @@ static void parse_args(struct pathspec *pathspec,
281310
}
282311
*rev_ret = rev;
283312

284-
if (read_cache() < 0)
285-
die(_("index file corrupt"));
286-
287313
parse_pathspec(pathspec, 0,
288314
PATHSPEC_PREFER_FULL |
289315
(patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0),
@@ -440,6 +466,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
440466
if (intent_to_add && reset_type != MIXED)
441467
die(_("-N can only be used with --mixed"));
442468

469+
prepare_repo_settings(the_repository);
470+
the_repository->settings.command_requires_full_index = 0;
471+
472+
if (read_cache() < 0)
473+
die(_("index file corrupt"));
474+
443475
/* Soft reset does not touch the index file nor the working tree
444476
* at all, but requires them in a good order. Other resets reset
445477
* the index file to the tree object we are switching to. */

cache-tree.c

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -802,13 +802,31 @@ int write_index_as_tree(struct object_id *oid, struct index_state *index_state,
802802

803803
static void prime_cache_tree_rec(struct repository *r,
804804
struct cache_tree *it,
805-
struct tree *tree)
805+
struct tree *tree,
806+
struct strbuf *tree_path)
806807
{
808+
struct strbuf subtree_path = STRBUF_INIT;
807809
struct tree_desc desc;
808810
struct name_entry entry;
809811
int cnt;
810812

811813
oidcpy(&it->oid, &tree->object.oid);
814+
815+
/*
816+
* If this entry is outside the sparse-checkout cone, then it might be
817+
* a sparse directory entry. Check the index to ensure it is by looking
818+
* for an entry with the exact same name as the tree. If no matching sparse
819+
* entry is found, a staged or conflicted entry is preventing this
820+
* directory from collapsing to a sparse directory entry, so the cache
821+
* tree expansion should continue.
822+
*/
823+
if (r->index->sparse_index &&
824+
!path_in_cone_modesparse_checkout(tree_path->buf, r->index) &&
825+
index_name_pos(r->index, tree_path->buf, tree_path->len) >= 0) {
826+
it->entry_count = 1;
827+
return;
828+
}
829+
812830
init_tree_desc(&desc, tree->buffer, tree->size);
813831
cnt = 0;
814832
while (tree_entry(&desc, &entry)) {
@@ -817,26 +835,38 @@ static void prime_cache_tree_rec(struct repository *r,
817835
else {
818836
struct cache_tree_sub *sub;
819837
struct tree *subtree = lookup_tree(r, &entry.oid);
838+
820839
if (!subtree->object.parsed)
821840
parse_tree(subtree);
822841
sub = cache_tree_sub(it, entry.path);
823842
sub->cache_tree = cache_tree();
824-
prime_cache_tree_rec(r, sub->cache_tree, subtree);
843+
strbuf_reset(&subtree_path);
844+
strbuf_grow(&subtree_path, tree_path->len + entry.pathlen + 1);
845+
strbuf_addbuf(&subtree_path, tree_path);
846+
strbuf_add(&subtree_path, entry.path, entry.pathlen);
847+
strbuf_addch(&subtree_path, '/');
848+
849+
prime_cache_tree_rec(r, sub->cache_tree, subtree, &subtree_path);
825850
cnt += sub->cache_tree->entry_count;
826851
}
827852
}
828853
it->entry_count = cnt;
854+
855+
strbuf_release(&subtree_path);
829856
}
830857

831858
void prime_cache_tree(struct repository *r,
832859
struct index_state *istate,
833860
struct tree *tree)
834861
{
862+
struct strbuf tree_path = STRBUF_INIT;
863+
835864
trace2_region_enter("cache-tree", "prime_cache_tree", r);
836865
cache_tree_free(&istate->cache_tree);
837866
istate->cache_tree = cache_tree();
838867

839-
prime_cache_tree_rec(r, istate->cache_tree, tree);
868+
prime_cache_tree_rec(r, istate->cache_tree, tree, &tree_path);
869+
strbuf_release(&tree_path);
840870
istate->cache_changed |= CACHE_TREE_CHANGED;
841871
trace2_region_leave("cache-tree", "prime_cache_tree", r);
842872
}

t/t1092-sparse-checkout-compatibility.sh

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,113 @@ test_expect_success 'checkout and reset (mixed) [sparse]' '
481481
test_sparse_match git reset update-folder2
482482
'
483483

484+
# NEEDSWORK: with mixed reset, files with differences between HEAD and <commit>
485+
# will be added to the work tree even if outside the sparse checkout
486+
# definition, and even if the file is modified to a state of having no local
487+
# changes. The file is "re-ignored" if a hard reset is executed. We may want to
488+
# change this behavior in the future and enforce that files are not written
489+
# outside of the sparse checkout definition.
490+
test_expect_success 'checkout and mixed reset file tracking [sparse]' '
491+
init_repos &&
492+
493+
test_all_match git checkout -b reset-test update-deep &&
494+
test_all_match git reset update-folder1 &&
495+
test_all_match git reset update-deep &&
496+
497+
# At this point, there are no changes in the working tree. However,
498+
# folder1/a now exists locally (even though it is outside of the sparse
499+
# paths).
500+
run_on_sparse test_path_exists folder1 &&
501+
502+
run_on_all rm folder1/a &&
503+
test_all_match git status --porcelain=v2 &&
504+
505+
test_all_match git reset --hard update-deep &&
506+
run_on_sparse test_path_is_missing folder1 &&
507+
test_path_exists full-checkout/folder1
508+
'
509+
510+
test_expect_success 'checkout and reset (merge)' '
511+
init_repos &&
512+
513+
write_script edit-contents <<-\EOF &&
514+
echo text >>$1
515+
EOF
516+
517+
test_all_match git checkout -b reset-test update-deep &&
518+
run_on_all ../edit-contents a &&
519+
test_all_match git reset --merge deepest &&
520+
test_all_match git status --porcelain=v2 &&
521+
522+
test_all_match git reset --hard update-deep &&
523+
run_on_all ../edit-contents deep/a &&
524+
test_all_match test_must_fail git reset --merge deepest
525+
'
526+
527+
test_expect_success 'checkout and reset (keep)' '
528+
init_repos &&
529+
530+
write_script edit-contents <<-\EOF &&
531+
echo text >>$1
532+
EOF
533+
534+
test_all_match git checkout -b reset-test update-deep &&
535+
run_on_all ../edit-contents a &&
536+
test_all_match git reset --keep deepest &&
537+
test_all_match git status --porcelain=v2 &&
538+
539+
test_all_match git reset --hard update-deep &&
540+
run_on_all ../edit-contents deep/a &&
541+
test_all_match test_must_fail git reset --keep deepest
542+
'
543+
544+
test_expect_success 'reset with pathspecs inside sparse definition' '
545+
init_repos &&
546+
547+
write_script edit-contents <<-\EOF &&
548+
echo text >>$1
549+
EOF
550+
551+
test_all_match git checkout -b reset-test update-deep &&
552+
run_on_all ../edit-contents deep/a &&
553+
554+
test_all_match git reset base -- deep/a &&
555+
test_all_match git status --porcelain=v2 &&
556+
557+
test_all_match git reset base -- nonexistent-file &&
558+
test_all_match git status --porcelain=v2 &&
559+
560+
test_all_match git reset deepest -- deep &&
561+
test_all_match git status --porcelain=v2
562+
'
563+
564+
test_expect_success 'reset with sparse directory pathspec outside definition' '
565+
init_repos &&
566+
567+
test_all_match git checkout -b reset-test update-deep &&
568+
test_all_match git reset --hard update-folder1 &&
569+
test_all_match git reset base -- folder1 &&
570+
test_all_match git status --porcelain=v2
571+
'
572+
573+
test_expect_success 'reset with pathspec match in sparse directory' '
574+
init_repos &&
575+
576+
test_all_match git checkout -b reset-test update-deep &&
577+
test_all_match git reset --hard update-folder1 &&
578+
test_all_match git reset base -- folder1/a &&
579+
test_all_match git status --porcelain=v2
580+
'
581+
582+
test_expect_success 'reset with wildcard pathspec' '
583+
init_repos &&
584+
585+
test_all_match git checkout -b reset-test update-deep &&
586+
test_all_match git reset --hard update-folder1 &&
587+
test_all_match git reset base -- */a &&
588+
test_all_match git status --porcelain=v2
589+
'
590+
484591
test_expect_success 'merge, cherry-pick, and rebase' '
485592
init_repos &&
486593
@@ -644,7 +751,7 @@ test_expect_success 'sparse-index is expanded and converted back' '
644751
init_repos &&
645752
646753
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \
647-
git -C sparse-index -c core.fsmonitor="" reset --hard &&
754+
git -C sparse-index -c core.fsmonitor="" read-tree -mu HEAD &&
648755
test_region index convert_to_sparse trace2.txt &&
649756
test_region index ensure_full_index trace2.txt
650757
'
@@ -681,9 +788,9 @@ test_expect_success 'sparse-index is not expanded' '
681788
ensure_not_expanded checkout - &&
682789
ensure_not_expanded switch rename-out-to-out &&
683790
ensure_not_expanded switch - &&
684-
git -C sparse-index reset --hard &&
791+
ensure_not_expanded reset --hard &&
685792
ensure_not_expanded checkout rename-out-to-out -- deep/deeper1 &&
686-
git -C sparse-index reset --hard &&
793+
ensure_not_expanded reset --hard &&
687794
ensure_not_expanded restore -s rename-out-to-out -- deep/deeper1 &&
688795
689796
echo >>sparse-index/README.md &&
@@ -693,6 +800,28 @@ test_expect_success 'sparse-index is not expanded' '
693800
echo >>sparse-index/untracked.txt &&
694801
ensure_not_expanded add . &&
695802
803+
for ref in update-deep update-folder1 update-folder2 update-deep
804+
do
805+
echo >>sparse-index/README.md &&
806+
ensure_not_expanded reset --mixed $ref
807+
ensure_not_expanded reset --hard $ref
808+
done &&
809+
810+
ensure_not_expanded reset --hard update-deep &&
811+
ensure_not_expanded reset --keep base &&
812+
ensure_not_expanded reset --merge update-deep &&
813+
814+
ensure_not_expanded reset base -- deep/a &&
815+
ensure_not_expanded reset base -- nonexistent-file &&
816+
ensure_not_expanded reset deepest -- deep &&
817+
818+
# Although folder1 is outside the sparse definition, it exists as a
819+
# directory entry in the index, so it will be reset without needing to
820+
# expand the full index.
821+
ensure_not_expanded reset --hard update-folder1 &&
822+
ensure_not_expanded reset base -- folder1 &&
823+
824+
ensure_not_expanded reset --hard update-deep &&
696825
ensure_not_expanded checkout -f update-deep &&
697826
(
698827
sane_unset GIT_TEST_MERGE_ALGORITHM &&

0 commit comments

Comments
 (0)