Skip to content

Commit

Permalink
Merge pull request microsoft#417 from vdye/sparse-index/git-reset
Browse files Browse the repository at this point in the history
Sparse index: `git reset`
  • Loading branch information
vdye authored Sep 7, 2021
2 parents b8a803e + e9331c4 commit f28fc01
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 9 deletions.
38 changes: 35 additions & 3 deletions builtin/reset.c
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,43 @@ static int read_from_tree(const struct pathspec *pathspec,
int intent_to_add)
{
struct diff_options opt;
unsigned int i;
char *skip_worktree_seen = NULL;

memset(&opt, 0, sizeof(opt));
copy_pathspec(&opt.pathspec, pathspec);
opt.output_format = DIFF_FORMAT_CALLBACK;
opt.format_callback = update_index_from_diff;
opt.format_callback_data = &intent_to_add;
opt.flags.override_submodule_config = 1;
opt.flags.recursive = 1;
opt.repo = the_repository;
opt.change = diff_change;
opt.add_remove = diff_addremove;

/*
* When pathspec is given for resetting a cone-mode sparse checkout, it may
* identify entries that are nested in sparse directories, in which case the
* index should be expanded. For the sake of efficiency, this check is
* overly-cautious: anything with a wildcard or a magic prefix requires
* expansion, as well as literal paths that aren't in the sparse checkout
* definition AND don't match any directory in the index.
*/
if (pathspec->nr && the_index.sparse_index) {
if (pathspec->magic || pathspec->has_wildcard) {
ensure_full_index(&the_index);
} else {
for (i = 0; i < pathspec->nr; i++) {
if (!path_in_cone_modesparse_checkout(pathspec->items[i].original, &the_index) &&
!matches_skip_worktree(pathspec, i, &skip_worktree_seen)) {
ensure_full_index(&the_index);
break;
}
}
}
}

free(skip_worktree_seen);

if (do_diff_cache(tree_oid, &opt))
return 1;
Expand Down Expand Up @@ -281,9 +310,6 @@ static void parse_args(struct pathspec *pathspec,
}
*rev_ret = rev;

if (read_cache() < 0)
die(_("index file corrupt"));

parse_pathspec(pathspec, 0,
PATHSPEC_PREFER_FULL |
(patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0),
Expand Down Expand Up @@ -440,6 +466,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
if (intent_to_add && reset_type != MIXED)
die(_("-N can only be used with --mixed"));

prepare_repo_settings(the_repository);
the_repository->settings.command_requires_full_index = 0;

if (read_cache() < 0)
die(_("index file corrupt"));

/* Soft reset does not touch the index file nor the working tree
* at all, but requires them in a good order. Other resets reset
* the index file to the tree object we are switching to. */
Expand Down
36 changes: 33 additions & 3 deletions cache-tree.c
Original file line number Diff line number Diff line change
Expand Up @@ -802,13 +802,31 @@ int write_index_as_tree(struct object_id *oid, struct index_state *index_state,

static void prime_cache_tree_rec(struct repository *r,
struct cache_tree *it,
struct tree *tree)
struct tree *tree,
struct strbuf *tree_path)
{
struct strbuf subtree_path = STRBUF_INIT;
struct tree_desc desc;
struct name_entry entry;
int cnt;

oidcpy(&it->oid, &tree->object.oid);

/*
* If this entry is outside the sparse-checkout cone, then it might be
* a sparse directory entry. Check the index to ensure it is by looking
* for an entry with the exact same name as the tree. If no matching sparse
* entry is found, a staged or conflicted entry is preventing this
* directory from collapsing to a sparse directory entry, so the cache
* tree expansion should continue.
*/
if (r->index->sparse_index &&
!path_in_cone_modesparse_checkout(tree_path->buf, r->index) &&
index_name_pos(r->index, tree_path->buf, tree_path->len) >= 0) {
it->entry_count = 1;
return;
}

init_tree_desc(&desc, tree->buffer, tree->size);
cnt = 0;
while (tree_entry(&desc, &entry)) {
Expand All @@ -817,26 +835,38 @@ static void prime_cache_tree_rec(struct repository *r,
else {
struct cache_tree_sub *sub;
struct tree *subtree = lookup_tree(r, &entry.oid);

if (!subtree->object.parsed)
parse_tree(subtree);
sub = cache_tree_sub(it, entry.path);
sub->cache_tree = cache_tree();
prime_cache_tree_rec(r, sub->cache_tree, subtree);
strbuf_reset(&subtree_path);
strbuf_grow(&subtree_path, tree_path->len + entry.pathlen + 1);
strbuf_addbuf(&subtree_path, tree_path);
strbuf_add(&subtree_path, entry.path, entry.pathlen);
strbuf_addch(&subtree_path, '/');

prime_cache_tree_rec(r, sub->cache_tree, subtree, &subtree_path);
cnt += sub->cache_tree->entry_count;
}
}
it->entry_count = cnt;

strbuf_release(&subtree_path);
}

void prime_cache_tree(struct repository *r,
struct index_state *istate,
struct tree *tree)
{
struct strbuf tree_path = STRBUF_INIT;

trace2_region_enter("cache-tree", "prime_cache_tree", r);
cache_tree_free(&istate->cache_tree);
istate->cache_tree = cache_tree();

prime_cache_tree_rec(r, istate->cache_tree, tree);
prime_cache_tree_rec(r, istate->cache_tree, tree, &tree_path);
strbuf_release(&tree_path);
istate->cache_changed |= CACHE_TREE_CHANGED;
trace2_region_leave("cache-tree", "prime_cache_tree", r);
}
Expand Down
135 changes: 132 additions & 3 deletions t/t1092-sparse-checkout-compatibility.sh
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,113 @@ test_expect_success 'checkout and reset (mixed) [sparse]' '
test_sparse_match git reset update-folder2
'

# NEEDSWORK: with mixed reset, files with differences between HEAD and <commit>
# will be added to the work tree even if outside the sparse checkout
# definition, and even if the file is modified to a state of having no local
# changes. The file is "re-ignored" if a hard reset is executed. We may want to
# change this behavior in the future and enforce that files are not written
# outside of the sparse checkout definition.
test_expect_success 'checkout and mixed reset file tracking [sparse]' '
init_repos &&
test_all_match git checkout -b reset-test update-deep &&
test_all_match git reset update-folder1 &&
test_all_match git reset update-deep &&
# At this point, there are no changes in the working tree. However,
# folder1/a now exists locally (even though it is outside of the sparse
# paths).
run_on_sparse test_path_exists folder1 &&
run_on_all rm folder1/a &&
test_all_match git status --porcelain=v2 &&
test_all_match git reset --hard update-deep &&
run_on_sparse test_path_is_missing folder1 &&
test_path_exists full-checkout/folder1
'

test_expect_success 'checkout and reset (merge)' '
init_repos &&
write_script edit-contents <<-\EOF &&
echo text >>$1
EOF
test_all_match git checkout -b reset-test update-deep &&
run_on_all ../edit-contents a &&
test_all_match git reset --merge deepest &&
test_all_match git status --porcelain=v2 &&
test_all_match git reset --hard update-deep &&
run_on_all ../edit-contents deep/a &&
test_all_match test_must_fail git reset --merge deepest
'

test_expect_success 'checkout and reset (keep)' '
init_repos &&
write_script edit-contents <<-\EOF &&
echo text >>$1
EOF
test_all_match git checkout -b reset-test update-deep &&
run_on_all ../edit-contents a &&
test_all_match git reset --keep deepest &&
test_all_match git status --porcelain=v2 &&
test_all_match git reset --hard update-deep &&
run_on_all ../edit-contents deep/a &&
test_all_match test_must_fail git reset --keep deepest
'

test_expect_success 'reset with pathspecs inside sparse definition' '
init_repos &&
write_script edit-contents <<-\EOF &&
echo text >>$1
EOF
test_all_match git checkout -b reset-test update-deep &&
run_on_all ../edit-contents deep/a &&
test_all_match git reset base -- deep/a &&
test_all_match git status --porcelain=v2 &&
test_all_match git reset base -- nonexistent-file &&
test_all_match git status --porcelain=v2 &&
test_all_match git reset deepest -- deep &&
test_all_match git status --porcelain=v2
'

test_expect_success 'reset with sparse directory pathspec outside definition' '
init_repos &&
test_all_match git checkout -b reset-test update-deep &&
test_all_match git reset --hard update-folder1 &&
test_all_match git reset base -- folder1 &&
test_all_match git status --porcelain=v2
'

test_expect_success 'reset with pathspec match in sparse directory' '
init_repos &&
test_all_match git checkout -b reset-test update-deep &&
test_all_match git reset --hard update-folder1 &&
test_all_match git reset base -- folder1/a &&
test_all_match git status --porcelain=v2
'

test_expect_success 'reset with wildcard pathspec' '
init_repos &&
test_all_match git checkout -b reset-test update-deep &&
test_all_match git reset --hard update-folder1 &&
test_all_match git reset base -- */a &&
test_all_match git status --porcelain=v2
'

test_expect_success 'merge, cherry-pick, and rebase' '
init_repos &&
Expand Down Expand Up @@ -644,7 +751,7 @@ test_expect_success 'sparse-index is expanded and converted back' '
init_repos &&
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \
git -C sparse-index -c core.fsmonitor="" reset --hard &&
git -C sparse-index -c core.fsmonitor="" read-tree -mu HEAD &&
test_region index convert_to_sparse trace2.txt &&
test_region index ensure_full_index trace2.txt
'
Expand Down Expand Up @@ -681,9 +788,9 @@ test_expect_success 'sparse-index is not expanded' '
ensure_not_expanded checkout - &&
ensure_not_expanded switch rename-out-to-out &&
ensure_not_expanded switch - &&
git -C sparse-index reset --hard &&
ensure_not_expanded reset --hard &&
ensure_not_expanded checkout rename-out-to-out -- deep/deeper1 &&
git -C sparse-index reset --hard &&
ensure_not_expanded reset --hard &&
ensure_not_expanded restore -s rename-out-to-out -- deep/deeper1 &&
echo >>sparse-index/README.md &&
Expand All @@ -693,6 +800,28 @@ test_expect_success 'sparse-index is not expanded' '
echo >>sparse-index/untracked.txt &&
ensure_not_expanded add . &&
for ref in update-deep update-folder1 update-folder2 update-deep
do
echo >>sparse-index/README.md &&
ensure_not_expanded reset --mixed $ref
ensure_not_expanded reset --hard $ref
done &&
ensure_not_expanded reset --hard update-deep &&
ensure_not_expanded reset --keep base &&
ensure_not_expanded reset --merge update-deep &&
ensure_not_expanded reset base -- deep/a &&
ensure_not_expanded reset base -- nonexistent-file &&
ensure_not_expanded reset deepest -- deep &&
# Although folder1 is outside the sparse definition, it exists as a
# directory entry in the index, so it will be reset without needing to
# expand the full index.
ensure_not_expanded reset --hard update-folder1 &&
ensure_not_expanded reset base -- folder1 &&
ensure_not_expanded reset --hard update-deep &&
ensure_not_expanded checkout -f update-deep &&
(
sane_unset GIT_TEST_MERGE_ALGORITHM &&
Expand Down

0 comments on commit f28fc01

Please sign in to comment.