Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sparse index: git reset #417

Merged
merged 6 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"));

vdye marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -777,13 +777,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)
vdye marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}

vdye marked this conversation as resolved.
Show resolved Hide resolved
init_tree_desc(&desc, tree->buffer, tree->size);
cnt = 0;
while (tree_entry(&desc, &entry)) {
Expand All @@ -792,26 +810,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 @@ -480,6 +480,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 @@ -643,7 +750,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 @@ -680,9 +787,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 &&
vdye marked this conversation as resolved.
Show resolved Hide resolved
ensure_not_expanded restore -s rename-out-to-out -- deep/deeper1 &&

echo >>sparse-index/README.md &&
Expand All @@ -692,6 +799,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 &&
Comment on lines +817 to +820
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! I'm excited to see that this is possible.

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