Merge branch 'ds/add-rm-with-sparse-index'

"git add", "git mv", and "git rm" have been adjusted to avoid
updating paths outside of the sparse-checkout definition unless
the user specifies a "--sparse" option.

* ds/add-rm-with-sparse-index:
  advice: update message to suggest '--sparse'
  mv: refuse to move sparse paths
  rm: skip sparse paths with missing SKIP_WORKTREE
  rm: add --sparse option
  add: update --renormalize to skip sparse paths
  add: update --chmod to skip sparse paths
  add: implement the --sparse option
  add: skip tracked paths outside sparse-checkout cone
  add: fail when adding an untracked sparse file
  dir: fix pattern matching on dirs
  dir: select directories correctly
  t1092: behavior for adding sparse files
  t3705: test that 'sparse_entry' is unstaged
This commit is contained in:
Junio C Hamano 2021-10-13 15:15:56 -07:00
commit 2d498a7c89
13 changed files with 504 additions and 51 deletions

View File

@ -9,7 +9,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p] 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]
[--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--sparse]
[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
[--] [<pathspec>...] [--] [<pathspec>...]
@ -79,6 +79,13 @@ in linkgit:gitglossary[7].
--force:: --force::
Allow adding otherwise ignored files. Allow adding otherwise ignored files.
--sparse::
Allow updating index entries outside of the sparse-checkout cone.
Normally, `git add` refuses to update index entries whose paths do
not fit within the sparse-checkout cone, since those files might
be removed from the working tree without warning. See
linkgit:git-sparse-checkout[1] for more details.
-i:: -i::
--interactive:: --interactive::
Add modified contents in the working tree interactively to Add modified contents in the working tree interactively to

View File

@ -72,6 +72,12 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
--ignore-unmatch:: --ignore-unmatch::
Exit with a zero status even if no files matched. Exit with a zero status even if no files matched.
--sparse::
Allow updating index entries outside of the sparse-checkout cone.
Normally, `git rm` refuses to update index entries whose paths do
not fit within the sparse-checkout cone. See
linkgit:git-sparse-checkout[1] for more.
-q:: -q::
--quiet:: --quiet::
`git rm` normally outputs one line (in the form of an `rm` command) `git rm` normally outputs one line (in the form of an `rm` command)

View File

@ -224,15 +224,16 @@ void advise_on_updating_sparse_paths(struct string_list *pathspec_list)
if (!pathspec_list->nr) if (!pathspec_list->nr)
return; return;
fprintf(stderr, _("The following pathspecs didn't match any" fprintf(stderr, _("The following paths and/or pathspecs matched paths that exist\n"
" eligible path, but they do match index\n" "outside of your sparse-checkout definition, so will not be\n"
"entries outside the current sparse checkout:\n")); "updated in the index:\n"));
for_each_string_list_item(item, pathspec_list) for_each_string_list_item(item, pathspec_list)
fprintf(stderr, "%s\n", item->string); fprintf(stderr, "%s\n", item->string);
advise_if_enabled(ADVICE_UPDATE_SPARSE_PATH, advise_if_enabled(ADVICE_UPDATE_SPARSE_PATH,
_("Disable or modify the sparsity rules if you intend" _("If you intend to update such entries, try one of the following:\n"
" to update such entries.")); "* Use the --sparse option.\n"
"* Disable or modify the sparsity rules."));
} }
void detach_advice(const char *new_name) void detach_advice(const char *new_name)

View File

@ -30,6 +30,7 @@ static int patch_interactive, add_interactive, edit_interactive;
static int take_worktree_changes; static int take_worktree_changes;
static int add_renormalize; static int add_renormalize;
static int pathspec_file_nul; static int pathspec_file_nul;
static int include_sparse;
static const char *pathspec_from_file; static const char *pathspec_from_file;
static int legacy_stash_p; /* support for the scripted `git stash` */ static int legacy_stash_p; /* support for the scripted `git stash` */
@ -46,7 +47,9 @@ static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only)
struct cache_entry *ce = active_cache[i]; struct cache_entry *ce = active_cache[i];
int err; int err;
if (ce_skip_worktree(ce)) if (!include_sparse &&
(ce_skip_worktree(ce) ||
!path_in_sparse_checkout(ce->name, &the_index)))
continue; continue;
if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL)) if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL))
@ -94,6 +97,10 @@ static void update_callback(struct diff_queue_struct *q,
for (i = 0; i < q->nr; i++) { for (i = 0; i < q->nr; i++) {
struct diff_filepair *p = q->queue[i]; struct diff_filepair *p = q->queue[i];
const char *path = p->one->path; const char *path = p->one->path;
if (!include_sparse && !path_in_sparse_checkout(path, &the_index))
continue;
switch (fix_unmerged_status(p, data)) { switch (fix_unmerged_status(p, data)) {
default: default:
die(_("unexpected diff status %c"), p->status); die(_("unexpected diff status %c"), p->status);
@ -147,7 +154,9 @@ static int renormalize_tracked_files(const struct pathspec *pathspec, int flags)
for (i = 0; i < active_nr; i++) { for (i = 0; i < active_nr; i++) {
struct cache_entry *ce = active_cache[i]; struct cache_entry *ce = active_cache[i];
if (ce_skip_worktree(ce)) if (!include_sparse &&
(ce_skip_worktree(ce) ||
!path_in_sparse_checkout(ce->name, &the_index)))
continue; continue;
if (ce_stage(ce)) if (ce_stage(ce))
continue; /* do not touch unmerged paths */ continue; /* do not touch unmerged paths */
@ -377,6 +386,7 @@ static struct option builtin_add_options[] = {
OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")), OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")),
OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")), OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")),
OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")), OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")),
OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x", OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x",
N_("override the executable bit of the listed files")), N_("override the executable bit of the listed files")),
OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo, OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,
@ -442,6 +452,7 @@ static void check_embedded_repo(const char *path)
static int add_files(struct dir_struct *dir, int flags) static int add_files(struct dir_struct *dir, int flags)
{ {
int i, exit_status = 0; int i, exit_status = 0;
struct string_list matched_sparse_paths = STRING_LIST_INIT_NODUP;
if (dir->ignored_nr) { if (dir->ignored_nr) {
fprintf(stderr, _(ignore_error)); fprintf(stderr, _(ignore_error));
@ -455,6 +466,12 @@ static int add_files(struct dir_struct *dir, int flags)
} }
for (i = 0; i < dir->nr; i++) { for (i = 0; i < dir->nr; i++) {
if (!include_sparse &&
!path_in_sparse_checkout(dir->entries[i]->name, &the_index)) {
string_list_append(&matched_sparse_paths,
dir->entries[i]->name);
continue;
}
if (add_file_to_index(&the_index, dir->entries[i]->name, flags)) { if (add_file_to_index(&the_index, dir->entries[i]->name, flags)) {
if (!ignore_add_errors) if (!ignore_add_errors)
die(_("adding files failed")); die(_("adding files failed"));
@ -463,6 +480,14 @@ static int add_files(struct dir_struct *dir, int flags)
check_embedded_repo(dir->entries[i]->name); check_embedded_repo(dir->entries[i]->name);
} }
} }
if (matched_sparse_paths.nr) {
advise_on_updating_sparse_paths(&matched_sparse_paths);
exit_status = 1;
}
string_list_clear(&matched_sparse_paths, 0);
return exit_status; return exit_status;
} }
@ -627,7 +652,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)
if (seen[i]) if (seen[i])
continue; continue;
if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) { if (!include_sparse &&
matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) {
string_list_append(&only_match_skip_worktree, string_list_append(&only_match_skip_worktree,
pathspec.items[i].original); pathspec.items[i].original);
continue; continue;

View File

@ -118,21 +118,23 @@ static int index_range_of_same_dir(const char *src, int length,
int cmd_mv(int argc, const char **argv, const char *prefix) int cmd_mv(int argc, const char **argv, const char *prefix)
{ {
int i, flags, gitmodules_modified = 0; int i, flags, gitmodules_modified = 0;
int verbose = 0, show_only = 0, force = 0, ignore_errors = 0; int verbose = 0, show_only = 0, force = 0, ignore_errors = 0, ignore_sparse = 0;
struct option builtin_mv_options[] = { struct option builtin_mv_options[] = {
OPT__VERBOSE(&verbose, N_("be verbose")), OPT__VERBOSE(&verbose, N_("be verbose")),
OPT__DRY_RUN(&show_only, N_("dry run")), OPT__DRY_RUN(&show_only, N_("dry run")),
OPT__FORCE(&force, N_("force move/rename even if target exists"), OPT__FORCE(&force, N_("force move/rename even if target exists"),
PARSE_OPT_NOCOMPLETE), PARSE_OPT_NOCOMPLETE),
OPT_BOOL('k', NULL, &ignore_errors, N_("skip move/rename errors")), OPT_BOOL('k', NULL, &ignore_errors, N_("skip move/rename errors")),
OPT_BOOL(0, "sparse", &ignore_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
OPT_END(), OPT_END(),
}; };
const char **source, **destination, **dest_path, **submodule_gitfile; const char **source, **destination, **dest_path, **submodule_gitfile;
enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes; enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX, SPARSE } *modes;
struct stat st; struct stat st;
struct string_list src_for_dst = STRING_LIST_INIT_NODUP; struct string_list src_for_dst = STRING_LIST_INIT_NODUP;
struct lock_file lock_file = LOCK_INIT; struct lock_file lock_file = LOCK_INIT;
struct cache_entry *ce; struct cache_entry *ce;
struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
git_config(git_default_config, NULL); git_config(git_default_config, NULL);
@ -176,14 +178,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
const char *src = source[i], *dst = destination[i]; const char *src = source[i], *dst = destination[i];
int length, src_is_dir; int length, src_is_dir;
const char *bad = NULL; const char *bad = NULL;
int skip_sparse = 0;
if (show_only) if (show_only)
printf(_("Checking rename of '%s' to '%s'\n"), src, dst); printf(_("Checking rename of '%s' to '%s'\n"), src, dst);
length = strlen(src); length = strlen(src);
if (lstat(src, &st) < 0) if (lstat(src, &st) < 0) {
bad = _("bad source"); /* only error if existence is expected. */
else if (!strncmp(src, dst, length) && if (modes[i] != SPARSE)
bad = _("bad source");
} else if (!strncmp(src, dst, length) &&
(dst[length] == 0 || dst[length] == '/')) { (dst[length] == 0 || dst[length] == '/')) {
bad = _("can not move directory into itself"); bad = _("can not move directory into itself");
} else if ((src_is_dir = S_ISDIR(st.st_mode)) } else if ((src_is_dir = S_ISDIR(st.st_mode))
@ -212,11 +217,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
dst_len = strlen(dst); dst_len = strlen(dst);
for (j = 0; j < last - first; j++) { for (j = 0; j < last - first; j++) {
const char *path = active_cache[first + j]->name; const struct cache_entry *ce = active_cache[first + j];
const char *path = ce->name;
source[argc + j] = path; source[argc + j] = path;
destination[argc + j] = destination[argc + j] =
prefix_path(dst, dst_len, path + length + 1); prefix_path(dst, dst_len, path + length + 1);
modes[argc + j] = INDEX; modes[argc + j] = ce_skip_worktree(ce) ? SPARSE : INDEX;
submodule_gitfile[argc + j] = NULL; submodule_gitfile[argc + j] = NULL;
} }
argc += last - first; argc += last - first;
@ -244,14 +250,36 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
bad = _("multiple sources for the same target"); bad = _("multiple sources for the same target");
else if (is_dir_sep(dst[strlen(dst) - 1])) else if (is_dir_sep(dst[strlen(dst) - 1]))
bad = _("destination directory does not exist"); bad = _("destination directory does not exist");
else else {
/*
* We check if the paths are in the sparse-checkout
* definition as a very final check, since that
* allows us to point the user to the --sparse
* option as a way to have a successful run.
*/
if (!ignore_sparse &&
!path_in_sparse_checkout(src, &the_index)) {
string_list_append(&only_match_skip_worktree, src);
skip_sparse = 1;
}
if (!ignore_sparse &&
!path_in_sparse_checkout(dst, &the_index)) {
string_list_append(&only_match_skip_worktree, dst);
skip_sparse = 1;
}
if (skip_sparse)
goto remove_entry;
string_list_insert(&src_for_dst, dst); string_list_insert(&src_for_dst, dst);
}
if (!bad) if (!bad)
continue; continue;
if (!ignore_errors) if (!ignore_errors)
die(_("%s, source=%s, destination=%s"), die(_("%s, source=%s, destination=%s"),
bad, src, dst); bad, src, dst);
remove_entry:
if (--argc > 0) { if (--argc > 0) {
int n = argc - i; int n = argc - i;
memmove(source + i, source + i + 1, memmove(source + i, source + i + 1,
@ -266,6 +294,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
} }
} }
if (only_match_skip_worktree.nr) {
advise_on_updating_sparse_paths(&only_match_skip_worktree);
if (!ignore_errors)
return 1;
}
for (i = 0; i < argc; i++) { for (i = 0; i < argc; i++) {
const char *src = source[i], *dst = destination[i]; const char *src = source[i], *dst = destination[i];
enum update_mode mode = modes[i]; enum update_mode mode = modes[i];
@ -274,7 +308,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
printf(_("Renaming %s to %s\n"), src, dst); printf(_("Renaming %s to %s\n"), src, dst);
if (show_only) if (show_only)
continue; continue;
if (mode != INDEX && rename(src, dst) < 0) { if (mode != INDEX && mode != SPARSE && rename(src, dst) < 0) {
if (ignore_errors) if (ignore_errors)
continue; continue;
die_errno(_("renaming '%s' failed"), src); die_errno(_("renaming '%s' failed"), src);

View File

@ -237,6 +237,7 @@ static int check_local_mod(struct object_id *head, int index_only)
static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0; static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0;
static int ignore_unmatch = 0, pathspec_file_nul; static int ignore_unmatch = 0, pathspec_file_nul;
static int include_sparse;
static char *pathspec_from_file; static char *pathspec_from_file;
static struct option builtin_rm_options[] = { static struct option builtin_rm_options[] = {
@ -247,6 +248,7 @@ static struct option builtin_rm_options[] = {
OPT_BOOL('r', NULL, &recursive, N_("allow recursive removal")), OPT_BOOL('r', NULL, &recursive, N_("allow recursive removal")),
OPT_BOOL( 0 , "ignore-unmatch", &ignore_unmatch, OPT_BOOL( 0 , "ignore-unmatch", &ignore_unmatch,
N_("exit with a zero status even if nothing matched")), N_("exit with a zero status even if nothing matched")),
OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),
OPT_END(), OPT_END(),
@ -298,7 +300,10 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
ensure_full_index(&the_index); ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++) { for (i = 0; i < active_nr; i++) {
const struct cache_entry *ce = active_cache[i]; const struct cache_entry *ce = active_cache[i];
if (ce_skip_worktree(ce))
if (!include_sparse &&
(ce_skip_worktree(ce) ||
!path_in_sparse_checkout(ce->name, &the_index)))
continue; continue;
if (!ce_path_match(&the_index, ce, &pathspec, seen)) if (!ce_path_match(&the_index, ce, &pathspec, seen))
continue; continue;
@ -322,7 +327,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
seen_any = 1; seen_any = 1;
else if (ignore_unmatch) else if (ignore_unmatch)
continue; continue;
else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) else if (!include_sparse &&
matches_skip_worktree(&pathspec, i, &skip_worktree_seen))
string_list_append(&only_match_skip_worktree, original); string_list_append(&only_match_skip_worktree, original);
else else
die(_("pathspec '%s' did not match any files"), original); die(_("pathspec '%s' did not match any files"), original);

56
dir.c
View File

@ -1294,7 +1294,7 @@ int match_pathname(const char *pathname, int pathlen,
* then our prefix match is all we need; we * then our prefix match is all we need; we
* do not need to call fnmatch at all. * do not need to call fnmatch at all.
*/ */
if (!patternlen && !namelen) if (!patternlen && (!namelen || (flags & PATTERN_FLAG_MUSTBEDIR)))
return 1; return 1;
} }
@ -1303,6 +1303,44 @@ int match_pathname(const char *pathname, int pathlen,
WM_PATHNAME) == 0; WM_PATHNAME) == 0;
} }
static int path_matches_dir_pattern(const char *pathname,
int pathlen,
struct strbuf **path_parent,
int *dtype,
struct path_pattern *pattern,
struct index_state *istate)
{
if (!*path_parent) {
char *slash;
CALLOC_ARRAY(*path_parent, 1);
strbuf_add(*path_parent, pathname, pathlen);
slash = find_last_dir_sep((*path_parent)->buf);
if (slash)
strbuf_setlen(*path_parent, slash - (*path_parent)->buf);
else
strbuf_setlen(*path_parent, 0);
}
/*
* If the parent directory matches the pattern, then we do not
* need to check for dtype.
*/
if ((*path_parent)->len &&
match_pathname((*path_parent)->buf, (*path_parent)->len,
pattern->base,
pattern->baselen ? pattern->baselen - 1 : 0,
pattern->pattern, pattern->nowildcardlen,
pattern->patternlen, pattern->flags))
return 1;
*dtype = resolve_dtype(*dtype, istate, pathname, pathlen);
if (*dtype != DT_DIR)
return 0;
return 1;
}
/* /*
* Scan the given exclude list in reverse to see whether pathname * Scan the given exclude list in reverse to see whether pathname
* should be ignored. The first match (i.e. the last on the list), if * should be ignored. The first match (i.e. the last on the list), if
@ -1318,6 +1356,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname
{ {
struct path_pattern *res = NULL; /* undecided */ struct path_pattern *res = NULL; /* undecided */
int i; int i;
struct strbuf *path_parent = NULL;
if (!pl->nr) if (!pl->nr)
return NULL; /* undefined */ return NULL; /* undefined */
@ -1327,11 +1366,10 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname
const char *exclude = pattern->pattern; const char *exclude = pattern->pattern;
int prefix = pattern->nowildcardlen; int prefix = pattern->nowildcardlen;
if (pattern->flags & PATTERN_FLAG_MUSTBEDIR) { if (pattern->flags & PATTERN_FLAG_MUSTBEDIR &&
*dtype = resolve_dtype(*dtype, istate, pathname, pathlen); !path_matches_dir_pattern(pathname, pathlen, &path_parent,
if (*dtype != DT_DIR) dtype, pattern, istate))
continue; continue;
}
if (pattern->flags & PATTERN_FLAG_NODIR) { if (pattern->flags & PATTERN_FLAG_NODIR) {
if (match_basename(basename, if (match_basename(basename,
@ -1355,6 +1393,12 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname
break; break;
} }
} }
if (path_parent) {
strbuf_release(path_parent);
free(path_parent);
}
return res; return res;
} }

View File

@ -39,7 +39,8 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec,
return; return;
for (i = 0; i < istate->cache_nr; i++) { for (i = 0; i < istate->cache_nr; i++) {
const struct cache_entry *ce = istate->cache[i]; const struct cache_entry *ce = istate->cache[i];
if (sw_action == PS_IGNORE_SKIP_WORKTREE && ce_skip_worktree(ce)) if (sw_action == PS_IGNORE_SKIP_WORKTREE &&
(ce_skip_worktree(ce) || !path_in_sparse_checkout(ce->name, istate)))
continue; continue;
ce_path_match(istate, ce, pathspec, seen); ce_path_match(istate, ce, pathspec, seen);
} }
@ -70,7 +71,7 @@ char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)
for (i = 0; i < istate->cache_nr; i++) { for (i = 0; i < istate->cache_nr; i++) {
struct cache_entry *ce = istate->cache[i]; struct cache_entry *ce = istate->cache[i];
if (ce_skip_worktree(ce)) if (ce_skip_worktree(ce) || !path_in_sparse_checkout(ce->name, istate))
ce_path_match(istate, ce, pathspec, seen); ce_path_match(istate, ce, pathspec, seen);
} }

View File

@ -411,7 +411,7 @@ test_expect_success 'sparse-checkout (init|set|disable) warns with unmerged stat
git -C unmerged sparse-checkout disable git -C unmerged sparse-checkout disable
' '
test_expect_success 'sparse-checkout reapply' ' test_expect_failure 'sparse-checkout reapply' '
git clone repo tweak && git clone repo tweak &&
echo dirty >tweak/deep/deeper2/a && echo dirty >tweak/deep/deeper2/a &&
@ -443,6 +443,8 @@ test_expect_success 'sparse-checkout reapply' '
test_i18ngrep "warning.*The following paths are unmerged" err && test_i18ngrep "warning.*The following paths are unmerged" err &&
test_path_is_file tweak/folder1/a && test_path_is_file tweak/folder1/a &&
# NEEDSWORK: We are asking to update a file outside of the
# sparse-checkout cone, but this is no longer allowed.
git -C tweak add folder1/a && git -C tweak add folder1/a &&
git -C tweak sparse-checkout reapply 2>err && git -C tweak sparse-checkout reapply 2>err &&
test_must_be_empty err && test_must_be_empty err &&

View File

@ -187,6 +187,16 @@ test_sparse_match () {
test_cmp sparse-checkout-err sparse-index-err test_cmp sparse-checkout-err sparse-index-err
} }
test_sparse_unstaged () {
file=$1 &&
for repo in sparse-checkout sparse-index
do
# Skip "unmerged" paths
git -C $repo diff --staged --diff-filter=u -- "$file" >diff &&
test_must_be_empty diff || return 1
done
}
test_expect_success 'sparse-index contents' ' test_expect_success 'sparse-index contents' '
init_repos && init_repos &&
@ -291,6 +301,20 @@ test_expect_success 'add, commit, checkout' '
test_all_match git checkout - test_all_match git checkout -
' '
test_expect_success 'add outside sparse cone' '
init_repos &&
run_on_sparse mkdir folder1 &&
run_on_sparse ../edit-contents folder1/a &&
run_on_sparse ../edit-contents folder1/newfile &&
test_sparse_match test_must_fail git add folder1/a &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
test_sparse_match test_must_fail git add folder1/newfile &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/newfile
'
test_expect_success 'commit including unstaged changes' ' test_expect_success 'commit including unstaged changes' '
init_repos && init_repos &&
@ -339,18 +363,24 @@ test_expect_success 'status/add: outside sparse cone' '
# Adding the path outside of the sparse-checkout cone should fail. # Adding the path outside of the sparse-checkout cone should fail.
test_sparse_match test_must_fail git add folder1/a && test_sparse_match test_must_fail git add folder1/a &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
test_sparse_match test_must_fail git add --refresh folder1/a && test_sparse_match test_must_fail git add --refresh folder1/a &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
test_sparse_match test_must_fail git add folder1/new &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/new &&
test_sparse_match git add --sparse folder1/a &&
test_sparse_match git add --sparse folder1/new &&
# NEEDSWORK: Adding a newly-tracked file outside the cone succeeds test_all_match git add --sparse . &&
test_sparse_match git add folder1/new &&
test_all_match git add . &&
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
test_all_match git commit -m folder1/new && test_all_match git commit -m folder1/new &&
test_all_match git rev-parse HEAD^{tree} && test_all_match git rev-parse HEAD^{tree} &&
run_on_all ../edit-contents folder1/newer && run_on_all ../edit-contents folder1/newer &&
test_all_match git add folder1/ && test_all_match git add --sparse folder1/ &&
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
test_all_match git commit -m folder1/newer && test_all_match git commit -m folder1/newer &&
test_all_match git rev-parse HEAD^{tree} test_all_match git rev-parse HEAD^{tree}
@ -494,11 +524,6 @@ test_expect_success 'merge, cherry-pick, and rebase' '
done done
' '
# NEEDSWORK: This test is documenting current behavior, but that
# behavior can be confusing to users so there is desire to change it.
# Right now, users might be using this flow to work through conflicts,
# so any solution should present advice to users who try this sequence
# of commands to follow whatever new method we create.
test_expect_success 'merge with conflict outside cone' ' test_expect_success 'merge with conflict outside cone' '
init_repos && init_repos &&
@ -513,13 +538,19 @@ test_expect_success 'merge with conflict outside cone' '
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
# 2. Add the file with conflict markers # 2. Add the file with conflict markers
test_all_match git add folder1/a && test_sparse_match test_must_fail git add folder1/a &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
test_all_match git add --sparse folder1/a &&
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
# 3. Rename the file to another sparse filename and # 3. Rename the file to another sparse filename and
# accept conflict markers as resolved content. # accept conflict markers as resolved content.
run_on_all mv folder2/a folder2/z && run_on_all mv folder2/a folder2/z &&
test_all_match git add folder2 && test_sparse_match test_must_fail git add folder2 &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder2/z &&
test_all_match git add --sparse folder2 &&
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
test_all_match git merge --continue && test_all_match git merge --continue &&
@ -544,13 +575,25 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
# 2. Add the file with conflict markers # 2. Add the file with conflict markers
test_all_match git add folder1/a && # NEEDSWORK: Even though the merge conflict removed the
# SKIP_WORKTREE bit from the index entry for folder1/a, we should
# warn that this is a problematic add.
test_sparse_match test_must_fail git add folder1/a &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
test_all_match git add --sparse folder1/a &&
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
# 3. Rename the file to another sparse filename and # 3. Rename the file to another sparse filename and
# accept conflict markers as resolved content. # accept conflict markers as resolved content.
# NEEDSWORK: This mode now fails, because folder2/z is
# outside of the sparse-checkout cone and does not match an
# existing index entry with the SKIP_WORKTREE bit cleared.
run_on_all mv folder2/a folder2/z && run_on_all mv folder2/a folder2/z &&
test_all_match git add folder2 && test_sparse_match test_must_fail git add folder2 &&
grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder2/z &&
test_all_match git add --sparse folder2 &&
test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 &&
test_all_match git $OPERATION --continue && test_all_match git $OPERATION --continue &&
@ -626,6 +669,7 @@ test_expect_success 'clean' '
test_expect_success 'submodule handling' ' test_expect_success 'submodule handling' '
init_repos && init_repos &&
test_sparse_match git sparse-checkout add modules &&
test_all_match mkdir modules && test_all_match mkdir modules &&
test_all_match touch modules/a && test_all_match touch modules/a &&
test_all_match git add modules && test_all_match git add modules &&
@ -635,6 +679,7 @@ test_expect_success 'submodule handling' '
test_all_match git commit -m "add submodule" && test_all_match git commit -m "add submodule" &&
# having a submodule prevents "modules" from collapse # having a submodule prevents "modules" from collapse
test_sparse_match git sparse-checkout set deep/deeper1 &&
test-tool -C sparse-index read-cache --table >cache && test-tool -C sparse-index read-cache --table >cache &&
grep "100644 blob .* modules/a" cache && grep "100644 blob .* modules/a" cache &&
grep "160000 commit $(git -C initial-repo rev-parse HEAD) modules/sub" cache grep "160000 commit $(git -C initial-repo rev-parse HEAD) modules/sub" cache

View File

@ -11,12 +11,15 @@ test_expect_success 'setup' "
git commit -m files && git commit -m files &&
cat >sparse_error_header <<-EOF && cat >sparse_error_header <<-EOF &&
The following pathspecs didn't match any eligible path, but they do match index The following paths and/or pathspecs matched paths that exist
entries outside the current sparse checkout: outside of your sparse-checkout definition, so will not be
updated in the index:
EOF EOF
cat >sparse_hint <<-EOF && cat >sparse_hint <<-EOF &&
hint: Disable or modify the sparsity rules if you intend to update such entries. hint: If you intend to update such entries, try one of the following:
hint: * Use the --sparse option.
hint: * Disable or modify the sparsity rules.
hint: Disable this message with \"git config advice.updateSparsePath false\" hint: Disable this message with \"git config advice.updateSparsePath false\"
EOF EOF
@ -37,9 +40,25 @@ done
test_expect_success 'recursive rm does not remove sparse entries' ' test_expect_success 'recursive rm does not remove sparse entries' '
git reset --hard && git reset --hard &&
git sparse-checkout set sub/dir && git sparse-checkout set sub/dir &&
git rm -r sub && test_must_fail git rm -r sub &&
git rm --sparse -r sub &&
git status --porcelain -uno >actual && git status --porcelain -uno >actual &&
echo "D sub/dir/e" >expected && cat >expected <<-\EOF &&
D sub/d
D sub/dir/e
EOF
test_cmp expected actual
'
test_expect_success 'recursive rm --sparse removes sparse entries' '
git reset --hard &&
git sparse-checkout set "sub/dir" &&
git rm --sparse -r sub &&
git status --porcelain -uno >actual &&
cat >expected <<-\EOF &&
D sub/d
D sub/dir/e
EOF
test_cmp expected actual test_cmp expected actual
' '
@ -75,4 +94,15 @@ test_expect_success 'do not warn about sparse entries with --ignore-unmatch' '
git ls-files --error-unmatch b git ls-files --error-unmatch b
' '
test_expect_success 'refuse to rm a non-skip-worktree path outside sparse cone' '
git reset --hard &&
git sparse-checkout set a &&
git update-index --no-skip-worktree b &&
test_must_fail git rm b 2>stderr &&
test_cmp b_error_and_hint stderr &&
git rm --sparse b 2>stderr &&
test_must_be_empty stderr &&
test_path_is_missing b
'
test_done test_done

View File

@ -19,6 +19,7 @@ setup_sparse_entry () {
fi && fi &&
git add sparse_entry && git add sparse_entry &&
git update-index --skip-worktree sparse_entry && git update-index --skip-worktree sparse_entry &&
git commit --allow-empty -m "ensure sparse_entry exists at HEAD" &&
SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry) SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry)
} }
@ -36,14 +37,22 @@ setup_gitignore () {
EOF EOF
} }
test_sparse_entry_unstaged () {
git diff --staged -- sparse_entry >diff &&
test_must_be_empty diff
}
test_expect_success 'setup' " test_expect_success 'setup' "
cat >sparse_error_header <<-EOF && cat >sparse_error_header <<-EOF &&
The following pathspecs didn't match any eligible path, but they do match index The following paths and/or pathspecs matched paths that exist
entries outside the current sparse checkout: outside of your sparse-checkout definition, so will not be
updated in the index:
EOF EOF
cat >sparse_hint <<-EOF && cat >sparse_hint <<-EOF &&
hint: Disable or modify the sparsity rules if you intend to update such entries. hint: If you intend to update such entries, try one of the following:
hint: * Use the --sparse option.
hint: * Disable or modify the sparsity rules.
hint: Disable this message with \"git config advice.updateSparsePath false\" hint: Disable this message with \"git config advice.updateSparsePath false\"
EOF EOF
@ -55,6 +64,7 @@ test_expect_success 'git add does not remove sparse entries' '
setup_sparse_entry && setup_sparse_entry &&
rm sparse_entry && rm sparse_entry &&
test_must_fail git add sparse_entry 2>stderr && test_must_fail git add sparse_entry 2>stderr &&
test_sparse_entry_unstaged &&
test_cmp error_and_hint stderr && test_cmp error_and_hint stderr &&
test_sparse_entry_unchanged test_sparse_entry_unchanged
' '
@ -73,6 +83,7 @@ test_expect_success 'git add . does not remove sparse entries' '
rm sparse_entry && rm sparse_entry &&
setup_gitignore && setup_gitignore &&
test_must_fail git add . 2>stderr && test_must_fail git add . 2>stderr &&
test_sparse_entry_unstaged &&
cat sparse_error_header >expect && cat sparse_error_header >expect &&
echo . >>expect && echo . >>expect &&
@ -88,6 +99,7 @@ do
setup_sparse_entry && setup_sparse_entry &&
echo modified >sparse_entry && echo modified >sparse_entry &&
test_must_fail git add $opt sparse_entry 2>stderr && test_must_fail git add $opt sparse_entry 2>stderr &&
test_sparse_entry_unstaged &&
test_cmp error_and_hint stderr && test_cmp error_and_hint stderr &&
test_sparse_entry_unchanged test_sparse_entry_unchanged
' '
@ -98,6 +110,7 @@ test_expect_success 'git add --refresh does not update sparse entries' '
git ls-files --debug sparse_entry | grep mtime >before && git ls-files --debug sparse_entry | grep mtime >before &&
test-tool chmtime -60 sparse_entry && test-tool chmtime -60 sparse_entry &&
test_must_fail git add --refresh sparse_entry 2>stderr && test_must_fail git add --refresh sparse_entry 2>stderr &&
test_sparse_entry_unstaged &&
test_cmp error_and_hint stderr && test_cmp error_and_hint stderr &&
git ls-files --debug sparse_entry | grep mtime >after && git ls-files --debug sparse_entry | grep mtime >after &&
test_cmp before after test_cmp before after
@ -106,6 +119,7 @@ test_expect_success 'git add --refresh does not update sparse entries' '
test_expect_success 'git add --chmod does not update sparse entries' ' test_expect_success 'git add --chmod does not update sparse entries' '
setup_sparse_entry && setup_sparse_entry &&
test_must_fail git add --chmod=+x sparse_entry 2>stderr && test_must_fail git add --chmod=+x sparse_entry 2>stderr &&
test_sparse_entry_unstaged &&
test_cmp error_and_hint stderr && test_cmp error_and_hint stderr &&
test_sparse_entry_unchanged && test_sparse_entry_unchanged &&
! test -x sparse_entry ! test -x sparse_entry
@ -116,6 +130,7 @@ test_expect_success 'git add --renormalize does not update sparse entries' '
setup_sparse_entry "LINEONE\r\nLINETWO\r\n" && setup_sparse_entry "LINEONE\r\nLINETWO\r\n" &&
echo "sparse_entry text=auto" >.gitattributes && echo "sparse_entry text=auto" >.gitattributes &&
test_must_fail git add --renormalize sparse_entry 2>stderr && test_must_fail git add --renormalize sparse_entry 2>stderr &&
test_sparse_entry_unstaged &&
test_cmp error_and_hint stderr && test_cmp error_and_hint stderr &&
test_sparse_entry_unchanged test_sparse_entry_unchanged
' '
@ -124,6 +139,7 @@ test_expect_success 'git add --dry-run --ignore-missing warn on sparse path' '
setup_sparse_entry && setup_sparse_entry &&
rm sparse_entry && rm sparse_entry &&
test_must_fail git add --dry-run --ignore-missing sparse_entry 2>stderr && test_must_fail git add --dry-run --ignore-missing sparse_entry 2>stderr &&
test_sparse_entry_unstaged &&
test_cmp error_and_hint stderr && test_cmp error_and_hint stderr &&
test_sparse_entry_unchanged test_sparse_entry_unchanged
' '
@ -145,11 +161,57 @@ test_expect_success 'do not warn when pathspec matches dense entries' '
git ls-files --error-unmatch dense_entry git ls-files --error-unmatch dense_entry
' '
test_expect_success 'git add fails outside of sparse-checkout definition' '
test_when_finished git sparse-checkout disable &&
test_commit a &&
git sparse-checkout init &&
git sparse-checkout set a &&
echo >>sparse_entry &&
git update-index --no-skip-worktree sparse_entry &&
test_must_fail git add sparse_entry &&
test_sparse_entry_unstaged &&
test_must_fail git add --chmod=+x sparse_entry &&
test_sparse_entry_unstaged &&
test_must_fail git add --renormalize sparse_entry &&
test_sparse_entry_unstaged &&
# Avoid munging CRLFs to avoid an error message
git -c core.autocrlf=input add --sparse sparse_entry 2>stderr &&
test_must_be_empty stderr &&
test-tool read-cache --table >actual &&
grep "^100644 blob.*sparse_entry\$" actual &&
git add --sparse --chmod=+x sparse_entry 2>stderr &&
test_must_be_empty stderr &&
test-tool read-cache --table >actual &&
grep "^100755 blob.*sparse_entry\$" actual &&
git reset &&
# This will print a message over stderr on Windows.
git add --sparse --renormalize sparse_entry &&
git status --porcelain >actual &&
grep "^M sparse_entry\$" actual
'
test_expect_success 'add obeys advice.updateSparsePath' ' test_expect_success 'add obeys advice.updateSparsePath' '
setup_sparse_entry && setup_sparse_entry &&
test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
test_sparse_entry_unstaged &&
test_cmp sparse_entry_error stderr test_cmp sparse_entry_error stderr
' '
test_expect_success 'add allows sparse entries with --sparse' '
git sparse-checkout set a &&
echo modified >sparse_entry &&
test_must_fail git add sparse_entry &&
test_sparse_entry_unchanged &&
git add --sparse sparse_entry 2>stderr &&
test_must_be_empty stderr
'
test_done test_done

189
t/t7002-mv-sparse-checkout.sh Executable file
View File

@ -0,0 +1,189 @@
#!/bin/sh
test_description='git mv in sparse working trees'
. ./test-lib.sh
test_expect_success 'setup' "
mkdir -p sub/dir sub/dir2 &&
touch a b c sub/d sub/dir/e sub/dir2/e &&
git add -A &&
git commit -m files &&
cat >sparse_error_header <<-EOF &&
The following paths and/or pathspecs matched paths that exist
outside of your sparse-checkout definition, so will not be
updated in the index:
EOF
cat >sparse_hint <<-EOF
hint: If you intend to update such entries, try one of the following:
hint: * Use the --sparse option.
hint: * Disable or modify the sparsity rules.
hint: Disable this message with \"git config advice.updateSparsePath false\"
EOF
"
test_expect_success 'mv refuses to move sparse-to-sparse' '
test_when_finished rm -f e &&
git reset --hard &&
git sparse-checkout set a &&
touch b &&
test_must_fail git mv b e 2>stderr &&
cat sparse_error_header >expect &&
echo b >>expect &&
echo e >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse b e 2>stderr &&
test_must_be_empty stderr
'
test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' '
test_when_finished rm -f b c e &&
git reset --hard &&
git sparse-checkout set a &&
# tracked-to-untracked
touch b &&
git mv -k b e 2>stderr &&
test_path_exists b &&
test_path_is_missing e &&
cat sparse_error_header >expect &&
echo b >>expect &&
echo e >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse b e 2>stderr &&
test_must_be_empty stderr &&
test_path_is_missing b &&
test_path_exists e &&
# tracked-to-tracked
git reset --hard &&
touch b &&
git mv -k b c 2>stderr &&
test_path_exists b &&
test_path_is_missing c &&
cat sparse_error_header >expect &&
echo b >>expect &&
echo c >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse b c 2>stderr &&
test_must_be_empty stderr &&
test_path_is_missing b &&
test_path_exists c
'
test_expect_success 'mv refuses to move non-sparse-to-sparse' '
test_when_finished rm -f b c e &&
git reset --hard &&
git sparse-checkout set a &&
# tracked-to-untracked
test_must_fail git mv a e 2>stderr &&
test_path_exists a &&
test_path_is_missing e &&
cat sparse_error_header >expect &&
echo e >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse a e 2>stderr &&
test_must_be_empty stderr &&
test_path_is_missing a &&
test_path_exists e &&
# tracked-to-tracked
rm e &&
git reset --hard &&
test_must_fail git mv a c 2>stderr &&
test_path_exists a &&
test_path_is_missing c &&
cat sparse_error_header >expect &&
echo c >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse a c 2>stderr &&
test_must_be_empty stderr &&
test_path_is_missing a &&
test_path_exists c
'
test_expect_success 'mv refuses to move sparse-to-non-sparse' '
test_when_finished rm -f b c e &&
git reset --hard &&
git sparse-checkout set a e &&
# tracked-to-untracked
touch b &&
test_must_fail git mv b e 2>stderr &&
cat sparse_error_header >expect &&
echo b >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse b e 2>stderr &&
test_must_be_empty stderr
'
test_expect_success 'recursive mv refuses to move (possible) sparse' '
test_when_finished rm -rf b c e sub2 &&
git reset --hard &&
# Without cone mode, "sub" and "sub2" do not match
git sparse-checkout set sub/dir sub2/dir &&
# Add contained contents to ensure we avoid non-existence errors
mkdir sub/dir2 &&
touch sub/d sub/dir2/e &&
test_must_fail git mv sub sub2 2>stderr &&
cat sparse_error_header >expect &&
cat >>expect <<-\EOF &&
sub/d
sub2/d
sub/dir/e
sub2/dir/e
sub/dir2/e
sub2/dir2/e
EOF
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse sub sub2 2>stderr &&
test_must_be_empty stderr &&
git commit -m "moved sub to sub2" &&
git rev-parse HEAD~1:sub >expect &&
git rev-parse HEAD:sub2 >actual &&
test_cmp expect actual &&
git reset --hard HEAD~1
'
test_expect_success 'recursive mv refuses to move sparse' '
git reset --hard &&
# Use cone mode so "sub/" matches the sparse-checkout patterns
git sparse-checkout init --cone &&
git sparse-checkout set sub/dir sub2/dir &&
# Add contained contents to ensure we avoid non-existence errors
mkdir sub/dir2 &&
touch sub/dir2/e &&
test_must_fail git mv sub sub2 2>stderr &&
cat sparse_error_header >expect &&
cat >>expect <<-\EOF &&
sub/dir2/e
sub2/dir2/e
EOF
cat sparse_hint >>expect &&
test_cmp expect stderr &&
git mv --sparse sub sub2 2>stderr &&
test_must_be_empty stderr &&
git commit -m "moved sub to sub2" &&
git rev-parse HEAD~1:sub >expect &&
git rev-parse HEAD:sub2 >actual &&
test_cmp expect actual &&
git reset --hard HEAD~1
'
test_done