Merge branch 'en/removing-untracked-fixes'

Various fixes in code paths that move untracked files away to make room.

* en/removing-untracked-fixes:
  Documentation: call out commands that nuke untracked files/directories
  Comment important codepaths regarding nuking untracked files/dirs
  unpack-trees: avoid nuking untracked dir in way of locally deleted file
  unpack-trees: avoid nuking untracked dir in way of unmerged file
  Change unpack_trees' 'reset' flag into an enum
  Remove ignored files by default when they are in the way
  unpack-trees: make dir an internal-only struct
  unpack-trees: introduce preserve_ignored to unpack_trees_options
  read-tree, merge-recursive: overwrite ignored files by default
  checkout, read-tree: fix leak of unpack_trees_options.dir
  t2500: add various tests for nuking untracked files
This commit is contained in:
Junio C Hamano 2021-10-13 15:15:57 -07:00
commit a7c2daa06d
23 changed files with 366 additions and 74 deletions

View File

@ -118,8 +118,9 @@ OPTIONS
-f:: -f::
--force:: --force::
When switching branches, proceed even if the index or the When switching branches, proceed even if the index or the
working tree differs from `HEAD`. This is used to throw away working tree differs from `HEAD`, and even if there are untracked
local changes. files in the way. This is used to throw away local changes and
any untracked files or directories that are in the way.
+ +
When checking out paths from the index, do not fail upon unmerged When checking out paths from the index, do not fail upon unmerged
entries; instead, unmerged entries are ignored. entries; instead, unmerged entries are ignored.

View File

@ -10,8 +10,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git read-tree' [[-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>] 'git read-tree' [[-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>]
[-u [--exclude-per-directory=<gitignore>] | -i]] [-u | -i]] [--index-output=<file>] [--no-sparse-checkout]
[--index-output=<file>] [--no-sparse-checkout]
(--empty | <tree-ish1> [<tree-ish2> [<tree-ish3>]]) (--empty | <tree-ish1> [<tree-ish2> [<tree-ish3>]])
@ -39,8 +38,9 @@ OPTIONS
--reset:: --reset::
Same as -m, except that unmerged entries are discarded instead Same as -m, except that unmerged entries are discarded instead
of failing. When used with `-u`, updates leading to loss of of failing. When used with `-u`, updates leading to loss of
working tree changes will not abort the operation. working tree changes or untracked files or directories will not
abort the operation.
-u:: -u::
After a successful merge, update the files in the work After a successful merge, update the files in the work
@ -88,21 +88,6 @@ OPTIONS
The command will refuse to overwrite entries that already The command will refuse to overwrite entries that already
existed in the original index file. existed in the original index file.
--exclude-per-directory=<gitignore>::
When running the command with `-u` and `-m` options, the
merge result may need to overwrite paths that are not
tracked in the current branch. The command usually
refuses to proceed with the merge to avoid losing such a
path. However this safety valve sometimes gets in the
way. For example, it often happens that the other
branch added a file that used to be a generated file in
your branch, and the safety valve triggers when you try
to switch to that branch after you ran `make` but before
running `make clean` to remove the generated file. This
option tells the command to read per-directory exclude
file (usually '.gitignore') and allows such an untracked
but explicitly ignored file to be overwritten.
--index-output=<file>:: --index-output=<file>::
Instead of writing the results out to `$GIT_INDEX_FILE`, Instead of writing the results out to `$GIT_INDEX_FILE`,
write the resulting index in the named file. While the write the resulting index in the named file. While the

View File

@ -69,7 +69,8 @@ linkgit:git-add[1]).
--hard:: --hard::
Resets the index and working tree. Any changes to tracked files in the Resets the index and working tree. Any changes to tracked files in the
working tree since `<commit>` are discarded. working tree since `<commit>` are discarded. Any untracked files or
directories in the way of writing any tracked files are simply deleted.
--merge:: --merge::
Resets the index and updates the files in the working tree that are Resets the index and updates the files in the working tree that are

View File

@ -1917,7 +1917,8 @@ static int fast_forward_to(struct tree *head, struct tree *remote, int reset)
opts.dst_index = &the_index; opts.dst_index = &the_index;
opts.update = 1; opts.update = 1;
opts.merge = 1; opts.merge = 1;
opts.reset = reset; opts.reset = reset ? UNPACK_RESET_PROTECT_UNTRACKED : 0;
opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
opts.fn = twoway_merge; opts.fn = twoway_merge;
init_tree_desc(&t[0], head->buffer, head->size); init_tree_desc(&t[0], head->buffer, head->size);
init_tree_desc(&t[1], remote->buffer, remote->size); init_tree_desc(&t[1], remote->buffer, remote->size);

View File

@ -646,7 +646,9 @@ static int reset_tree(struct tree *tree, const struct checkout_opts *o,
opts.head_idx = -1; opts.head_idx = -1;
opts.update = worktree; opts.update = worktree;
opts.skip_unmerged = !worktree; opts.skip_unmerged = !worktree;
opts.reset = 1; opts.reset = o->force ? UNPACK_RESET_OVERWRITE_UNTRACKED :
UNPACK_RESET_PROTECT_UNTRACKED;
opts.preserve_ignored = (!o->force && !o->overwrite_ignore);
opts.merge = 1; opts.merge = 1;
opts.fn = oneway_merge; opts.fn = oneway_merge;
opts.verbose_update = o->show_progress; opts.verbose_update = o->show_progress;
@ -746,11 +748,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
new_branch_info->commit ? new_branch_info->commit ?
&new_branch_info->commit->object.oid : &new_branch_info->commit->object.oid :
&new_branch_info->oid, NULL); &new_branch_info->oid, NULL);
if (opts->overwrite_ignore) { topts.preserve_ignored = !opts->overwrite_ignore;
topts.dir = xcalloc(1, sizeof(*topts.dir));
topts.dir->flags |= DIR_SHOW_IGNORED;
setup_standard_excludes(topts.dir);
}
tree = parse_tree_indirect(old_branch_info->commit ? tree = parse_tree_indirect(old_branch_info->commit ?
&old_branch_info->commit->object.oid : &old_branch_info->commit->object.oid :
the_hash_algo->empty_tree); the_hash_algo->empty_tree);

View File

@ -687,6 +687,7 @@ static int checkout(int submodule_progress)
opts.update = 1; opts.update = 1;
opts.merge = 1; opts.merge = 1;
opts.clone = 1; opts.clone = 1;
opts.preserve_ignored = 0;
opts.fn = oneway_merge; opts.fn = oneway_merge;
opts.verbose_update = (option_verbosity >= 0); opts.verbose_update = (option_verbosity >= 0);
opts.src_index = &the_index; opts.src_index = &the_index;

View File

@ -680,6 +680,7 @@ static int read_tree_trivial(struct object_id *common, struct object_id *head,
opts.verbose_update = 1; opts.verbose_update = 1;
opts.trivial_merges_only = 1; opts.trivial_merges_only = 1;
opts.merge = 1; opts.merge = 1;
opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
trees[nr_trees] = parse_tree_indirect(common); trees[nr_trees] = parse_tree_indirect(common);
if (!trees[nr_trees++]) if (!trees[nr_trees++])
return -1; return -1;

View File

@ -38,7 +38,7 @@ static int list_tree(struct object_id *oid)
} }
static const char * const read_tree_usage[] = { static const char * const read_tree_usage[] = {
N_("git read-tree [(-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>) [-u [--exclude-per-directory=<gitignore>] | -i]] [--no-sparse-checkout] [--index-output=<file>] (--empty | <tree-ish1> [<tree-ish2> [<tree-ish3>]])"), N_("git read-tree [(-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>) [-u | -i]] [--no-sparse-checkout] [--index-output=<file>] (--empty | <tree-ish1> [<tree-ish2> [<tree-ish3>]])"),
NULL NULL
}; };
@ -53,24 +53,16 @@ static int index_output_cb(const struct option *opt, const char *arg,
static int exclude_per_directory_cb(const struct option *opt, const char *arg, static int exclude_per_directory_cb(const struct option *opt, const char *arg,
int unset) int unset)
{ {
struct dir_struct *dir;
struct unpack_trees_options *opts; struct unpack_trees_options *opts;
BUG_ON_OPT_NEG(unset); BUG_ON_OPT_NEG(unset);
opts = (struct unpack_trees_options *)opt->value; opts = (struct unpack_trees_options *)opt->value;
if (opts->dir) if (!opts->update)
die("more than one --exclude-per-directory given."); die("--exclude-per-directory is meaningless unless -u");
if (strcmp(arg, ".gitignore"))
dir = xcalloc(1, sizeof(*opts->dir)); die("--exclude-per-directory argument must be .gitignore");
dir->flags |= DIR_SHOW_IGNORED;
dir->exclude_per_dir = arg;
opts->dir = dir;
/* We do not need to nor want to do read-directory
* here; we are merely interested in reusing the
* per directory ignore stack mechanism.
*/
return 0; return 0;
} }
@ -174,6 +166,9 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix)
if (1 < opts.merge + opts.reset + prefix_set) if (1 < opts.merge + opts.reset + prefix_set)
die("Which one? -m, --reset, or --prefix?"); die("Which one? -m, --reset, or --prefix?");
if (opts.reset)
opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
/* /*
* NEEDSWORK * NEEDSWORK
* *
@ -209,8 +204,9 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix)
if ((opts.update || opts.index_only) && !opts.merge) if ((opts.update || opts.index_only) && !opts.merge)
die("%s is meaningless without -m, --reset, or --prefix", die("%s is meaningless without -m, --reset, or --prefix",
opts.update ? "-u" : "-i"); opts.update ? "-u" : "-i");
if ((opts.dir && !opts.update)) if (opts.update && !opts.reset)
die("--exclude-per-directory is meaningless unless -u"); opts.preserve_ignored = 0;
/* otherwise, opts.preserve_ignored is irrelevant */
if (opts.merge && !opts.index_only) if (opts.merge && !opts.index_only)
setup_work_tree(); setup_work_tree();

View File

@ -67,12 +67,18 @@ static int reset_index(const char *ref, const struct object_id *oid, int reset_t
case KEEP: case KEEP:
case MERGE: case MERGE:
opts.update = 1; opts.update = 1;
opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
break; break;
case HARD: case HARD:
opts.update = 1; opts.update = 1;
/* fallthrough */ opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
break;
case MIXED:
opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
/* but opts.update=0, so working tree not updated */
break;
default: default:
opts.reset = 1; BUG("invalid reset_type passed to reset_index");
} }
read_cache_unmerged(); read_cache_unmerged();

View File

@ -256,8 +256,10 @@ static int reset_tree(struct object_id *i_tree, int update, int reset)
opts.src_index = &the_index; opts.src_index = &the_index;
opts.dst_index = &the_index; opts.dst_index = &the_index;
opts.merge = 1; opts.merge = 1;
opts.reset = reset; opts.reset = reset ? UNPACK_RESET_PROTECT_UNTRACKED : 0;
opts.update = update; opts.update = update;
if (update)
opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
opts.fn = oneway_merge; opts.fn = oneway_merge;
if (unpack_trees(nr_trees, t, &opts)) if (unpack_trees(nr_trees, t, &opts))
@ -1533,6 +1535,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
} else { } else {
struct child_process cp = CHILD_PROCESS_INIT; struct child_process cp = CHILD_PROCESS_INIT;
cp.git_cmd = 1; cp.git_cmd = 1;
/* BUG: this nukes untracked files in the way */
strvec_pushl(&cp.args, "reset", "--hard", "-q", strvec_pushl(&cp.args, "reset", "--hard", "-q",
"--no-recurse-submodules", NULL); "--no-recurse-submodules", NULL);
if (run_command(&cp)) { if (run_command(&cp)) {

View File

@ -3086,6 +3086,10 @@ static int add_submodule(const struct add_data *add_data)
prepare_submodule_repo_env(&cp.env_array); prepare_submodule_repo_env(&cp.env_array);
cp.git_cmd = 1; cp.git_cmd = 1;
cp.dir = add_data->sm_path; cp.dir = add_data->sm_path;
/*
* NOTE: we only get here if add_data->force is true, so
* passing --force to checkout is reasonable.
*/
strvec_pushl(&cp.args, "checkout", "-f", "-q", NULL); strvec_pushl(&cp.args, "checkout", "-f", "-q", NULL);
if (add_data->branch) { if (add_data->branch) {

View File

@ -91,7 +91,7 @@ do
git checkout -q $commit -- . git checkout -q $commit -- .
git rerere git rerere
fi fi
git reset -q --hard git reset -q --hard # Might nuke untracked files...
done done
if test -z "$branch" if test -z "$branch"

View File

@ -4062,11 +4062,7 @@ static int checkout(struct merge_options *opt,
unpack_opts.quiet = 0; /* FIXME: sequencer might want quiet? */ unpack_opts.quiet = 0; /* FIXME: sequencer might want quiet? */
unpack_opts.verbose_update = (opt->verbosity > 2); unpack_opts.verbose_update = (opt->verbosity > 2);
unpack_opts.fn = twoway_merge; unpack_opts.fn = twoway_merge;
if (1/* FIXME: opts->overwrite_ignore*/) { unpack_opts.preserve_ignored = 0; /* FIXME: !opts->overwrite_ignore */
CALLOC_ARRAY(unpack_opts.dir, 1);
unpack_opts.dir->flags |= DIR_SHOW_IGNORED;
setup_standard_excludes(unpack_opts.dir);
}
parse_tree(prev); parse_tree(prev);
init_tree_desc(&trees[0], prev->buffer, prev->size); init_tree_desc(&trees[0], prev->buffer, prev->size);
parse_tree(next); parse_tree(next);
@ -4074,8 +4070,6 @@ static int checkout(struct merge_options *opt,
ret = unpack_trees(2, trees, &unpack_opts); ret = unpack_trees(2, trees, &unpack_opts);
clear_unpack_trees_porcelain(&unpack_opts); clear_unpack_trees_porcelain(&unpack_opts);
dir_clear(unpack_opts.dir);
FREE_AND_NULL(unpack_opts.dir);
return ret; return ret;
} }

View File

@ -409,8 +409,11 @@ static int unpack_trees_start(struct merge_options *opt,
memset(&opt->priv->unpack_opts, 0, sizeof(opt->priv->unpack_opts)); memset(&opt->priv->unpack_opts, 0, sizeof(opt->priv->unpack_opts));
if (opt->priv->call_depth) if (opt->priv->call_depth)
opt->priv->unpack_opts.index_only = 1; opt->priv->unpack_opts.index_only = 1;
else else {
opt->priv->unpack_opts.update = 1; opt->priv->unpack_opts.update = 1;
/* FIXME: should only do this if !overwrite_ignore */
opt->priv->unpack_opts.preserve_ignored = 0;
}
opt->priv->unpack_opts.merge = 1; opt->priv->unpack_opts.merge = 1;
opt->priv->unpack_opts.head_idx = 2; opt->priv->unpack_opts.head_idx = 2;
opt->priv->unpack_opts.fn = threeway_merge; opt->priv->unpack_opts.fn = threeway_merge;

View File

@ -53,7 +53,6 @@ int checkout_fast_forward(struct repository *r,
struct unpack_trees_options opts; struct unpack_trees_options opts;
struct tree_desc t[MAX_UNPACK_TREES]; struct tree_desc t[MAX_UNPACK_TREES];
int i, nr_trees = 0; int i, nr_trees = 0;
struct dir_struct dir = DIR_INIT;
struct lock_file lock_file = LOCK_INIT; struct lock_file lock_file = LOCK_INIT;
refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL); refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL);
@ -80,11 +79,7 @@ int checkout_fast_forward(struct repository *r,
} }
memset(&opts, 0, sizeof(opts)); memset(&opts, 0, sizeof(opts));
if (overwrite_ignore) { opts.preserve_ignored = !overwrite_ignore;
dir.flags |= DIR_SHOW_IGNORED;
setup_standard_excludes(&dir);
opts.dir = &dir;
}
opts.head_idx = 1; opts.head_idx = 1;
opts.src_index = r->index; opts.src_index = r->index;
@ -101,7 +96,6 @@ int checkout_fast_forward(struct repository *r,
clear_unpack_trees_porcelain(&opts); clear_unpack_trees_porcelain(&opts);
return -1; return -1;
} }
dir_clear(&dir);
clear_unpack_trees_porcelain(&opts); clear_unpack_trees_porcelain(&opts);
if (write_locked_index(r->index, &lock_file, COMMIT_LOCK)) if (write_locked_index(r->index, &lock_file, COMMIT_LOCK))

View File

@ -56,9 +56,10 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,
unpack_tree_opts.fn = reset_hard ? oneway_merge : twoway_merge; unpack_tree_opts.fn = reset_hard ? oneway_merge : twoway_merge;
unpack_tree_opts.update = 1; unpack_tree_opts.update = 1;
unpack_tree_opts.merge = 1; unpack_tree_opts.merge = 1;
unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL); init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL);
if (!detach_head) if (!detach_head)
unpack_tree_opts.reset = 1; unpack_tree_opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
if (repo_read_index_unmerged(r) < 0) { if (repo_read_index_unmerged(r) < 0) {
ret = error(_("could not read index")); ret = error(_("could not read index"));

View File

@ -3693,6 +3693,7 @@ static int do_reset(struct repository *r,
unpack_tree_opts.fn = oneway_merge; unpack_tree_opts.fn = oneway_merge;
unpack_tree_opts.merge = 1; unpack_tree_opts.merge = 1;
unpack_tree_opts.update = 1; unpack_tree_opts.update = 1;
unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
init_checkout_metadata(&unpack_tree_opts.meta, name, &oid, NULL); init_checkout_metadata(&unpack_tree_opts.meta, name, &oid, NULL);
if (repo_read_index_unmerged(r)) { if (repo_read_index_unmerged(r)) {

View File

@ -1896,6 +1896,7 @@ static void submodule_reset_index(const char *path)
strvec_pushf(&cp.args, "--super-prefix=%s%s/", strvec_pushf(&cp.args, "--super-prefix=%s%s/",
get_super_prefix_or_empty(), path); get_super_prefix_or_empty(), path);
/* TODO: determine if this might overwright untracked files */
strvec_pushl(&cp.args, "read-tree", "-u", "--reset", NULL); strvec_pushl(&cp.args, "read-tree", "-u", "--reset", NULL);
strvec_push(&cp.args, empty_tree_oid_hex()); strvec_push(&cp.args, empty_tree_oid_hex());

View File

@ -6,7 +6,6 @@ test_description='read-tree can handle submodules'
. "$TEST_DIRECTORY"/lib-submodule-update.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh
KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS=1 KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS=1
KNOWN_FAILURE_SUBMODULE_OVERWRITE_IGNORED_UNTRACKED=1
test_submodule_switch_recursing_with_args "read-tree -u -m" test_submodule_switch_recursing_with_args "read-tree -u -m"

244
t/t2500-untracked-overwriting.sh Executable file
View File

@ -0,0 +1,244 @@
#!/bin/sh
test_description='Test handling of overwriting untracked files'
. ./test-lib.sh
test_setup_reset () {
git init reset_$1 &&
(
cd reset_$1 &&
test_commit init &&
git branch stable &&
git branch work &&
git checkout work &&
test_commit foo &&
git checkout stable
)
}
test_expect_success 'reset --hard will nuke untracked files/dirs' '
test_setup_reset hard &&
(
cd reset_hard &&
git ls-tree -r stable &&
git log --all --name-status --oneline &&
git ls-tree -r work &&
mkdir foo.t &&
echo precious >foo.t/file &&
echo foo >expect &&
git reset --hard work &&
# check that untracked directory foo.t/ was nuked
test_path_is_file foo.t &&
test_cmp expect foo.t
)
'
test_expect_success 'reset --merge will preserve untracked files/dirs' '
test_setup_reset merge &&
(
cd reset_merge &&
mkdir foo.t &&
echo precious >foo.t/file &&
cp foo.t/file expect &&
test_must_fail git reset --merge work 2>error &&
test_cmp expect foo.t/file &&
grep "Updating .foo.t. would lose untracked files" error
)
'
test_expect_success 'reset --keep will preserve untracked files/dirs' '
test_setup_reset keep &&
(
cd reset_keep &&
mkdir foo.t &&
echo precious >foo.t/file &&
cp foo.t/file expect &&
test_must_fail git reset --merge work 2>error &&
test_cmp expect foo.t/file &&
grep "Updating.*foo.t.*would lose untracked files" error
)
'
test_setup_checkout_m () {
git init checkout &&
(
cd checkout &&
test_commit init &&
test_write_lines file has some >filler &&
git add filler &&
git commit -m filler &&
git branch stable &&
git switch -c work &&
echo stuff >notes.txt &&
test_write_lines file has some words >filler &&
git add notes.txt filler &&
git commit -m filler &&
git checkout stable
)
}
test_expect_success 'checkout -m does not nuke untracked file' '
test_setup_checkout_m &&
(
cd checkout &&
# Tweak filler
test_write_lines this file has some >filler &&
# Make an untracked file, save its contents in "expect"
echo precious >notes.txt &&
cp notes.txt expect &&
test_must_fail git checkout -m work &&
test_cmp expect notes.txt
)
'
test_setup_sequencing () {
git init sequencing_$1 &&
(
cd sequencing_$1 &&
test_commit init &&
test_write_lines this file has some words >filler &&
git add filler &&
git commit -m filler &&
mkdir -p foo/bar &&
test_commit foo/bar/baz &&
git branch simple &&
git branch fooey &&
git checkout fooey &&
git rm foo/bar/baz.t &&
echo stuff >>filler &&
git add -u &&
git commit -m "changes" &&
git checkout simple &&
echo items >>filler &&
echo newstuff >>newfile &&
git add filler newfile &&
git commit -m another
)
}
test_expect_success 'git rebase --abort and untracked files' '
test_setup_sequencing rebase_abort_and_untracked &&
(
cd sequencing_rebase_abort_and_untracked &&
git checkout fooey &&
test_must_fail git rebase simple &&
cat init.t &&
git rm init.t &&
echo precious >init.t &&
cp init.t expect &&
git status --porcelain &&
test_must_fail git rebase --abort &&
test_cmp expect init.t
)
'
test_expect_success 'git rebase fast forwarding and untracked files' '
test_setup_sequencing rebase_fast_forward_and_untracked &&
(
cd sequencing_rebase_fast_forward_and_untracked &&
git checkout init &&
echo precious >filler &&
cp filler expect &&
test_must_fail git rebase init simple &&
test_cmp expect filler
)
'
test_expect_failure 'git rebase --autostash and untracked files' '
test_setup_sequencing rebase_autostash_and_untracked &&
(
cd sequencing_rebase_autostash_and_untracked &&
git checkout simple &&
git rm filler &&
mkdir filler &&
echo precious >filler/file &&
cp filler/file expect &&
git rebase --autostash init &&
test_path_is_file filler/file
)
'
test_expect_failure 'git stash and untracked files' '
test_setup_sequencing stash_and_untracked_files &&
(
cd sequencing_stash_and_untracked_files &&
git checkout simple &&
git rm filler &&
mkdir filler &&
echo precious >filler/file &&
cp filler/file expect &&
git status --porcelain &&
git stash push &&
git status --porcelain &&
test_path_is_file filler/file
)
'
test_expect_success 'git am --abort and untracked dir vs. unmerged file' '
test_setup_sequencing am_abort_and_untracked &&
(
cd sequencing_am_abort_and_untracked &&
git format-patch -1 --stdout fooey >changes.mbox &&
test_must_fail git am --3way changes.mbox &&
# Delete the conflicted file; we will stage and commit it later
rm filler &&
# Put an unrelated untracked directory there
mkdir filler &&
echo foo >filler/file1 &&
echo bar >filler/file2 &&
test_must_fail git am --abort 2>errors &&
test_path_is_dir filler &&
grep "Updating .filler. would lose untracked files in it" errors
)
'
test_expect_success 'git am --skip and untracked dir vs deleted file' '
test_setup_sequencing am_skip_and_untracked &&
(
cd sequencing_am_skip_and_untracked &&
git checkout fooey &&
git format-patch -1 --stdout simple >changes.mbox &&
test_must_fail git am --3way changes.mbox &&
# Delete newfile
rm newfile &&
# Put an unrelated untracked directory there
mkdir newfile &&
echo foo >newfile/file1 &&
echo bar >newfile/file2 &&
# Change our mind about resolutions, just skip this patch
test_must_fail git am --skip 2>errors &&
test_path_is_dir newfile &&
grep "Updating .newfile. would lose untracked files in it" errors
)
'
test_done

View File

@ -6,7 +6,6 @@ test_description='reset can handle submodules'
. "$TEST_DIRECTORY"/lib-submodule-update.sh . "$TEST_DIRECTORY"/lib-submodule-update.sh
KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS=1 KNOWN_FAILURE_DIRECTORY_SUBMODULE_CONFLICTS=1
KNOWN_FAILURE_SUBMODULE_OVERWRITE_IGNORED_UNTRACKED=1
test_submodule_switch_recursing_with_args "reset --keep" test_submodule_switch_recursing_with_args "reset --keep"

View File

@ -1694,9 +1694,15 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
static struct cache_entry *dfc; static struct cache_entry *dfc;
struct pattern_list pl; struct pattern_list pl;
int free_pattern_list = 0; int free_pattern_list = 0;
struct dir_struct dir = DIR_INIT;
if (o->reset == UNPACK_RESET_INVALID)
BUG("o->reset had a value of 1; should be UNPACK_TREES_*_UNTRACKED");
if (len > MAX_UNPACK_TREES) if (len > MAX_UNPACK_TREES)
die("unpack_trees takes at most %d trees", MAX_UNPACK_TREES); die("unpack_trees takes at most %d trees", MAX_UNPACK_TREES);
if (o->dir)
BUG("o->dir is for internal use only");
trace_performance_enter(); trace_performance_enter();
trace2_region_enter("unpack_trees", "unpack_trees", the_repository); trace2_region_enter("unpack_trees", "unpack_trees", the_repository);
@ -1707,6 +1713,16 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
ensure_full_index(o->dst_index); ensure_full_index(o->dst_index);
} }
if (o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED &&
o->preserve_ignored)
BUG("UNPACK_RESET_OVERWRITE_UNTRACKED incompatible with preserved ignored files");
if (!o->preserve_ignored) {
o->dir = &dir;
o->dir->flags |= DIR_SHOW_IGNORED;
setup_standard_excludes(o->dir);
}
if (!core_apply_sparse_checkout || !o->update) if (!core_apply_sparse_checkout || !o->update)
o->skip_sparse_checkout = 1; o->skip_sparse_checkout = 1;
if (!o->skip_sparse_checkout && !o->pl) { if (!o->skip_sparse_checkout && !o->pl) {
@ -1868,6 +1884,10 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
done: done:
if (free_pattern_list) if (free_pattern_list)
clear_pattern_list(&pl); clear_pattern_list(&pl);
if (o->dir) {
dir_clear(o->dir);
o->dir = NULL;
}
trace2_region_leave("unpack_trees", "unpack_trees", the_repository); trace2_region_leave("unpack_trees", "unpack_trees", the_repository);
trace_performance_leave("unpack_trees"); trace_performance_leave("unpack_trees");
return ret; return ret;
@ -2158,9 +2178,15 @@ static int icase_exists(struct unpack_trees_options *o, const char *name, int le
return src && !ie_match_stat(o->src_index, src, st, CE_MATCH_IGNORE_VALID|CE_MATCH_IGNORE_SKIP_WORKTREE); return src && !ie_match_stat(o->src_index, src, st, CE_MATCH_IGNORE_VALID|CE_MATCH_IGNORE_SKIP_WORKTREE);
} }
enum absent_checking_type {
COMPLETELY_ABSENT,
ABSENT_ANY_DIRECTORY
};
static int check_ok_to_remove(const char *name, int len, int dtype, static int check_ok_to_remove(const char *name, int len, int dtype,
const struct cache_entry *ce, struct stat *st, const struct cache_entry *ce, struct stat *st,
enum unpack_trees_error_types error_type, enum unpack_trees_error_types error_type,
enum absent_checking_type absent_type,
struct unpack_trees_options *o) struct unpack_trees_options *o)
{ {
const struct cache_entry *result; const struct cache_entry *result;
@ -2195,6 +2221,10 @@ static int check_ok_to_remove(const char *name, int len, int dtype,
return 0; return 0;
} }
/* If we only care about directories, then we can remove */
if (absent_type == ABSENT_ANY_DIRECTORY)
return 0;
/* /*
* The previous round may already have decided to * The previous round may already have decided to
* delete this path, which is in a subdirectory that * delete this path, which is in a subdirectory that
@ -2215,12 +2245,14 @@ static int check_ok_to_remove(const char *name, int len, int dtype,
*/ */
static int verify_absent_1(const struct cache_entry *ce, static int verify_absent_1(const struct cache_entry *ce,
enum unpack_trees_error_types error_type, enum unpack_trees_error_types error_type,
enum absent_checking_type absent_type,
struct unpack_trees_options *o) struct unpack_trees_options *o)
{ {
int len; int len;
struct stat st; struct stat st;
if (o->index_only || o->reset || !o->update) if (o->index_only || !o->update ||
o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED)
return 0; return 0;
len = check_leading_path(ce->name, ce_namelen(ce), 0); len = check_leading_path(ce->name, ce_namelen(ce), 0);
@ -2240,7 +2272,8 @@ static int verify_absent_1(const struct cache_entry *ce,
NULL, o); NULL, o);
else else
ret = check_ok_to_remove(path, len, DT_UNKNOWN, NULL, ret = check_ok_to_remove(path, len, DT_UNKNOWN, NULL,
&st, error_type, o); &st, error_type,
absent_type, o);
} }
free(path); free(path);
return ret; return ret;
@ -2255,7 +2288,7 @@ static int verify_absent_1(const struct cache_entry *ce,
return check_ok_to_remove(ce->name, ce_namelen(ce), return check_ok_to_remove(ce->name, ce_namelen(ce),
ce_to_dtype(ce), ce, &st, ce_to_dtype(ce), ce, &st,
error_type, o); error_type, absent_type, o);
} }
} }
@ -2265,14 +2298,23 @@ static int verify_absent(const struct cache_entry *ce,
{ {
if (!o->skip_sparse_checkout && (ce->ce_flags & CE_NEW_SKIP_WORKTREE)) if (!o->skip_sparse_checkout && (ce->ce_flags & CE_NEW_SKIP_WORKTREE))
return 0; return 0;
return verify_absent_1(ce, error_type, o); return verify_absent_1(ce, error_type, COMPLETELY_ABSENT, o);
}
static int verify_absent_if_directory(const struct cache_entry *ce,
enum unpack_trees_error_types error_type,
struct unpack_trees_options *o)
{
if (!o->skip_sparse_checkout && (ce->ce_flags & CE_NEW_SKIP_WORKTREE))
return 0;
return verify_absent_1(ce, error_type, ABSENT_ANY_DIRECTORY, o);
} }
static int verify_absent_sparse(const struct cache_entry *ce, static int verify_absent_sparse(const struct cache_entry *ce,
enum unpack_trees_error_types error_type, enum unpack_trees_error_types error_type,
struct unpack_trees_options *o) struct unpack_trees_options *o)
{ {
return verify_absent_1(ce, error_type, o); return verify_absent_1(ce, error_type, COMPLETELY_ABSENT, o);
} }
static int merged_entry(const struct cache_entry *ce, static int merged_entry(const struct cache_entry *ce,
@ -2346,6 +2388,12 @@ static int merged_entry(const struct cache_entry *ce,
* Previously unmerged entry left as an existence * Previously unmerged entry left as an existence
* marker by read_index_unmerged(); * marker by read_index_unmerged();
*/ */
if (verify_absent_if_directory(merge,
ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN, o)) {
discard_cache_entry(merge);
return -1;
}
invalidate_ce_path(old, o); invalidate_ce_path(old, o);
} }
@ -2363,7 +2411,10 @@ static int deleted_entry(const struct cache_entry *ce,
if (verify_absent(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o)) if (verify_absent(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o))
return -1; return -1;
return 0; return 0;
} else if (verify_absent_if_directory(ce, ERROR_WOULD_LOSE_UNTRACKED_REMOVED, o)) {
return -1;
} }
if (!(old->ce_flags & CE_CONFLICTED) && verify_uptodate(old, o)) if (!(old->ce_flags & CE_CONFLICTED) && verify_uptodate(old, o))
return -1; return -1;
add_entry(o, ce, CE_REMOVE, 0); add_entry(o, ce, CE_REMOVE, 0);

View File

@ -45,10 +45,17 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
*/ */
void clear_unpack_trees_porcelain(struct unpack_trees_options *opts); void clear_unpack_trees_porcelain(struct unpack_trees_options *opts);
enum unpack_trees_reset_type {
UNPACK_RESET_NONE = 0, /* traditional "false" value; still valid */
UNPACK_RESET_INVALID = 1, /* "true" no longer valid; use below values */
UNPACK_RESET_PROTECT_UNTRACKED,
UNPACK_RESET_OVERWRITE_UNTRACKED
};
struct unpack_trees_options { struct unpack_trees_options {
unsigned int reset, unsigned int merge,
merge,
update, update,
preserve_ignored,
clone, clone,
index_only, index_only,
nontrivial_merge, nontrivial_merge,
@ -64,9 +71,9 @@ struct unpack_trees_options {
exiting_early, exiting_early,
show_all_errors, show_all_errors,
dry_run; dry_run;
enum unpack_trees_reset_type reset;
const char *prefix; const char *prefix;
int cache_bottom; int cache_bottom;
struct dir_struct *dir;
struct pathspec *pathspec; struct pathspec *pathspec;
merge_fn_t fn; merge_fn_t fn;
const char *msgs[NB_UNPACK_TREES_WARNING_TYPES]; const char *msgs[NB_UNPACK_TREES_WARNING_TYPES];
@ -88,6 +95,7 @@ struct unpack_trees_options {
struct index_state result; struct index_state result;
struct pattern_list *pl; /* for internal use */ struct pattern_list *pl; /* for internal use */
struct dir_struct *dir; /* for internal use only */
struct checkout_metadata meta; struct checkout_metadata meta;
}; };