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:
commit
190f9bf62a
@ -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
7
dir.c
@ -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;
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 &&
|
||||
|
||||
|
147
unpack-trees.c
147
unpack-trees.c
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user