Merge branch 'ds/status-with-sparse-index'
"git status" codepath learned to work with sparsely populated index without hydrating it fully. * ds/status-with-sparse-index: t1092: document bad sparse-checkout behavior fsmonitor: integrate with sparse index wt-status: expand added sparse directory entries status: use sparse-index throughout status: skip sparse-checkout percentage with sparse-index diff-lib: handle index diffs with sparse dirs dir.c: accept a directory as part of cone-mode patterns unpack-trees: unpack sparse directory entries unpack-trees: rename unpack_nondirectories() unpack-trees: compare sparse directories correctly unpack-trees: preserve cache_bottom t1092: add tests for status/add and sparse files t1092: expand repository data shape t1092: replace incorrect 'echo' with 'cat' sparse-index: include EXTENDED flag when expanding sparse-index: skip indexes with unmerged entries
This commit is contained in:
commit
b271a3034f
@ -1517,6 +1517,9 @@ int cmd_status(int argc, const char **argv, const char *prefix)
|
|||||||
if (argc == 2 && !strcmp(argv[1], "-h"))
|
if (argc == 2 && !strcmp(argv[1], "-h"))
|
||||||
usage_with_options(builtin_status_usage, builtin_status_options);
|
usage_with_options(builtin_status_usage, builtin_status_options);
|
||||||
|
|
||||||
|
prepare_repo_settings(the_repository);
|
||||||
|
the_repository->settings.command_requires_full_index = 0;
|
||||||
|
|
||||||
status_init_config(&s, git_status_config);
|
status_init_config(&s, git_status_config);
|
||||||
argc = parse_options(argc, argv, prefix,
|
argc = parse_options(argc, argv, prefix,
|
||||||
builtin_status_options,
|
builtin_status_options,
|
||||||
|
19
diff-lib.c
19
diff-lib.c
@ -325,6 +325,11 @@ static void show_new_file(struct rev_info *revs,
|
|||||||
unsigned dirty_submodule = 0;
|
unsigned dirty_submodule = 0;
|
||||||
struct index_state *istate = revs->diffopt.repo->index;
|
struct index_state *istate = revs->diffopt.repo->index;
|
||||||
|
|
||||||
|
if (new_file && S_ISSPARSEDIR(new_file->ce_mode)) {
|
||||||
|
diff_tree_oid(NULL, &new_file->oid, new_file->name, &revs->diffopt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* New file in the index: it might actually be different in
|
* New file in the index: it might actually be different in
|
||||||
* the working tree.
|
* the working tree.
|
||||||
@ -347,6 +352,20 @@ static int show_modified(struct rev_info *revs,
|
|||||||
unsigned dirty_submodule = 0;
|
unsigned dirty_submodule = 0;
|
||||||
struct index_state *istate = revs->diffopt.repo->index;
|
struct index_state *istate = revs->diffopt.repo->index;
|
||||||
|
|
||||||
|
assert(S_ISSPARSEDIR(old_entry->ce_mode) ==
|
||||||
|
S_ISSPARSEDIR(new_entry->ce_mode));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If both are sparse directory entries, then expand the
|
||||||
|
* modifications to the file level. If only one was a sparse
|
||||||
|
* directory, then they appear as an add and delete instead of
|
||||||
|
* a modification.
|
||||||
|
*/
|
||||||
|
if (S_ISSPARSEDIR(new_entry->ce_mode)) {
|
||||||
|
diff_tree_oid(&old_entry->oid, &new_entry->oid, new_entry->name, &revs->diffopt);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (get_stat_data(istate, new_entry, &oid, &mode, cached, match_missing,
|
if (get_stat_data(istate, new_entry, &oid, &mode, cached, match_missing,
|
||||||
&dirty_submodule, &revs->diffopt) < 0) {
|
&dirty_submodule, &revs->diffopt) < 0) {
|
||||||
if (report_missing)
|
if (report_missing)
|
||||||
|
24
dir.c
24
dir.c
@ -1380,7 +1380,7 @@ enum pattern_match_result path_matches_pattern_list(
|
|||||||
struct path_pattern *pattern;
|
struct path_pattern *pattern;
|
||||||
struct strbuf parent_pathname = STRBUF_INIT;
|
struct strbuf parent_pathname = STRBUF_INIT;
|
||||||
int result = NOT_MATCHED;
|
int result = NOT_MATCHED;
|
||||||
const char *slash_pos;
|
size_t slash_pos;
|
||||||
|
|
||||||
if (!pl->use_cone_patterns) {
|
if (!pl->use_cone_patterns) {
|
||||||
pattern = last_matching_pattern_from_list(pathname, pathlen, basename,
|
pattern = last_matching_pattern_from_list(pathname, pathlen, basename,
|
||||||
@ -1401,21 +1401,35 @@ enum pattern_match_result path_matches_pattern_list(
|
|||||||
strbuf_addch(&parent_pathname, '/');
|
strbuf_addch(&parent_pathname, '/');
|
||||||
strbuf_add(&parent_pathname, pathname, pathlen);
|
strbuf_add(&parent_pathname, pathname, pathlen);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Directory entries are matched if and only if a file
|
||||||
|
* contained immediately within them is matched. For the
|
||||||
|
* case of a directory entry, modify the path to create
|
||||||
|
* a fake filename within this directory, allowing us to
|
||||||
|
* use the file-base matching logic in an equivalent way.
|
||||||
|
*/
|
||||||
|
if (parent_pathname.len > 0 &&
|
||||||
|
parent_pathname.buf[parent_pathname.len - 1] == '/') {
|
||||||
|
slash_pos = parent_pathname.len - 1;
|
||||||
|
strbuf_add(&parent_pathname, "-", 1);
|
||||||
|
} else {
|
||||||
|
const char *slash_ptr = strrchr(parent_pathname.buf, '/');
|
||||||
|
slash_pos = slash_ptr ? slash_ptr - parent_pathname.buf : 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (hashmap_contains_path(&pl->recursive_hashmap,
|
if (hashmap_contains_path(&pl->recursive_hashmap,
|
||||||
&parent_pathname)) {
|
&parent_pathname)) {
|
||||||
result = MATCHED_RECURSIVE;
|
result = MATCHED_RECURSIVE;
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
slash_pos = strrchr(parent_pathname.buf, '/');
|
if (!slash_pos) {
|
||||||
|
|
||||||
if (slash_pos == parent_pathname.buf) {
|
|
||||||
/* include every file in root */
|
/* include every file in root */
|
||||||
result = MATCHED;
|
result = MATCHED;
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
strbuf_setlen(&parent_pathname, slash_pos - parent_pathname.buf);
|
strbuf_setlen(&parent_pathname, slash_pos);
|
||||||
|
|
||||||
if (hashmap_contains_path(&pl->parent_hashmap, &parent_pathname)) {
|
if (hashmap_contains_path(&pl->parent_hashmap, &parent_pathname)) {
|
||||||
result = MATCHED;
|
result = MATCHED;
|
||||||
|
10
read-cache.c
10
read-cache.c
@ -1585,8 +1585,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
|
|||||||
*/
|
*/
|
||||||
preload_index(istate, pathspec, 0);
|
preload_index(istate, pathspec, 0);
|
||||||
trace2_region_enter("index", "refresh", NULL);
|
trace2_region_enter("index", "refresh", NULL);
|
||||||
/* TODO: audit for interaction with sparse-index. */
|
|
||||||
ensure_full_index(istate);
|
|
||||||
for (i = 0; i < istate->cache_nr; i++) {
|
for (i = 0; i < istate->cache_nr; i++) {
|
||||||
struct cache_entry *ce, *new_entry;
|
struct cache_entry *ce, *new_entry;
|
||||||
int cache_errno = 0;
|
int cache_errno = 0;
|
||||||
@ -1601,6 +1600,13 @@ int refresh_index(struct index_state *istate, unsigned int flags,
|
|||||||
if (ignore_skip_worktree && ce_skip_worktree(ce))
|
if (ignore_skip_worktree && ce_skip_worktree(ce))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If this entry is a sparse directory, then there isn't
|
||||||
|
* any stat() information to update. Ignore the entry.
|
||||||
|
*/
|
||||||
|
if (S_ISSPARSEDIR(ce->ce_mode))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (pathspec && !ce_path_match(istate, ce, pathspec, seen))
|
if (pathspec && !ce_path_match(istate, ce, pathspec, seen))
|
||||||
filtered = 1;
|
filtered = 1;
|
||||||
|
|
||||||
|
@ -116,6 +116,17 @@ int set_sparse_index_config(struct repository *repo, int enable)
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int index_has_unmerged_entries(struct index_state *istate)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < istate->cache_nr; i++) {
|
||||||
|
if (ce_stage(istate->cache[i]))
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int convert_to_sparse(struct index_state *istate)
|
int convert_to_sparse(struct index_state *istate)
|
||||||
{
|
{
|
||||||
int test_env;
|
int test_env;
|
||||||
@ -152,6 +163,13 @@ int convert_to_sparse(struct index_state *istate)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NEEDSWORK: If we have unmerged entries, then stay full.
|
||||||
|
* Unmerged entries prevent the cache-tree extension from working.
|
||||||
|
*/
|
||||||
|
if (index_has_unmerged_entries(istate))
|
||||||
|
return 0;
|
||||||
|
|
||||||
if (cache_tree_update(istate, 0)) {
|
if (cache_tree_update(istate, 0)) {
|
||||||
warning(_("unable to update cache-tree, staying full"));
|
warning(_("unable to update cache-tree, staying full"));
|
||||||
return -1;
|
return -1;
|
||||||
@ -168,6 +186,10 @@ int convert_to_sparse(struct index_state *istate)
|
|||||||
cache_tree_free(&istate->cache_tree);
|
cache_tree_free(&istate->cache_tree);
|
||||||
cache_tree_update(istate, 0);
|
cache_tree_update(istate, 0);
|
||||||
|
|
||||||
|
istate->fsmonitor_has_run_once = 0;
|
||||||
|
FREE_AND_NULL(istate->fsmonitor_dirty);
|
||||||
|
FREE_AND_NULL(istate->fsmonitor_last_update);
|
||||||
|
|
||||||
istate->sparse_index = 1;
|
istate->sparse_index = 1;
|
||||||
trace2_region_leave("index", "convert_to_sparse", istate->repo);
|
trace2_region_leave("index", "convert_to_sparse", istate->repo);
|
||||||
return 0;
|
return 0;
|
||||||
@ -195,7 +217,7 @@ static int add_path_to_index(const struct object_id *oid,
|
|||||||
strbuf_addstr(base, path);
|
strbuf_addstr(base, path);
|
||||||
|
|
||||||
ce = make_cache_entry(istate, mode, oid, base->buf, 0, 0);
|
ce = make_cache_entry(istate, mode, oid, base->buf, 0, 0);
|
||||||
ce->ce_flags |= CE_SKIP_WORKTREE;
|
ce->ce_flags |= CE_SKIP_WORKTREE | CE_EXTENDED;
|
||||||
set_index_entry(istate, istate->cache_nr++, ce);
|
set_index_entry(istate, istate->cache_nr++, ce);
|
||||||
|
|
||||||
strbuf_setlen(base, len);
|
strbuf_setlen(base, len);
|
||||||
@ -264,6 +286,9 @@ void ensure_full_index(struct index_state *istate)
|
|||||||
istate->cache = full->cache;
|
istate->cache = full->cache;
|
||||||
istate->cache_nr = full->cache_nr;
|
istate->cache_nr = full->cache_nr;
|
||||||
istate->cache_alloc = full->cache_alloc;
|
istate->cache_alloc = full->cache_alloc;
|
||||||
|
istate->fsmonitor_has_run_once = 0;
|
||||||
|
FREE_AND_NULL(istate->fsmonitor_dirty);
|
||||||
|
FREE_AND_NULL(istate->fsmonitor_last_update);
|
||||||
|
|
||||||
strbuf_release(&base);
|
strbuf_release(&base);
|
||||||
free(full);
|
free(full);
|
||||||
|
@ -17,7 +17,7 @@ test_expect_success 'setup' '
|
|||||||
echo "after folder1" >g &&
|
echo "after folder1" >g &&
|
||||||
echo "after x" >z &&
|
echo "after x" >z &&
|
||||||
mkdir folder1 folder2 deep x &&
|
mkdir folder1 folder2 deep x &&
|
||||||
mkdir deep/deeper1 deep/deeper2 &&
|
mkdir deep/deeper1 deep/deeper2 deep/before deep/later &&
|
||||||
mkdir deep/deeper1/deepest &&
|
mkdir deep/deeper1/deepest &&
|
||||||
echo "after deeper1" >deep/e &&
|
echo "after deeper1" >deep/e &&
|
||||||
echo "after deepest" >deep/deeper1/e &&
|
echo "after deepest" >deep/deeper1/e &&
|
||||||
@ -25,10 +25,23 @@ test_expect_success 'setup' '
|
|||||||
cp a folder2 &&
|
cp a folder2 &&
|
||||||
cp a x &&
|
cp a x &&
|
||||||
cp a deep &&
|
cp a deep &&
|
||||||
|
cp a deep/before &&
|
||||||
cp a deep/deeper1 &&
|
cp a deep/deeper1 &&
|
||||||
cp a deep/deeper2 &&
|
cp a deep/deeper2 &&
|
||||||
|
cp a deep/later &&
|
||||||
cp a deep/deeper1/deepest &&
|
cp a deep/deeper1/deepest &&
|
||||||
cp -r deep/deeper1/deepest deep/deeper2 &&
|
cp -r deep/deeper1/deepest deep/deeper2 &&
|
||||||
|
mkdir deep/deeper1/0 &&
|
||||||
|
mkdir deep/deeper1/0/0 &&
|
||||||
|
touch deep/deeper1/0/1 &&
|
||||||
|
touch deep/deeper1/0/0/0 &&
|
||||||
|
>folder1- &&
|
||||||
|
>folder1.x &&
|
||||||
|
>folder10 &&
|
||||||
|
cp -r deep/deeper1/0 folder1 &&
|
||||||
|
cp -r deep/deeper1/0 folder2 &&
|
||||||
|
echo >>folder1/0/0/0 &&
|
||||||
|
echo >>folder2/0/1 &&
|
||||||
git add . &&
|
git add . &&
|
||||||
git commit -m "initial commit" &&
|
git commit -m "initial commit" &&
|
||||||
git checkout -b base &&
|
git checkout -b base &&
|
||||||
@ -40,7 +53,7 @@ test_expect_success 'setup' '
|
|||||||
done &&
|
done &&
|
||||||
|
|
||||||
git checkout -b rename-base base &&
|
git checkout -b rename-base base &&
|
||||||
echo >folder1/larger-content <<-\EOF &&
|
cat >folder1/larger-content <<-\EOF &&
|
||||||
matching
|
matching
|
||||||
lines
|
lines
|
||||||
help
|
help
|
||||||
@ -56,11 +69,17 @@ test_expect_success 'setup' '
|
|||||||
mv folder1/a folder2/b &&
|
mv folder1/a folder2/b &&
|
||||||
mv folder1/larger-content folder2/edited-content &&
|
mv folder1/larger-content folder2/edited-content &&
|
||||||
echo >>folder2/edited-content &&
|
echo >>folder2/edited-content &&
|
||||||
|
echo >>folder2/0/1 &&
|
||||||
|
echo stuff >>deep/deeper1/a &&
|
||||||
git add . &&
|
git add . &&
|
||||||
git commit -m "rename folder1/... to folder2/..." &&
|
git commit -m "rename folder1/... to folder2/..." &&
|
||||||
|
|
||||||
git checkout -b rename-out-to-in rename-base &&
|
git checkout -b rename-out-to-in rename-base &&
|
||||||
mv folder1/a deep/deeper1/b &&
|
mv folder1/a deep/deeper1/b &&
|
||||||
|
echo more stuff >>deep/deeper1/a &&
|
||||||
|
rm folder2/0/1 &&
|
||||||
|
mkdir folder2/0/1 &&
|
||||||
|
echo >>folder2/0/1/1 &&
|
||||||
mv folder1/larger-content deep/deeper1/edited-content &&
|
mv folder1/larger-content deep/deeper1/edited-content &&
|
||||||
echo >>deep/deeper1/edited-content &&
|
echo >>deep/deeper1/edited-content &&
|
||||||
git add . &&
|
git add . &&
|
||||||
@ -68,6 +87,9 @@ test_expect_success 'setup' '
|
|||||||
|
|
||||||
git checkout -b rename-in-to-out rename-base &&
|
git checkout -b rename-in-to-out rename-base &&
|
||||||
mv deep/deeper1/a folder1/b &&
|
mv deep/deeper1/a folder1/b &&
|
||||||
|
echo >>folder2/0/1 &&
|
||||||
|
rm -rf folder1/0/0 &&
|
||||||
|
echo >>folder1/0/0 &&
|
||||||
mv deep/deeper1/larger-content folder1/edited-content &&
|
mv deep/deeper1/larger-content folder1/edited-content &&
|
||||||
echo >>folder1/edited-content &&
|
echo >>folder1/edited-content &&
|
||||||
git add . &&
|
git add . &&
|
||||||
@ -196,6 +218,14 @@ test_expect_success 'status with options' '
|
|||||||
test_all_match git status --porcelain=v2 -uno
|
test_all_match git status --porcelain=v2 -uno
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'status reports sparse-checkout' '
|
||||||
|
init_repos &&
|
||||||
|
git -C sparse-checkout status >full &&
|
||||||
|
git -C sparse-index status >sparse &&
|
||||||
|
test_i18ngrep "You are in a sparse checkout with " full &&
|
||||||
|
test_i18ngrep "You are in a sparse checkout." sparse
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success 'add, commit, checkout' '
|
test_expect_success 'add, commit, checkout' '
|
||||||
init_repos &&
|
init_repos &&
|
||||||
|
|
||||||
@ -232,6 +262,44 @@ test_expect_success 'add, commit, checkout' '
|
|||||||
test_all_match git checkout -
|
test_all_match git checkout -
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'status/add: outside sparse cone' '
|
||||||
|
init_repos &&
|
||||||
|
|
||||||
|
# adding a "missing" file outside the cone should fail
|
||||||
|
test_sparse_match test_must_fail git add folder1/a &&
|
||||||
|
|
||||||
|
# folder1 is at HEAD, but outside the sparse cone
|
||||||
|
run_on_sparse mkdir folder1 &&
|
||||||
|
cp initial-repo/folder1/a sparse-checkout/folder1/a &&
|
||||||
|
cp initial-repo/folder1/a sparse-index/folder1/a &&
|
||||||
|
|
||||||
|
test_sparse_match git status &&
|
||||||
|
|
||||||
|
write_script edit-contents <<-\EOF &&
|
||||||
|
echo text >>$1
|
||||||
|
EOF
|
||||||
|
run_on_sparse ../edit-contents folder1/a &&
|
||||||
|
run_on_all ../edit-contents folder1/new &&
|
||||||
|
|
||||||
|
test_sparse_match git status --porcelain=v2 &&
|
||||||
|
|
||||||
|
# This "git add folder1/a" fails with a warning
|
||||||
|
# in the sparse repos, differing from the full
|
||||||
|
# repo. This is intentional.
|
||||||
|
test_sparse_match test_must_fail git add folder1/a &&
|
||||||
|
test_sparse_match test_must_fail git add --refresh folder1/a &&
|
||||||
|
test_all_match git status --porcelain=v2 &&
|
||||||
|
|
||||||
|
test_all_match git add . &&
|
||||||
|
test_all_match git status --porcelain=v2 &&
|
||||||
|
test_all_match git commit -m folder1/new &&
|
||||||
|
|
||||||
|
run_on_all ../edit-contents folder1/newer &&
|
||||||
|
test_all_match git add folder1/ &&
|
||||||
|
test_all_match git status --porcelain=v2 &&
|
||||||
|
test_all_match git commit -m folder1/newer
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success 'checkout and reset --hard' '
|
test_expect_success 'checkout and reset --hard' '
|
||||||
init_repos &&
|
init_repos &&
|
||||||
|
|
||||||
@ -262,13 +330,29 @@ test_expect_success 'diff --staged' '
|
|||||||
test_all_match git diff --staged
|
test_all_match git diff --staged
|
||||||
'
|
'
|
||||||
|
|
||||||
test_expect_success 'diff with renames' '
|
test_expect_success 'diff with renames and conflicts' '
|
||||||
init_repos &&
|
init_repos &&
|
||||||
|
|
||||||
for branch in rename-out-to-out rename-out-to-in rename-in-to-out
|
for branch in rename-out-to-out rename-out-to-in rename-in-to-out
|
||||||
do
|
do
|
||||||
test_all_match git checkout rename-base &&
|
test_all_match git checkout rename-base &&
|
||||||
test_all_match git checkout $branch -- . &&
|
test_all_match git checkout $branch -- . &&
|
||||||
|
test_all_match git status --porcelain=v2 &&
|
||||||
|
test_all_match git diff --staged --no-renames &&
|
||||||
|
test_all_match git diff --staged --find-renames || return 1
|
||||||
|
done
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'diff with directory/file conflicts' '
|
||||||
|
init_repos &&
|
||||||
|
|
||||||
|
for branch in rename-out-to-out rename-out-to-in rename-in-to-out
|
||||||
|
do
|
||||||
|
git -C full-checkout reset --hard &&
|
||||||
|
test_sparse_match git reset --hard &&
|
||||||
|
test_all_match git checkout $branch &&
|
||||||
|
test_all_match git checkout rename-base -- . &&
|
||||||
|
test_all_match git status --porcelain=v2 &&
|
||||||
test_all_match git diff --staged --no-renames &&
|
test_all_match git diff --staged --no-renames &&
|
||||||
test_all_match git diff --staged --find-renames || return 1
|
test_all_match git diff --staged --find-renames || return 1
|
||||||
done
|
done
|
||||||
@ -308,8 +392,8 @@ test_expect_failure 'blame with pathspec outside sparse definition' '
|
|||||||
test_all_match git blame deep/deeper2/deepest/a
|
test_all_match git blame deep/deeper2/deepest/a
|
||||||
'
|
'
|
||||||
|
|
||||||
# TODO: reset currently does not behave as expected when in a
|
# NEEDSWORK: a sparse-checkout behaves differently from a full checkout
|
||||||
# sparse-checkout.
|
# in this scenario, but it shouldn't.
|
||||||
test_expect_failure 'checkout and reset (mixed)' '
|
test_expect_failure 'checkout and reset (mixed)' '
|
||||||
init_repos &&
|
init_repos &&
|
||||||
|
|
||||||
@ -319,8 +403,8 @@ test_expect_failure 'checkout and reset (mixed)' '
|
|||||||
test_all_match git reset update-folder2
|
test_all_match git reset update-folder2
|
||||||
'
|
'
|
||||||
|
|
||||||
# Ensure that sparse-index behaves identically to
|
# NEEDSWORK: a sparse-checkout behaves differently from a full checkout
|
||||||
# sparse-checkout with a full index.
|
# in this scenario, but it shouldn't.
|
||||||
test_expect_success 'checkout and reset (mixed) [sparse]' '
|
test_expect_success 'checkout and reset (mixed) [sparse]' '
|
||||||
init_repos &&
|
init_repos &&
|
||||||
|
|
||||||
@ -352,6 +436,28 @@ test_expect_success 'merge with outside renames' '
|
|||||||
done
|
done
|
||||||
'
|
'
|
||||||
|
|
||||||
|
# Sparse-index fails to convert the index in the
|
||||||
|
# final 'git cherry-pick' command.
|
||||||
|
test_expect_success 'cherry-pick with conflicts' '
|
||||||
|
init_repos &&
|
||||||
|
|
||||||
|
write_script edit-conflict <<-\EOF &&
|
||||||
|
echo $1 >conflict
|
||||||
|
EOF
|
||||||
|
|
||||||
|
test_all_match git checkout -b to-cherry-pick &&
|
||||||
|
run_on_all ../edit-conflict ABC &&
|
||||||
|
test_all_match git add conflict &&
|
||||||
|
test_all_match git commit -m "conflict to pick" &&
|
||||||
|
|
||||||
|
test_all_match git checkout -B base HEAD~1 &&
|
||||||
|
run_on_all ../edit-conflict DEF &&
|
||||||
|
test_all_match git add conflict &&
|
||||||
|
test_all_match git commit -m "conflict in base" &&
|
||||||
|
|
||||||
|
test_all_match test_must_fail git cherry-pick to-cherry-pick
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success 'clean' '
|
test_expect_success 'clean' '
|
||||||
init_repos &&
|
init_repos &&
|
||||||
|
|
||||||
@ -405,12 +511,52 @@ test_expect_success 'sparse-index is expanded and converted back' '
|
|||||||
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \
|
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="" reset --hard &&
|
||||||
test_region index convert_to_sparse trace2.txt &&
|
test_region index convert_to_sparse trace2.txt &&
|
||||||
test_region index ensure_full_index trace2.txt &&
|
|
||||||
|
|
||||||
rm trace2.txt &&
|
|
||||||
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \
|
|
||||||
git -C sparse-index -c core.fsmonitor="" status -uno &&
|
|
||||||
test_region index ensure_full_index trace2.txt
|
test_region index ensure_full_index trace2.txt
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'sparse-index is not expanded' '
|
||||||
|
init_repos &&
|
||||||
|
|
||||||
|
rm -f trace2.txt &&
|
||||||
|
echo >>sparse-index/untracked.txt &&
|
||||||
|
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \
|
||||||
|
git -C sparse-index status &&
|
||||||
|
test_region ! index ensure_full_index trace2.txt
|
||||||
|
'
|
||||||
|
|
||||||
|
# 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' '
|
||||||
|
init_repos &&
|
||||||
|
|
||||||
|
test_all_match git checkout rename-out-to-in &&
|
||||||
|
|
||||||
|
# Sparse checkouts do not agree with full checkouts about
|
||||||
|
# how to report a directory/file conflict during a reset.
|
||||||
|
# This command would fail with test_all_match because the
|
||||||
|
# full checkout reports "T folder1/0/1" while a sparse
|
||||||
|
# checkout reports "D folder1/0/1". This matches because
|
||||||
|
# the sparse checkouts skip "adding" the other side of
|
||||||
|
# the conflict.
|
||||||
|
test_sparse_match git reset --mixed HEAD~1 &&
|
||||||
|
test_sparse_match test-tool read-cache --table --expand &&
|
||||||
|
test_sparse_match git status --porcelain=v2 &&
|
||||||
|
|
||||||
|
# At this point, sparse-checkouts behave differently
|
||||||
|
# from the full-checkout.
|
||||||
|
test_sparse_match git checkout --orphan new-branch &&
|
||||||
|
test_sparse_match test-tool read-cache --table --expand &&
|
||||||
|
test_sparse_match git status --porcelain=v2
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'add everything with deep new file' '
|
||||||
|
init_repos &&
|
||||||
|
|
||||||
|
run_on_sparse git sparse-checkout set deep/deeper1/deepest &&
|
||||||
|
|
||||||
|
run_on_all touch deep/deeper1/x &&
|
||||||
|
test_all_match git add . &&
|
||||||
|
test_all_match git status --porcelain=v2
|
||||||
|
'
|
||||||
|
|
||||||
test_done
|
test_done
|
||||||
|
@ -73,6 +73,7 @@ test_expect_success 'setup' '
|
|||||||
expect*
|
expect*
|
||||||
actual*
|
actual*
|
||||||
marker*
|
marker*
|
||||||
|
trace2*
|
||||||
EOF
|
EOF
|
||||||
'
|
'
|
||||||
|
|
||||||
@ -383,4 +384,52 @@ test_expect_success 'status succeeds after staging/unstaging' '
|
|||||||
)
|
)
|
||||||
'
|
'
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# check_sparse_index_behavior [!]
|
||||||
|
# If "!" is supplied, then we verify that we do not call ensure_full_index
|
||||||
|
# during a call to 'git status'. Otherwise, we verify that we _do_ call it.
|
||||||
|
check_sparse_index_behavior () {
|
||||||
|
git status --porcelain=v2 >expect &&
|
||||||
|
git sparse-checkout init --cone --sparse-index &&
|
||||||
|
git sparse-checkout set dir1 dir2 &&
|
||||||
|
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \
|
||||||
|
git status --porcelain=v2 >actual &&
|
||||||
|
test_region $1 index ensure_full_index trace2.txt &&
|
||||||
|
test_region fsm_hook query trace2.txt &&
|
||||||
|
test_cmp expect actual &&
|
||||||
|
rm trace2.txt &&
|
||||||
|
git sparse-checkout disable
|
||||||
|
}
|
||||||
|
|
||||||
|
test_expect_success 'status succeeds with sparse index' '
|
||||||
|
git reset --hard &&
|
||||||
|
|
||||||
|
test_config core.fsmonitor "$TEST_DIRECTORY/t7519/fsmonitor-all" &&
|
||||||
|
check_sparse_index_behavior ! &&
|
||||||
|
|
||||||
|
write_script .git/hooks/fsmonitor-test<<-\EOF &&
|
||||||
|
printf "last_update_token\0"
|
||||||
|
EOF
|
||||||
|
git config core.fsmonitor .git/hooks/fsmonitor-test &&
|
||||||
|
check_sparse_index_behavior ! &&
|
||||||
|
|
||||||
|
write_script .git/hooks/fsmonitor-test<<-\EOF &&
|
||||||
|
printf "last_update_token\0"
|
||||||
|
printf "dir1/modified\0"
|
||||||
|
EOF
|
||||||
|
check_sparse_index_behavior ! &&
|
||||||
|
|
||||||
|
cp -r dir1 dir1a &&
|
||||||
|
git add dir1a &&
|
||||||
|
git commit -m "add dir1a" &&
|
||||||
|
|
||||||
|
# This one modifies outside the sparse-checkout definition
|
||||||
|
# and hence we expect to expand the sparse-index.
|
||||||
|
write_script .git/hooks/fsmonitor-test<<-\EOF &&
|
||||||
|
printf "last_update_token\0"
|
||||||
|
printf "dir1a/modified\0"
|
||||||
|
EOF
|
||||||
|
check_sparse_index_behavior
|
||||||
|
'
|
||||||
|
|
||||||
test_done
|
test_done
|
||||||
|
142
unpack-trees.c
142
unpack-trees.c
@ -600,6 +600,13 @@ static void mark_ce_used(struct cache_entry *ce, struct unpack_trees_options *o)
|
|||||||
{
|
{
|
||||||
ce->ce_flags |= CE_UNPACKED;
|
ce->ce_flags |= CE_UNPACKED;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If this is a sparse directory, don't advance cache_bottom.
|
||||||
|
* That will be advanced later using the cache-tree data.
|
||||||
|
*/
|
||||||
|
if (S_ISSPARSEDIR(ce->ce_mode))
|
||||||
|
return;
|
||||||
|
|
||||||
if (o->cache_bottom < o->src_index->cache_nr &&
|
if (o->cache_bottom < o->src_index->cache_nr &&
|
||||||
o->src_index->cache[o->cache_bottom] == ce) {
|
o->src_index->cache[o->cache_bottom] == ce) {
|
||||||
int bottom = o->cache_bottom;
|
int bottom = o->cache_bottom;
|
||||||
@ -797,7 +804,7 @@ static int traverse_by_cache_tree(int pos, int nr_entries, int nr_names,
|
|||||||
BUG("We need cache-tree to do this optimization");
|
BUG("We need cache-tree to do this optimization");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Do what unpack_callback() and unpack_nondirectories() normally
|
* Do what unpack_callback() and unpack_single_entry() normally
|
||||||
* do. But we walk all paths in an iterative loop instead.
|
* do. But we walk all paths in an iterative loop instead.
|
||||||
*
|
*
|
||||||
* D/F conflicts and higher stage entries are not a concern
|
* D/F conflicts and higher stage entries are not a concern
|
||||||
@ -976,6 +983,7 @@ static int do_compare_entry(const struct cache_entry *ce,
|
|||||||
int pathlen, ce_len;
|
int pathlen, ce_len;
|
||||||
const char *ce_name;
|
const char *ce_name;
|
||||||
int cmp;
|
int cmp;
|
||||||
|
unsigned ce_mode;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If we have not precomputed the traverse path, it is quicker
|
* If we have not precomputed the traverse path, it is quicker
|
||||||
@ -998,7 +1006,8 @@ static int do_compare_entry(const struct cache_entry *ce,
|
|||||||
ce_len -= pathlen;
|
ce_len -= pathlen;
|
||||||
ce_name = ce->name + pathlen;
|
ce_name = ce->name + pathlen;
|
||||||
|
|
||||||
return df_name_compare(ce_name, ce_len, S_IFREG, name, namelen, mode);
|
ce_mode = S_ISSPARSEDIR(ce->ce_mode) ? S_IFDIR : S_IFREG;
|
||||||
|
return df_name_compare(ce_name, ce_len, ce_mode, name, namelen, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compare_entry(const struct cache_entry *ce, const struct traverse_info *info, const struct name_entry *n)
|
static int compare_entry(const struct cache_entry *ce, const struct traverse_info *info, const struct name_entry *n)
|
||||||
@ -1007,6 +1016,16 @@ static int compare_entry(const struct cache_entry *ce, const struct traverse_inf
|
|||||||
if (cmp)
|
if (cmp)
|
||||||
return cmp;
|
return cmp;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* At this point, we know that we have a prefix match. If ce
|
||||||
|
* is a sparse directory, then allow an exact match. This only
|
||||||
|
* works when the input name is a directory, since ce->name
|
||||||
|
* ends in a directory separator.
|
||||||
|
*/
|
||||||
|
if (S_ISSPARSEDIR(ce->ce_mode) &&
|
||||||
|
ce->ce_namelen == traverse_path_len(info, tree_entry_len(n)) + 1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Even if the beginning compared identically, the ce should
|
* Even if the beginning compared identically, the ce should
|
||||||
* compare as bigger than a directory leading up to it!
|
* compare as bigger than a directory leading up to it!
|
||||||
@ -1033,13 +1052,15 @@ static struct cache_entry *create_ce_entry(const struct traverse_info *info,
|
|||||||
const struct name_entry *n,
|
const struct name_entry *n,
|
||||||
int stage,
|
int stage,
|
||||||
struct index_state *istate,
|
struct index_state *istate,
|
||||||
int is_transient)
|
int is_transient,
|
||||||
|
int is_sparse_directory)
|
||||||
{
|
{
|
||||||
size_t len = traverse_path_len(info, tree_entry_len(n));
|
size_t len = traverse_path_len(info, tree_entry_len(n));
|
||||||
|
size_t alloc_len = is_sparse_directory ? len + 1 : len;
|
||||||
struct cache_entry *ce =
|
struct cache_entry *ce =
|
||||||
is_transient ?
|
is_transient ?
|
||||||
make_empty_transient_cache_entry(len, NULL) :
|
make_empty_transient_cache_entry(alloc_len, NULL) :
|
||||||
make_empty_cache_entry(istate, len);
|
make_empty_cache_entry(istate, alloc_len);
|
||||||
|
|
||||||
ce->ce_mode = create_ce_mode(n->mode);
|
ce->ce_mode = create_ce_mode(n->mode);
|
||||||
ce->ce_flags = create_ce_flags(stage);
|
ce->ce_flags = create_ce_flags(stage);
|
||||||
@ -1048,6 +1069,13 @@ static struct cache_entry *create_ce_entry(const struct traverse_info *info,
|
|||||||
/* len+1 because the cache_entry allocates space for NUL */
|
/* len+1 because the cache_entry allocates space for NUL */
|
||||||
make_traverse_path(ce->name, len + 1, info, n->path, n->pathlen);
|
make_traverse_path(ce->name, len + 1, info, n->path, n->pathlen);
|
||||||
|
|
||||||
|
if (is_sparse_directory) {
|
||||||
|
ce->name[len] = '/';
|
||||||
|
ce->name[len + 1] = '\0';
|
||||||
|
ce->ce_namelen++;
|
||||||
|
ce->ce_flags |= CE_SKIP_WORKTREE;
|
||||||
|
}
|
||||||
|
|
||||||
return ce;
|
return ce;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1056,20 +1084,27 @@ static struct cache_entry *create_ce_entry(const struct traverse_info *info,
|
|||||||
* without actually calling it. If you change the logic here you may need to
|
* without actually calling it. If you change the logic here you may need to
|
||||||
* check and change there as well.
|
* check and change there as well.
|
||||||
*/
|
*/
|
||||||
static int unpack_nondirectories(int n, unsigned long mask,
|
static int unpack_single_entry(int n, unsigned long mask,
|
||||||
unsigned long dirmask,
|
unsigned long dirmask,
|
||||||
struct cache_entry **src,
|
struct cache_entry **src,
|
||||||
const struct name_entry *names,
|
const struct name_entry *names,
|
||||||
const struct traverse_info *info)
|
const struct traverse_info *info)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
struct unpack_trees_options *o = info->data;
|
struct unpack_trees_options *o = info->data;
|
||||||
unsigned long conflicts = info->df_conflicts | dirmask;
|
unsigned long conflicts = info->df_conflicts | dirmask;
|
||||||
|
|
||||||
/* Do we have *only* directories? Nothing to do */
|
|
||||||
if (mask == dirmask && !src[0])
|
if (mask == dirmask && !src[0])
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When we have a sparse directory entry for src[0],
|
||||||
|
* then this isn't necessarily a directory-file conflict.
|
||||||
|
*/
|
||||||
|
if (mask == dirmask && src[0] &&
|
||||||
|
S_ISSPARSEDIR(src[0]->ce_mode))
|
||||||
|
conflicts = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ok, we've filled in up to any potential index entry in src[0],
|
* Ok, we've filled in up to any potential index entry in src[0],
|
||||||
* now do the rest.
|
* now do the rest.
|
||||||
@ -1099,7 +1134,9 @@ static int unpack_nondirectories(int n, unsigned long mask,
|
|||||||
* not stored in the index. otherwise construct the
|
* not stored in the index. otherwise construct the
|
||||||
* cache entry from the index aware logic.
|
* cache entry from the index aware logic.
|
||||||
*/
|
*/
|
||||||
src[i + o->merge] = create_ce_entry(info, names + i, stage, &o->result, o->merge);
|
src[i + o->merge] = create_ce_entry(info, names + i, stage,
|
||||||
|
&o->result, o->merge,
|
||||||
|
bit & dirmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (o->merge) {
|
if (o->merge) {
|
||||||
@ -1203,16 +1240,71 @@ static int find_cache_pos(struct traverse_info *info,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Given a sparse directory entry 'ce', compare ce->name to
|
||||||
|
* info->name + '/' + p->path + '/' if info->name is non-empty.
|
||||||
|
* Compare ce->name to p->path + '/' otherwise. Note that
|
||||||
|
* ce->name must end in a trailing '/' because it is a sparse
|
||||||
|
* directory entry.
|
||||||
|
*/
|
||||||
|
static int sparse_dir_matches_path(const struct cache_entry *ce,
|
||||||
|
struct traverse_info *info,
|
||||||
|
const struct name_entry *p)
|
||||||
|
{
|
||||||
|
assert(S_ISSPARSEDIR(ce->ce_mode));
|
||||||
|
assert(ce->name[ce->ce_namelen - 1] == '/');
|
||||||
|
|
||||||
|
if (info->namelen)
|
||||||
|
return ce->ce_namelen == info->namelen + p->pathlen + 2 &&
|
||||||
|
ce->name[info->namelen] == '/' &&
|
||||||
|
!strncmp(ce->name, info->name, info->namelen) &&
|
||||||
|
!strncmp(ce->name + info->namelen + 1, p->path, p->pathlen);
|
||||||
|
return ce->ce_namelen == p->pathlen + 1 &&
|
||||||
|
!strncmp(ce->name, p->path, p->pathlen);
|
||||||
|
}
|
||||||
|
|
||||||
static struct cache_entry *find_cache_entry(struct traverse_info *info,
|
static struct cache_entry *find_cache_entry(struct traverse_info *info,
|
||||||
const struct name_entry *p)
|
const struct name_entry *p)
|
||||||
{
|
{
|
||||||
|
struct cache_entry *ce;
|
||||||
int pos = find_cache_pos(info, p->path, p->pathlen);
|
int pos = find_cache_pos(info, p->path, p->pathlen);
|
||||||
struct unpack_trees_options *o = info->data;
|
struct unpack_trees_options *o = info->data;
|
||||||
|
|
||||||
if (0 <= pos)
|
if (0 <= pos)
|
||||||
return o->src_index->cache[pos];
|
return o->src_index->cache[pos];
|
||||||
else
|
|
||||||
|
/*
|
||||||
|
* Check for a sparse-directory entry named "path/".
|
||||||
|
* Due to the input p->path not having a trailing
|
||||||
|
* slash, the negative 'pos' value overshoots the
|
||||||
|
* expected position, hence "-2" instead of "-1".
|
||||||
|
*/
|
||||||
|
pos = -pos - 2;
|
||||||
|
|
||||||
|
if (pos < 0 || pos >= o->src_index->cache_nr)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Due to lexicographic sorting and sparse directory
|
||||||
|
* entries ending with a trailing slash, our path as a
|
||||||
|
* sparse directory (e.g "subdir/") and our path as a
|
||||||
|
* file (e.g. "subdir") might be separated by other
|
||||||
|
* paths (e.g. "subdir-").
|
||||||
|
*/
|
||||||
|
while (pos >= 0) {
|
||||||
|
ce = o->src_index->cache[pos];
|
||||||
|
|
||||||
|
if (strncmp(ce->name, p->path, p->pathlen))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (S_ISSPARSEDIR(ce->ce_mode) &&
|
||||||
|
sparse_dir_matches_path(ce, info, p))
|
||||||
|
return ce;
|
||||||
|
|
||||||
|
pos--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void debug_path(struct traverse_info *info)
|
static void debug_path(struct traverse_info *info)
|
||||||
@ -1247,6 +1339,21 @@ static void debug_unpack_callback(int n,
|
|||||||
debug_name_entry(i, names + i);
|
debug_name_entry(i, names + i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns true if and only if the given cache_entry is a
|
||||||
|
* sparse-directory entry that matches the given name_entry
|
||||||
|
* from the tree walk at the given traverse_info.
|
||||||
|
*/
|
||||||
|
static int is_sparse_directory_entry(struct cache_entry *ce,
|
||||||
|
struct name_entry *name,
|
||||||
|
struct traverse_info *info)
|
||||||
|
{
|
||||||
|
if (!ce || !name || !S_ISSPARSEDIR(ce->ce_mode))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return sparse_dir_matches_path(ce, info, name);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Note that traverse_by_cache_tree() duplicates some logic in this function
|
* 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
|
* without actually calling it. If you change the logic here you may need to
|
||||||
@ -1303,7 +1410,7 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unpack_nondirectories(n, mask, dirmask, src, names, info) < 0)
|
if (unpack_single_entry(n, mask, dirmask, src, names, info) < 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (o->merge && src[0]) {
|
if (o->merge && src[0]) {
|
||||||
@ -1333,9 +1440,12 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traverse_trees_recursive(n, dirmask, mask & ~dirmask,
|
if (!is_sparse_directory_entry(src[0], names, info) &&
|
||||||
names, info) < 0)
|
traverse_trees_recursive(n, dirmask, mask & ~dirmask,
|
||||||
|
names, info) < 0) {
|
||||||
return -1;
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
65
wt-status.c
65
wt-status.c
@ -657,6 +657,36 @@ static void wt_status_collect_changes_index(struct wt_status *s)
|
|||||||
clear_pathspec(&rev.prune_data);
|
clear_pathspec(&rev.prune_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int add_file_to_list(const struct object_id *oid,
|
||||||
|
struct strbuf *base, const char *path,
|
||||||
|
unsigned int mode, void *context)
|
||||||
|
{
|
||||||
|
struct string_list_item *it;
|
||||||
|
struct wt_status_change_data *d;
|
||||||
|
struct wt_status *s = context;
|
||||||
|
struct strbuf full_name = STRBUF_INIT;
|
||||||
|
|
||||||
|
if (S_ISDIR(mode))
|
||||||
|
return READ_TREE_RECURSIVE;
|
||||||
|
|
||||||
|
strbuf_add(&full_name, base->buf, base->len);
|
||||||
|
strbuf_addstr(&full_name, path);
|
||||||
|
it = string_list_insert(&s->change, full_name.buf);
|
||||||
|
d = it->util;
|
||||||
|
if (!d) {
|
||||||
|
CALLOC_ARRAY(d, 1);
|
||||||
|
it->util = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->index_status = DIFF_STATUS_ADDED;
|
||||||
|
/* Leave {mode,oid}_head zero for adds. */
|
||||||
|
d->mode_index = mode;
|
||||||
|
oidcpy(&d->oid_index, oid);
|
||||||
|
s->committable = 1;
|
||||||
|
strbuf_release(&full_name);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void wt_status_collect_changes_initial(struct wt_status *s)
|
static void wt_status_collect_changes_initial(struct wt_status *s)
|
||||||
{
|
{
|
||||||
struct index_state *istate = s->repo->index;
|
struct index_state *istate = s->repo->index;
|
||||||
@ -671,6 +701,27 @@ static void wt_status_collect_changes_initial(struct wt_status *s)
|
|||||||
continue;
|
continue;
|
||||||
if (ce_intent_to_add(ce))
|
if (ce_intent_to_add(ce))
|
||||||
continue;
|
continue;
|
||||||
|
if (S_ISSPARSEDIR(ce->ce_mode)) {
|
||||||
|
/*
|
||||||
|
* This is a sparse directory entry, so we want to collect all
|
||||||
|
* of the added files within the tree. This requires recursively
|
||||||
|
* expanding the trees to find the elements that are new in this
|
||||||
|
* tree and marking them with DIFF_STATUS_ADDED.
|
||||||
|
*/
|
||||||
|
struct strbuf base = STRBUF_INIT;
|
||||||
|
struct pathspec ps = { 0 };
|
||||||
|
struct tree *tree = lookup_tree(istate->repo, &ce->oid);
|
||||||
|
|
||||||
|
ps.recursive = 1;
|
||||||
|
ps.has_wildcard = 1;
|
||||||
|
ps.max_depth = -1;
|
||||||
|
|
||||||
|
strbuf_add(&base, ce->name, ce->ce_namelen);
|
||||||
|
read_tree_at(istate->repo, tree, &base, &ps,
|
||||||
|
add_file_to_list, s);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
it = string_list_insert(&s->change, ce->name);
|
it = string_list_insert(&s->change, ce->name);
|
||||||
d = it->util;
|
d = it->util;
|
||||||
if (!d) {
|
if (!d) {
|
||||||
@ -1492,9 +1543,12 @@ static void show_sparse_checkout_in_use(struct wt_status *s,
|
|||||||
if (s->state.sparse_checkout_percentage == SPARSE_CHECKOUT_DISABLED)
|
if (s->state.sparse_checkout_percentage == SPARSE_CHECKOUT_DISABLED)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
status_printf_ln(s, color,
|
if (s->state.sparse_checkout_percentage == SPARSE_CHECKOUT_SPARSE_INDEX)
|
||||||
_("You are in a sparse checkout with %d%% of tracked files present."),
|
status_printf_ln(s, color, _("You are in a sparse checkout."));
|
||||||
s->state.sparse_checkout_percentage);
|
else
|
||||||
|
status_printf_ln(s, color,
|
||||||
|
_("You are in a sparse checkout with %d%% of tracked files present."),
|
||||||
|
s->state.sparse_checkout_percentage);
|
||||||
wt_longstatus_print_trailer(s);
|
wt_longstatus_print_trailer(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1652,6 +1706,11 @@ static void wt_status_check_sparse_checkout(struct repository *r,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r->index->sparse_index) {
|
||||||
|
state->sparse_checkout_percentage = SPARSE_CHECKOUT_SPARSE_INDEX;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (i = 0; i < r->index->cache_nr; i++) {
|
for (i = 0; i < r->index->cache_nr; i++) {
|
||||||
struct cache_entry *ce = r->index->cache[i];
|
struct cache_entry *ce = r->index->cache[i];
|
||||||
if (ce_skip_worktree(ce))
|
if (ce_skip_worktree(ce))
|
||||||
|
@ -78,6 +78,7 @@ enum wt_status_format {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#define SPARSE_CHECKOUT_DISABLED -1
|
#define SPARSE_CHECKOUT_DISABLED -1
|
||||||
|
#define SPARSE_CHECKOUT_SPARSE_INDEX -2
|
||||||
|
|
||||||
struct wt_status_state {
|
struct wt_status_state {
|
||||||
int merge_in_progress;
|
int merge_in_progress;
|
||||||
|
Loading…
Reference in New Issue
Block a user