1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-10 18:56:10 +02:00

Merge branch 'vd/sparse-read-tree'

"git read-tree" has been made to be aware of the sparse-index
feature.

* vd/sparse-read-tree:
  read-tree: make three-way merge sparse-aware
  read-tree: make two-way merge sparse-aware
  read-tree: narrow scope of index expansion for '--prefix'
  read-tree: integrate with sparse index
  read-tree: expand sparse checkout test coverage
  read-tree: explicitly disallow prefixes with a leading '/'
  status: fix nested sparse directory diff in sparse index
  sparse-index: prevent repo root from becoming sparse
This commit is contained in:
Junio C Hamano 2022-03-16 17:53:08 -07:00
commit 190f9bf62a
7 changed files with 308 additions and 13 deletions

View File

@ -160,15 +160,22 @@ 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?");
/* Prefix should not start with a directory separator */
if (opts.prefix && opts.prefix[0] == '/')
die("Invalid prefix, prefix cannot start with '/'");
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 +217,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:

7
dir.c
View File

@ -1463,10 +1463,11 @@ static int path_in_sparse_checkout_1(const char *path,
const char *end, *slash;
/*
* We default to accepting a path if there are no patterns or
* they are of the wrong type.
* We default to accepting a path if the path is empty, there are no
* patterns, or the patterns are of the wrong type.
*/
if (init_sparse_checkout_patterns(istate) ||
if (!*path ||
init_sparse_checkout_patterns(istate) ||
(require_cone_mode &&
!istate->sparse_checkout_patterns->use_cone_patterns))
return 1;

View File

@ -117,6 +117,7 @@ test_perf_on_all git diff
test_perf_on_all git diff --cached
test_perf_on_all git blame $SPARSE_CONE/a
test_perf_on_all git blame $SPARSE_CONE/f3/a
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 $SPARSE_CONE/a

View File

@ -25,4 +25,14 @@ test_expect_success 'read-tree --prefix' '
cmp expect actual
'
test_expect_success 'read-tree --prefix with leading slash exits with error' '
git rm -rf . &&
test_must_fail git read-tree --prefix=/two/ $tree &&
git read-tree --prefix=two/ $tree &&
git rm -rf . &&
test_must_fail git read-tree --prefix=/ $tree &&
git read-tree --prefix= $tree
'
test_done

View File

@ -244,6 +244,24 @@ test_expect_success 'expanded in-memory index matches full index' '
test_sparse_match git ls-files --stage
'
test_expect_success 'root directory cannot be sparse' '
init_repos &&
# Remove all in-cone files and directories from the index, collapse index
# with `git sparse-checkout reapply`
git -C sparse-index rm -r . &&
git -C sparse-index sparse-checkout reapply &&
# Verify sparse directories still present, root directory is not sparse
cat >expect <<-EOF &&
folder1/
folder2/
x/
EOF
git -C sparse-index ls-files --sparse >actual &&
test_cmp expect actual
'
test_expect_success 'status with options' '
init_repos &&
test_sparse_match ls &&
@ -260,6 +278,13 @@ test_expect_success 'status with options' '
test_all_match git status --porcelain=v2 -uno
'
test_expect_success 'status with diff in unexpanded sparse directory' '
init_repos &&
test_all_match git checkout rename-base &&
test_all_match git reset --soft rename-out-to-out &&
test_all_match git status --porcelain=v2
'
test_expect_success 'status reports sparse-checkout' '
init_repos &&
git -C sparse-checkout status >full &&
@ -794,6 +819,93 @@ test_expect_success 'update-index --cacheinfo' '
test_cmp expect sparse-checkout-out
'
for MERGE_TREES in "base HEAD update-folder2" \
"update-folder1 update-folder2" \
"update-folder2"
do
test_expect_success "'read-tree -mu $MERGE_TREES' with files outside sparse definition" '
init_repos &&
# 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
'
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 --prefix' '
init_repos &&
# If files differing between the index and target <commit-ish> exist
# inside the prefix, `read-tree --prefix` should fail
test_all_match test_must_fail git read-tree --prefix=deep/ deepest &&
test_all_match test_must_fail git read-tree --prefix=folder1/ update-folder1 &&
# If no differing index entries exist matching the prefix,
# `read-tree --prefix` updates the index successfully
test_all_match git rm -rf deep/deeper1/deepest/ &&
test_all_match git read-tree --prefix=deep/deeper1/deepest -u deepest &&
test_all_match git status --porcelain=v2 &&
test_all_match git rm -rf --sparse folder1/ &&
test_all_match git read-tree --prefix=folder1/ -u update-folder1 &&
test_all_match git status --porcelain=v2 &&
test_all_match git rm -rf --sparse folder2/0 &&
test_all_match git read-tree --prefix=folder2/0/ -u rename-out-to-out &&
test_all_match git status --porcelain=v2
'
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 &&
@ -1297,6 +1409,27 @@ test_expect_success 'sparse index is not expanded: fetch/pull' '
ensure_not_expanded pull full base
'
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 "base HEAD update-folder2" \
"base HEAD rename-base" \
"base update-folder2" \
"base rename-base" \
"update-folder2"
do
ensure_not_expanded read-tree -mu $MERGE_TREES &&
ensure_not_expanded reset --hard || 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
'
test_expect_success 'ls-files' '
init_repos &&

View File

@ -1360,6 +1360,42 @@ static int is_sparse_directory_entry(struct cache_entry *ce,
return sparse_dir_matches_path(ce, info, name);
}
static int unpack_sparse_callback(int n, unsigned long mask, unsigned long dirmask, struct name_entry *names, struct traverse_info *info)
{
struct cache_entry *src[MAX_UNPACK_TREES + 1] = { NULL, };
struct unpack_trees_options *o = info->data;
int ret;
assert(o->merge);
/*
* Unlike in 'unpack_callback', where src[0] is derived from the index when
* merging, src[0] is a transient cache entry derived from the first tree
* provided. Create the temporary entry as if it came from a non-sparse index.
*/
if (!is_null_oid(&names[0].oid)) {
src[0] = create_ce_entry(info, &names[0], 0,
&o->result, 1,
dirmask & (1ul << 0));
src[0]->ce_flags |= (CE_SKIP_WORKTREE | CE_NEW_SKIP_WORKTREE);
}
/*
* 'unpack_single_entry' assumes that src[0] is derived directly from
* the index, rather than from an entry in 'names'. This is *not* true when
* merging a sparse directory, in which case names[0] is the "index" source
* entry. To match the expectations of 'unpack_single_entry', shift past the
* "index" tree (i.e., names[0]) and adjust 'names', 'n', 'mask', and
* 'dirmask' accordingly.
*/
ret = unpack_single_entry(n - 1, mask >> 1, dirmask >> 1, src, names + 1, info);
if (src[0])
discard_cache_entry(src[0]);
return ret >= 0 ? mask : -1;
}
/*
* Note that traverse_by_cache_tree() duplicates some logic in this function
* without actually calling it. If you change the logic here you may need to
@ -1693,6 +1729,41 @@ static void populate_from_existing_patterns(struct unpack_trees_options *o,
o->pl = pl;
}
static void update_sparsity_for_prefix(const char *prefix,
struct index_state *istate)
{
int prefix_len = strlen(prefix);
struct strbuf ce_prefix = STRBUF_INIT;
if (!istate->sparse_index)
return;
while (prefix_len > 0 && prefix[prefix_len - 1] == '/')
prefix_len--;
if (prefix_len <= 0)
BUG("Invalid prefix passed to update_sparsity_for_prefix");
strbuf_grow(&ce_prefix, prefix_len + 1);
strbuf_add(&ce_prefix, prefix, prefix_len);
strbuf_addch(&ce_prefix, '/');
/*
* If the prefix points to a sparse directory or a path inside a sparse
* directory, the index should be expanded. This is accomplished in one
* of two ways:
* - if the prefix is inside a sparse directory, it will be expanded by
* the 'ensure_full_index(...)' call in 'index_name_pos(...)'.
* - if the prefix matches an existing sparse directory entry,
* 'index_name_pos(...)' will return its index position, triggering
* the 'ensure_full_index(...)' below.
*/
if (!path_in_cone_mode_sparse_checkout(ce_prefix.buf, istate) &&
index_name_pos(istate, ce_prefix.buf, ce_prefix.len) >= 0)
ensure_full_index(istate);
strbuf_release(&ce_prefix);
}
static int verify_absent(const struct cache_entry *,
enum unpack_trees_error_types,
@ -1739,6 +1810,9 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
setup_standard_excludes(o->dir);
}
if (o->prefix)
update_sparsity_for_prefix(o->prefix, o->src_index);
if (!core_apply_sparse_checkout || !o->update)
o->skip_sparse_checkout = 1;
if (!o->skip_sparse_checkout && !o->pl) {
@ -2436,6 +2510,37 @@ static int merged_entry(const struct cache_entry *ce,
return 1;
}
static int merged_sparse_dir(const struct cache_entry * const *src, int n,
struct unpack_trees_options *o)
{
struct tree_desc t[MAX_UNPACK_TREES + 1];
void * tree_bufs[MAX_UNPACK_TREES + 1];
struct traverse_info info;
int i, ret;
/*
* Create the tree traversal information for traversing into *only* the
* sparse directory.
*/
setup_traverse_info(&info, src[0]->name);
info.fn = unpack_sparse_callback;
info.data = o;
info.show_all_errors = o->show_all_errors;
info.pathspec = o->pathspec;
/* Get the tree descriptors of the sparse directory in each of the merging trees */
for (i = 0; i < n; i++)
tree_bufs[i] = fill_tree_descriptor(o->src_index->repo, &t[i],
src[i] && !is_null_oid(&src[i]->oid) ? &src[i]->oid : NULL);
ret = traverse_trees(o->src_index, n, t, &info);
for (i = 0; i < n; i++)
free(tree_bufs[i]);
return ret;
}
static int deleted_entry(const struct cache_entry *ce,
const struct cache_entry *old,
struct unpack_trees_options *o)
@ -2540,16 +2645,24 @@ int threeway_merge(const struct cache_entry * const *stages,
*/
/* #14, #14ALT, #2ALT */
if (remote && !df_conflict_head && head_match && !remote_match) {
if (index && !same(index, remote) && !same(index, head))
return reject_merge(index, o);
if (index && !same(index, remote) && !same(index, head)) {
if (S_ISSPARSEDIR(index->ce_mode))
return merged_sparse_dir(stages, 4, o);
else
return reject_merge(index, o);
}
return merged_entry(remote, index, o);
}
/*
* If we have an entry in the index cache, then we want to
* make sure that it matches head.
*/
if (index && !same(index, head))
return reject_merge(index, o);
if (index && !same(index, head)) {
if (S_ISSPARSEDIR(index->ce_mode))
return merged_sparse_dir(stages, 4, o);
else
return reject_merge(index, o);
}
if (head) {
/* #5ALT, #15 */
@ -2611,11 +2724,21 @@ int threeway_merge(const struct cache_entry * const *stages,
}
/* Below are "no merge" cases, which require that the index be
* up-to-date to avoid the files getting overwritten with
* conflict resolution files.
*/
/* Handle "no merge" cases (see t/t1000-read-tree-m-3way.sh) */
if (index) {
/*
* If we've reached the "no merge" cases and we're merging
* a sparse directory, we may have an "edit/edit" conflict that
* can be resolved by individually merging directory contents.
*/
if (S_ISSPARSEDIR(index->ce_mode))
return merged_sparse_dir(stages, 4, o);
/*
* If we're not merging a sparse directory, ensure the index is
* up-to-date to avoid files getting overwritten with conflict
* resolution files
*/
if (verify_uptodate(index, o))
return -1;
}
@ -2706,6 +2829,14 @@ int twoway_merge(const struct cache_entry * const *src,
* reject the merge instead.
*/
return merged_entry(newtree, current, o);
} else if (S_ISSPARSEDIR(current->ce_mode)) {
/*
* The sparse directories differ, but we don't know whether that's
* because of two different files in the directory being modified
* (can be trivially merged) or if there is a real file conflict.
* Merge the sparse directory by OID to compare file-by-file.
*/
return merged_sparse_dir(src, 3, o);
} else
return reject_merge(current, o);
}

View File

@ -651,6 +651,15 @@ 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` option must be enabled to allow the diff to recurse
* into subdirectories of sparse directory index entries. If it is not
* enabled, a subdirectory containing file(s) with changes is reported
* as "modified", rather than the modified files themselves.
*/
rev.diffopt.flags.recursive = 1;
copy_pathspec(&rev.prune_data, &s->pathspec);
run_diff_index(&rev, 1);
object_array_clear(&rev.pending);