diff --git a/builtin/read-tree.c b/builtin/read-tree.c index 2109c4c9e5c1c7..a7b7f822281f8e 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -160,8 +160,6 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix) argc = parse_options(argc, argv, cmd_prefix, read_tree_options, read_tree_usage, 0); - hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); - prefix_set = opts.prefix ? 1 : 0; if (1 < opts.merge + opts.reset + prefix_set) die("Which one? -m, --reset, or --prefix?"); @@ -169,6 +167,11 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix) if (opts.reset) opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED; + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); + /* * NEEDSWORK * @@ -210,6 +213,9 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix) if (opts.merge && !opts.index_only) setup_work_tree(); + if (opts.skip_sparse_checkout) + ensure_full_index(&the_index); + if (opts.merge) { switch (stage - 1) { case 0: @@ -219,11 +225,21 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix) opts.fn = opts.prefix ? bind_merge : oneway_merge; break; case 2: + /* + * TODO: update twoway_merge to handle edit/edit conflicts in + * sparse directories. + */ + ensure_full_index(&the_index); opts.fn = twoway_merge; opts.initial_checkout = is_cache_unborn(); break; case 3: default: + /* + * TODO: update threeway_merge to handle edit/edit conflicts in + * sparse directories. + */ + ensure_full_index(&the_index); opts.fn = threeway_merge; break; } diff --git a/t/perf/p2000-sparse-operations.sh b/t/perf/p2000-sparse-operations.sh index 20e931efd4376a..5da8dd2c9baf66 100755 --- a/t/perf/p2000-sparse-operations.sh +++ b/t/perf/p2000-sparse-operations.sh @@ -113,6 +113,7 @@ test_perf_on_all git checkout -f - test_perf_on_all git reset test_perf_on_all git reset --hard test_perf_on_all git reset -- does-not-exist +test_perf_on_all git read-tree -mu HEAD test_perf_on_all git checkout-index -f --all test_perf_on_all git update-index --add --remove test_perf_on_all git diff diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index d97ef4c9a66eb2..125d978112dee0 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -817,6 +817,117 @@ test_expect_success 'update-index --cacheinfo' ' test_sparse_match git status --porcelain=v2 ' +test_expect_success 'read-tree --merge with files outside sparse definition' ' + init_repos && + + test_all_match git checkout -b test-branch update-folder1 && + for MERGE_TREES in "base HEAD update-folder2" \ + "update-folder1 update-folder2" \ + "update-folder2" + do + # Clean up and remove on-disk files + test_all_match git reset --hard HEAD && + test_sparse_match git sparse-checkout reapply && + + # Although the index matches, without --no-sparse-checkout, outside-of- + # definition files will not exist on disk for sparse checkouts + test_all_match git read-tree -mu $MERGE_TREES && + test_all_match git status --porcelain=v2 && + test_path_is_missing sparse-checkout/folder2 && + test_path_is_missing sparse-index/folder2 && + + test_all_match git read-tree --reset -u HEAD && + test_all_match git status --porcelain=v2 && + + test_all_match git read-tree -mu --no-sparse-checkout $MERGE_TREES && + test_all_match git status --porcelain=v2 && + test_cmp sparse-checkout/folder2/a sparse-index/folder2/a && + test_cmp sparse-checkout/folder2/a full-checkout/folder2/a || return 1 + done +' + +test_expect_success 'read-tree --merge with edit/edit conflicts in sparse directories' ' + init_repos && + + # Merge of multiple changes to same directory (but not same files) should + # succeed + test_all_match git read-tree -mu base rename-base update-folder1 && + test_all_match git status --porcelain=v2 && + + test_all_match git reset --hard && + + test_all_match git read-tree -mu rename-base update-folder2 && + test_all_match git status --porcelain=v2 && + + test_all_match git reset --hard && + + test_all_match test_must_fail git read-tree -mu base update-folder1 rename-out-to-in && + test_all_match test_must_fail git read-tree -mu rename-out-to-in update-folder1 +' + +test_expect_success 'read-tree --merge with modified file outside definition' ' + init_repos && + + write_script edit-contents <<-\EOF && + echo text >>$1 + EOF + + test_all_match git checkout -b test-branch update-folder1 && + run_on_sparse mkdir -p folder2 && + run_on_all ../edit-contents folder2/a && + + # With manually-modified file, full-checkout cannot merge, but it is ignored + # in sparse checkouts + test_must_fail git -C full-checkout read-tree -mu update-folder2 && + test_sparse_match git read-tree -mu update-folder2 && + test_sparse_match git status --porcelain=v2 && + + # Reset only the sparse checkouts to "undo" the merge. All three checkouts + # now have matching indexes and matching folder2/a on disk. + test_sparse_match git read-tree --reset -u HEAD && + + # When --no-sparse-checkout is specified, sparse checkouts identify the file + # on disk and prevent the merge + test_all_match test_must_fail git read-tree -mu --no-sparse-checkout update-folder2 +' + +test_expect_success 'read-tree --prefix outside sparse definition' ' + init_repos && + + # Cannot read-tree --prefix with a single argument when files exist within + # prefix + test_all_match test_must_fail git read-tree --prefix=folder1/ -u update-folder1 && + + test_all_match git read-tree --prefix=folder2/0 -u rename-base && + test_path_is_missing sparse-checkout/folder2 && + test_path_is_missing sparse-index/folder2 && + + test_all_match git read-tree --reset -u HEAD && + test_all_match git read-tree --prefix=folder2/0 -u --no-sparse-checkout rename-base && + test_cmp sparse-checkout/folder2/0/a sparse-index/folder2/0/a && + test_cmp sparse-checkout/folder2/0/a full-checkout/folder2/0/a +' + +test_expect_success 'read-tree --merge with directory-file conflicts' ' + init_repos && + + test_all_match git checkout -b test-branch rename-base && + + # Although the index matches, without --no-sparse-checkout, outside-of- + # definition files will not exist on disk for sparse checkouts + test_sparse_match git read-tree -mu rename-out-to-out && + test_sparse_match git status --porcelain=v2 && + test_path_is_missing sparse-checkout/folder2 && + test_path_is_missing sparse-index/folder2 && + + test_sparse_match git read-tree --reset -u HEAD && + test_sparse_match git status --porcelain=v2 && + + test_sparse_match git read-tree -mu --no-sparse-checkout rename-out-to-out && + test_sparse_match git status --porcelain=v2 && + test_cmp sparse-checkout/folder2/0/1 sparse-index/folder2/0/1 +' + test_expect_success 'merge, cherry-pick, and rebase' ' init_repos && @@ -1395,6 +1506,23 @@ test_expect_success 'sparse index is not expanded: update-index' ' ensure_not_expanded update-index --add --remove --again ' +test_expect_success 'sparse index is not expanded: read-tree' ' + init_repos && + + ensure_not_expanded checkout -b test-branch update-folder1 && + for MERGE_TREES in "update-folder2" + do + ensure_not_expanded read-tree -mu $MERGE_TREES && + ensure_not_expanded reset --hard HEAD || return 1 + done && + + rm -rf sparse-index/deep/deeper2 && + ensure_not_expanded add . && + ensure_not_expanded commit -m "test" && + + ensure_not_expanded read-tree --prefix=deep/deeper2 -u deepest +' + # NEEDSWORK: a sparse-checkout behaves differently from a full checkout # in this scenario, but it shouldn't. test_expect_success 'reset mixed and checkout orphan' ' diff --git a/unpack-trees.c b/unpack-trees.c index 03679f999c2db8..b28bdbfb859250 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -1772,6 +1772,58 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options setup_standard_excludes(o->dir); } + /* + * If the prefix is equal to or contained within a sparse directory, the + * index needs to be expanded to traverse with the specified prefix. Note + * that only the src_index is checked because the prefix is only specified + * in cases where src_index == dst_index. + */ + if (o->prefix && o->src_index->sparse_index) { + int i, ce_len; + struct cache_entry *ce; + int prefix_len = strlen(o->prefix); + + if (prefix_len > 0) { + for (i = 0; i < o->src_index->cache_nr; i++) { + ce = o->src_index->cache[i]; + ce_len = ce_namelen(ce); + + if (!S_ISSPARSEDIR(ce->ce_mode)) + continue; + + /* + * Normalize comparison length for cache entry vs. prefix - + * either may have a trailing slash, which we do not want to + * compare (can assume both are directories). + */ + if (ce->name[ce_len - 1] == '/') + ce_len--; + if (o->prefix[prefix_len - 1] == '/') + prefix_len--; + + /* + * If prefix length is shorter, then it is either a parent to + * this sparse directory, or a completely different path. In + * either case, we don't need to expand the index + */ + if (prefix_len < ce_len) + continue; + + /* + * Avoid the case of expanding the index with a prefix + * a/beta for a sparse directory a/b. + */ + if (ce_len < prefix_len && o->prefix[ce_len] != '/') + continue; + + if (!strncmp(ce->name, o->prefix, ce_len)) { + ensure_full_index(o->src_index); + break; + } + } + } + } + if (!core_apply_sparse_checkout || !o->update) o->skip_sparse_checkout = 1; if (!o->skip_sparse_checkout && !o->pl) { diff --git a/wt-status.c b/wt-status.c index c9bca2294e678f..d51cb602b15fcc 100644 --- a/wt-status.c +++ b/wt-status.c @@ -651,6 +651,13 @@ static void wt_status_collect_changes_index(struct wt_status *s) rev.diffopt.detect_rename = s->detect_rename >= 0 ? s->detect_rename : rev.diffopt.detect_rename; rev.diffopt.rename_limit = s->rename_limit >= 0 ? s->rename_limit : rev.diffopt.rename_limit; rev.diffopt.rename_score = s->rename_score >= 0 ? s->rename_score : rev.diffopt.rename_score; + + /* + * The `recursive` flag must be set to properly perform a diff on sparse + * directory entries, if they exist + */ + rev.diffopt.flags.recursive = 1; + copy_pathspec(&rev.prune_data, &s->pathspec); run_diff_index(&rev, 1); object_array_clear(&rev.pending);