[1/2] read-tree --reset: add --protect-untracked
diff mbox series

Message ID 20190501101403.20294-2-phillip.wood123@gmail.com
State New
Headers show
Series
  • read-tree: improve untracked file support
Related show

Commit Message

Phillip Wood May 1, 2019, 10:14 a.m. UTC
From: Phillip Wood <phillip.wood@dunelm.org.uk>

Currently there is no way to get git to discard changes to the
worktree without overwriting untracked files. `reset --hard`,
`checkout --force`, `checkout <rev> :/` and `read-tree --reset -u`
will all overwrite untracked files. unpack_trees() has known how to
avoid overwriting untracked files since commit fcc387db9b ("read-tree
-m -u: do not overwrite or remove untracked working tree files.",
2006-05-17) in response to concerns about lost files [1] but those
protections do not apply to --reset. This patch adds an
--protect-untracked option for read-tree/unpack_trees() to apply the
same protections with --reset. It does not change the behavior of any
of the porcelain commands but paves the way for adding options or
changing the default for those in future.

Note the actual change in unpack-trees.c is quite simple, the majority
of this patch is converting existing callers to use the new
unpack_trees_reset_type enum.

[1] https://public-inbox.org/git/8aa486160605161500m1dd8428cj@mail.gmail.com/

Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
---

Notes:
    adding --protect-untracked to the invocation of
    test_submodule_forced_switch() in t1013 fixes the known breakage of
    tests 57 & 58 but breaks test 64 which relies on forcing. I'm not sure
    that the expected results of 57 & 58 are correct if we're forcing.

 Documentation/git-read-tree.txt | 10 +++++++--
 builtin/am.c                    |  8 ++++---
 builtin/checkout.c              |  2 +-
 builtin/read-tree.c             | 12 +++++++++++
 builtin/rebase.c                |  2 +-
 builtin/reset.c                 |  2 +-
 builtin/stash.c                 |  7 ++++---
 t/lib-read-tree.sh              | 11 ++++++++++
 t/t1005-read-tree-reset.sh      | 37 +++++++++++++++++++++++++++++++--
 unpack-trees.c                  |  3 ++-
 unpack-trees.h                  | 10 +++++++--
 11 files changed, 88 insertions(+), 16 deletions(-)

Comments

Duy Nguyen May 1, 2019, 10:18 a.m. UTC | #1
On Wed, May 1, 2019 at 5:14 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
> diff --git a/unpack-trees.h b/unpack-trees.h
> index d344d7d296..732b262c4d 100644
> --- a/unpack-trees.h
> +++ b/unpack-trees.h
> @@ -41,9 +41,15 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
>   */
>  void clear_unpack_trees_porcelain(struct unpack_trees_options *opts);
>
> +enum unpack_trees_reset_type {
> +       UNPACK_NO_RESET = 0,
> +       UNPACK_RESET_OVERWRITE_UNTRACKED,
> +       UNPACK_RESET_PROTECT_UNTRACKED
> +};
> +
>  struct unpack_trees_options {
> -       unsigned int reset,
> -                    merge,
> +       enum unpack_trees_reset_type reset;

Can we add protected_untracked that can be used in combination with
the current "reset"?

This opens up (or raises the question about) the opportunity to
protect untracked changes on non-reset updates.

> +       unsigned int merge,
>                      update,
>                      clone,
>                      index_only,
> --
> 2.21.0
>
Duy Nguyen May 1, 2019, 10:32 a.m. UTC | #2
On Wed, May 1, 2019 at 5:18 PM Duy Nguyen <pclouds@gmail.com> wrote:
>
> On Wed, May 1, 2019 at 5:14 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
> > diff --git a/unpack-trees.h b/unpack-trees.h
> > index d344d7d296..732b262c4d 100644
> > --- a/unpack-trees.h
> > +++ b/unpack-trees.h
> > @@ -41,9 +41,15 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
> >   */
> >  void clear_unpack_trees_porcelain(struct unpack_trees_options *opts);
> >
> > +enum unpack_trees_reset_type {
> > +       UNPACK_NO_RESET = 0,
> > +       UNPACK_RESET_OVERWRITE_UNTRACKED,
> > +       UNPACK_RESET_PROTECT_UNTRACKED
> > +};
> > +
> >  struct unpack_trees_options {
> > -       unsigned int reset,
> > -                    merge,
> > +       enum unpack_trees_reset_type reset;
>
> Can we add protected_untracked that can be used in combination with
> the current "reset"?
>
> This opens up (or raises the question about) the opportunity to
> protect untracked changes on non-reset updates.

Bahh. "protect_untracked" is already on for non-reset updates. Sorry
for the noise.
Duy Nguyen May 2, 2019, 10:38 a.m. UTC | #3
On Wed, May 1, 2019 at 5:14 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> From: Phillip Wood <phillip.wood@dunelm.org.uk>
>
> Currently there is no way to get git to discard changes to the
> worktree without overwriting untracked files. `reset --hard`,
> `checkout --force`, `checkout <rev> :/` and `read-tree --reset -u`

"checkout <rev> :/" does not use unpack-trees, I think, so it does not
belong to this group.

> will all overwrite untracked files. unpack_trees() has known how to
> avoid overwriting untracked files since commit fcc387db9b ("read-tree
> -m -u: do not overwrite or remove untracked working tree files.",
> 2006-05-17) in response to concerns about lost files [1] but those
> protections do not apply to --reset. This patch adds an
> --protect-untracked option for read-tree/unpack_trees() to apply th
> same protections with --reset.

Your enum makes me wonder if we should have --reset-tracked instead of
--protect-untracked (say what you want to reset seems tiny bit easier
to understand than "reset except untracked"). Which leads to another
question, do we ever need --reset-untracked (i.e. overwriting
untracked files but never ever touch tracked ones).

I have one use case that --reset-untracked might make sense. Suppose
you do "git reset HEAD^" where HEAD has some new files. The new files
will be left alone in worktree of course, but switching back to HEAD
after that, unpack-trees will cry murder because it would otherwise
overwrite untracked files (that have the exact same content).

--reset-untracked might be useful for that. I'm carrying a patch to
detect identical file content though which addresses this very
specific case. Maybe it's the only case that --reset-untracked may be
useful.

> It does not change the behavior of any
> of the porcelain commands but paves the way for adding options or
> changing the default for those in future.
>
> Note the actual change in unpack-trees.c is quite simple, the majority
> of this patch is converting existing callers to use the new
> unpack_trees_reset_type enum.

This could be split in a separate patch, then the actual change would
become small and more to the point.

> [1] https://public-inbox.org/git/8aa486160605161500m1dd8428cj@mail.gmail.com/
>
> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
> ---
>
> Notes:
>     adding --protect-untracked to the invocation of
>     test_submodule_forced_switch() in t1013 fixes the known breakage of
>     tests 57 & 58 but breaks test 64 which relies on forcing. I'm not sure
>     that the expected results of 57 & 58 are correct if we're forcing.
>
>  Documentation/git-read-tree.txt | 10 +++++++--
>  builtin/am.c                    |  8 ++++---
>  builtin/checkout.c              |  2 +-
>  builtin/read-tree.c             | 12 +++++++++++
>  builtin/rebase.c                |  2 +-
>  builtin/reset.c                 |  2 +-
>  builtin/stash.c                 |  7 ++++---
>  t/lib-read-tree.sh              | 11 ++++++++++
>  t/t1005-read-tree-reset.sh      | 37 +++++++++++++++++++++++++++++++--
>  unpack-trees.c                  |  3 ++-
>  unpack-trees.h                  | 10 +++++++--
>  11 files changed, 88 insertions(+), 16 deletions(-)
>
> diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt
> index d271842608..67864c6bbc 100644
> --- a/Documentation/git-read-tree.txt
> +++ b/Documentation/git-read-tree.txt
> @@ -40,12 +40,18 @@ OPTIONS
>  --reset::
>         Same as -m, except that unmerged entries are discarded instead
>         of failing. When used with `-u`, updates leading to loss of
> -       working tree changes will not abort the operation.
> +       working tree changes will not abort the operation and
> +       untracked files will be overwritten. Use `--protect-untracked`
> +       to avoid overwriting untracked files.
>
>  -u::
>         After a successful merge, update the files in the work
>         tree with the result of the merge.
>
> +--protect-untracked::
> +       Prevent `--reset` `-u` from overwriting untracked files. Requires
> +       `--reset` and `-u` (`-m` never overwrites untracked files).
> +
>  -i::
>         Usually a merge requires the index file as well as the
>         files in the working tree to be up to date with the
> @@ -89,7 +95,7 @@ OPTIONS
>         existed in the original index file.
>
>  --exclude-per-directory=<gitignore>::
> -       When running the command with `-u` and `-m` options, the
> +       When using `-u` with `-m` or `--reset` `--protect-untracked` 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
> diff --git a/builtin/am.c b/builtin/am.c
> index 912d9821b1..a92394b682 100644
> --- a/builtin/am.c
> +++ b/builtin/am.c
> @@ -1854,7 +1854,8 @@ static void am_resolve(struct am_state *state)
>   * true, any unmerged entries will be discarded. Returns 0 on success, -1 on
>   * failure.
>   */
> -static int fast_forward_to(struct tree *head, struct tree *remote, int reset)
> +static int fast_forward_to(struct tree *head, struct tree *remote,
> +                          enum unpack_trees_reset_type reset)
>  {
>         struct lock_file lock_file = LOCK_INIT;
>         struct unpack_trees_options opts;
> @@ -1942,7 +1943,8 @@ static int clean_index(const struct object_id *head, const struct object_id *rem
>
>         read_cache_unmerged();
>
> -       if (fast_forward_to(head_tree, head_tree, 1))
> +       if (fast_forward_to(head_tree, head_tree,
> +                           UNPACK_RESET_OVERWRITE_UNTRACKED))
>                 return -1;
>
>         if (write_cache_as_tree(&index, 0, NULL))
> @@ -1952,7 +1954,7 @@ static int clean_index(const struct object_id *head, const struct object_id *rem
>         if (!index_tree)
>                 return error(_("Could not parse object '%s'."), oid_to_hex(&index));
>
> -       if (fast_forward_to(index_tree, remote_tree, 0))
> +       if (fast_forward_to(index_tree, remote_tree, UNPACK_NO_RESET))
>                 return -1;
>
>         if (merge_tree(remote_tree))
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index ffa776c6e1..e9e70018f9 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -506,7 +506,7 @@ static int reset_tree(struct tree *tree, const struct checkout_opts *o,
>         opts.head_idx = -1;
>         opts.update = worktree;
>         opts.skip_unmerged = !worktree;
> -       opts.reset = 1;
> +       opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
>         opts.merge = 1;
>         opts.fn = oneway_merge;
>         opts.verbose_update = o->show_progress;
> diff --git a/builtin/read-tree.c b/builtin/read-tree.c
> index 5c9c082595..23735adde9 100644
> --- a/builtin/read-tree.c
> +++ b/builtin/read-tree.c
> @@ -114,6 +114,7 @@ static int git_read_tree_config(const char *var, const char *value, void *cb)
>  int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
>  {
>         int i, stage = 0;
> +       int protect_untracked = -1;
>         struct object_id oid;
>         struct tree_desc t[MAX_UNPACK_TREES];
>         struct unpack_trees_options opts;
> @@ -140,6 +141,8 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
>                   PARSE_OPT_NONEG },
>                 OPT_BOOL('u', NULL, &opts.update,
>                          N_("update working tree with merge result")),
> +               OPT_BOOL(0, "protect-untracked", &protect_untracked,
> +                        N_("do not overwrite untracked files")),
>                 { OPTION_CALLBACK, 0, "exclude-per-directory", &opts,
>                   N_("gitignore"),
>                   N_("allow explicitly ignored files to be overwritten"),
> @@ -209,8 +212,17 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
>         if ((opts.update || opts.index_only) && !opts.merge)
>                 die("%s is meaningless without -m, --reset, or --prefix",
>                     opts.update ? "-u" : "-i");
> +       if (protect_untracked >= 0) {
> +               if (!opts.reset || !opts.update)
> +                       die("-%s-protect-untracked requires --reset and -u",
> +                           protect_untracked ? "" : "-no");
> +               opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
> +       }
>         if ((opts.dir && !opts.update))
>                 die("--exclude-per-directory is meaningless unless -u");
> +       if (opts.dir && opts.reset == UNPACK_RESET_OVERWRITE_UNTRACKED)
> +               warning("--exclude-per-directory without --preserve-untracked "
> +                       "has no effect");
>         if (opts.merge && !opts.index_only)
>                 setup_work_tree();
>
> diff --git a/builtin/rebase.c b/builtin/rebase.c
> index 2e41ad5644..feb30a71f5 100644
> --- a/builtin/rebase.c
> +++ b/builtin/rebase.c
> @@ -398,7 +398,7 @@ static int reset_head(struct object_id *oid, const char *action,
>         unpack_tree_opts.update = 1;
>         unpack_tree_opts.merge = 1;
>         if (!detach_head)
> -               unpack_tree_opts.reset = 1;
> +               unpack_tree_opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
>
>         if (repo_read_index_unmerged(the_repository) < 0) {
>                 ret = error(_("could not read index"));
> diff --git a/builtin/reset.c b/builtin/reset.c
> index 26ef9a7bd0..a39dd92fb2 100644
> --- a/builtin/reset.c
> +++ b/builtin/reset.c
> @@ -70,7 +70,7 @@ static int reset_index(const struct object_id *oid, int reset_type, int quiet)
>                 opts.update = 1;
>                 /* fallthrough */
>         default:
> -               opts.reset = 1;
> +               opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
>         }
>
>         read_cache_unmerged();
> diff --git a/builtin/stash.c b/builtin/stash.c
> index 2a8e6d09b4..175d1b62d3 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -227,7 +227,8 @@ static int clear_stash(int argc, const char **argv, const char *prefix)
>         return do_clear_stash();
>  }
>
> -static int reset_tree(struct object_id *i_tree, int update, int reset)
> +static int reset_tree(struct object_id *i_tree, int update,
> +                     enum unpack_trees_reset_type reset)
>  {
>         int nr_trees = 1;
>         struct unpack_trees_options opts;
> @@ -461,7 +462,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
>         }
>
>         if (has_index) {
> -               if (reset_tree(&index_tree, 0, 0))
> +               if (reset_tree(&index_tree, 0, UNPACK_NO_RESET))
>                         return -1;
>         } else {
>                 struct strbuf out = STRBUF_INIT;
> @@ -471,7 +472,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
>                         return -1;
>                 }
>
> -               if (reset_tree(&c_tree, 0, 1)) {
> +               if (reset_tree(&c_tree, 0, UNPACK_RESET_OVERWRITE_UNTRACKED)) {
>                         strbuf_release(&out);
>                         return -1;
>                 }
> diff --git a/t/lib-read-tree.sh b/t/lib-read-tree.sh
> index b95f485606..22f516d91f 100644
> --- a/t/lib-read-tree.sh
> +++ b/t/lib-read-tree.sh
> @@ -39,3 +39,14 @@ read_tree_u_must_fail () {
>         test_cmp pre-dry-run-wt post-dry-run-wt &&
>         test_must_fail git read-tree "$@"
>  }
> +
> +read_tree_u_must_fail_save_err () {
> +       git ls-files -s >pre-dry-run &&
> +       git diff-files -p >pre-dry-run-wt &&
> +       test_must_fail git read-tree -n "$@" &&
> +       git ls-files -s >post-dry-run &&
> +       git diff-files -p >post-dry-run-wt &&
> +       test_cmp pre-dry-run post-dry-run &&
> +       test_cmp pre-dry-run-wt post-dry-run-wt &&
> +       test_must_fail git read-tree "$@" 2>actual-err
> +}
> diff --git a/t/t1005-read-tree-reset.sh b/t/t1005-read-tree-reset.sh
> index 83b09e1310..6c9dd6805b 100755
> --- a/t/t1005-read-tree-reset.sh
> +++ b/t/t1005-read-tree-reset.sh
> @@ -19,15 +19,48 @@ test_expect_success 'setup' '
>         git add df &&
>         echo content >new &&
>         git add new &&
> -       git commit -m two
> +       git commit -m two &&
> +       git ls-files >expect-two
>  '
>
> -test_expect_success 'reset should work' '
> +test_expect_success '--protect-untracked option sanity checks' '
> +       read_tree_u_must_fail --reset --protect-untracked HEAD &&
> +       read_tree_u_must_fail --reset --no-protect-untracked HEAD &&
> +       read_tree_u_must_fail -m -u --protect-untracked HEAD &&
> +       read_tree_u_must_fail -m -u --no-protect-untracked
> +'
> +
> +test_expect_success 'reset should reset worktree' '
> +       echo changed >df &&
>         read_tree_u_must_succeed -u --reset HEAD^ &&
>         git ls-files >actual &&
>         test_cmp expect actual
>  '
>
> +test_expect_success 'reset --protect-untracked protects untracked file' '
> +       echo changed >new &&
> +       read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
> +       echo "error: Untracked working tree file '\'new\'' would be overwritten by merge." >expected-err &&
> +       test_cmp expected-err actual-err
> +'
> +
> +test_expect_success 'reset --protect-untracked protects untracked directory' '
> +       rm new &&
> +       mkdir new &&
> +       echo untracked >new/untracked &&
> +       read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
> +       echo "error: Updating '\'new\'' would lose untracked files in it" >expected-err &&
> +       test_cmp expected-err actual-err
> +'
> +
> +test_expect_success 'reset --protect-untracked resets' '
> +       rm -rf new &&
> +       echo changed >df/file &&
> +       read_tree_u_must_succeed -u --reset --protect-untracked HEAD &&
> +       git ls-files >actual-two &&
> +       test_cmp expect-two actual-two
> +'
> +
>  test_expect_success 'reset should remove remnants from a failed merge' '
>         read_tree_u_must_succeed --reset -u HEAD &&
>         git ls-files -s >expect &&
> diff --git a/unpack-trees.c b/unpack-trees.c
> index 50189909b8..b1722730fe 100644
> --- a/unpack-trees.c
> +++ b/unpack-trees.c
> @@ -1917,7 +1917,8 @@ static int verify_absent_1(const struct cache_entry *ce,
>         int len;
>         struct stat st;
>
> -       if (o->index_only || o->reset || !o->update)
> +       if (o->index_only || o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED ||
> +           !o->update)
>                 return 0;
>
>         len = check_leading_path(ce->name, ce_namelen(ce));
> diff --git a/unpack-trees.h b/unpack-trees.h
> index d344d7d296..732b262c4d 100644
> --- a/unpack-trees.h
> +++ b/unpack-trees.h
> @@ -41,9 +41,15 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
>   */
>  void clear_unpack_trees_porcelain(struct unpack_trees_options *opts);
>
> +enum unpack_trees_reset_type {
> +       UNPACK_NO_RESET = 0,
> +       UNPACK_RESET_OVERWRITE_UNTRACKED,
> +       UNPACK_RESET_PROTECT_UNTRACKED
> +};
> +
>  struct unpack_trees_options {
> -       unsigned int reset,
> -                    merge,
> +       enum unpack_trees_reset_type reset;
> +       unsigned int merge,
>                      update,
>                      clone,
>                      index_only,
> --
> 2.21.0
>
Phillip Wood May 3, 2019, 3:20 p.m. UTC | #4
On 02/05/2019 11:38, Duy Nguyen wrote:
> On Wed, May 1, 2019 at 5:14 PM Phillip Wood <phillip.wood123@gmail.com> wrote:
>>
>> From: Phillip Wood <phillip.wood@dunelm.org.uk>
>>
>> Currently there is no way to get git to discard changes to the
>> worktree without overwriting untracked files. `reset --hard`,
>> `checkout --force`, `checkout <rev> :/` and `read-tree --reset -u`
> 
> "checkout <rev> :/" does not use unpack-trees, I think, so it does not
> belong to this group.

You're right that it does not use unpack-trees, but I'm making the point 
that there is no way to discard changes to tracked files without 
overwriting untracked files whatever one uses.

>> will all overwrite untracked files. unpack_trees() has known how to
>> avoid overwriting untracked files since commit fcc387db9b ("read-tree
>> -m -u: do not overwrite or remove untracked working tree files.",
>> 2006-05-17) in response to concerns about lost files [1] but those
>> protections do not apply to --reset. This patch adds an
>> --protect-untracked option for read-tree/unpack_trees() to apply th
>> same protections with --reset.
> 
> Your enum makes me wonder if we should have --reset-tracked instead of
> --protect-untracked (say what you want to reset seems tiny bit easier
> to understand than "reset except untracked"). Which leads to another
> question, do we ever need --reset-untracked (i.e. overwriting
> untracked files but never ever touch tracked ones).
> 
> I have one use case that --reset-untracked might make sense. Suppose
> you do "git reset HEAD^" where HEAD has some new files. The new files
> will be left alone in worktree of course, but switching back to HEAD
> after that, unpack-trees will cry murder because it would otherwise
> overwrite untracked files (that have the exact same content).
> 
> --reset-untracked might be useful for that. I'm carrying a patch to
> detect identical file content though which addresses this very
> specific case. Maybe it's the only case that --reset-untracked may be
> useful.

That patch sounds useful (I played around with "hg update" the other day 
and I think it does the same). I think that's probably a common case, it 
also works if one does "git checkout <other-branch> file" then "git 
reset HEAD" followed "git checkout <other-branch>". --reset-untracked is 
just like --reset which we can't get rid of without breaking backwards 
compatibility. Having --reset-tracked is maybe better than having a 
--protect-tracked option that only works with --reset -u.

>> It does not change the behavior of any
>> of the porcelain commands but paves the way for adding options or
>> changing the default for those in future.
>>
>> Note the actual change in unpack-trees.c is quite simple, the majority
>> of this patch is converting existing callers to use the new
>> unpack_trees_reset_type enum.
> 
> This could be split in a separate patch, then the actual change would
> become small and more to the point.

Yes that would make it clearer.

Best Wishes

Phillip

> 
>> [1] https://public-inbox.org/git/8aa486160605161500m1dd8428cj@mail.gmail.com/
>>
>> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
>> ---
>>
>> Notes:
>>      adding --protect-untracked to the invocation of
>>      test_submodule_forced_switch() in t1013 fixes the known breakage of
>>      tests 57 & 58 but breaks test 64 which relies on forcing. I'm not sure
>>      that the expected results of 57 & 58 are correct if we're forcing.
>>
>>   Documentation/git-read-tree.txt | 10 +++++++--
>>   builtin/am.c                    |  8 ++++---
>>   builtin/checkout.c              |  2 +-
>>   builtin/read-tree.c             | 12 +++++++++++
>>   builtin/rebase.c                |  2 +-
>>   builtin/reset.c                 |  2 +-
>>   builtin/stash.c                 |  7 ++++---
>>   t/lib-read-tree.sh              | 11 ++++++++++
>>   t/t1005-read-tree-reset.sh      | 37 +++++++++++++++++++++++++++++++--
>>   unpack-trees.c                  |  3 ++-
>>   unpack-trees.h                  | 10 +++++++--
>>   11 files changed, 88 insertions(+), 16 deletions(-)
>>
>> diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt
>> index d271842608..67864c6bbc 100644
>> --- a/Documentation/git-read-tree.txt
>> +++ b/Documentation/git-read-tree.txt
>> @@ -40,12 +40,18 @@ OPTIONS
>>   --reset::
>>          Same as -m, except that unmerged entries are discarded instead
>>          of failing. When used with `-u`, updates leading to loss of
>> -       working tree changes will not abort the operation.
>> +       working tree changes will not abort the operation and
>> +       untracked files will be overwritten. Use `--protect-untracked`
>> +       to avoid overwriting untracked files.
>>
>>   -u::
>>          After a successful merge, update the files in the work
>>          tree with the result of the merge.
>>
>> +--protect-untracked::
>> +       Prevent `--reset` `-u` from overwriting untracked files. Requires
>> +       `--reset` and `-u` (`-m` never overwrites untracked files).
>> +
>>   -i::
>>          Usually a merge requires the index file as well as the
>>          files in the working tree to be up to date with the
>> @@ -89,7 +95,7 @@ OPTIONS
>>          existed in the original index file.
>>
>>   --exclude-per-directory=<gitignore>::
>> -       When running the command with `-u` and `-m` options, the
>> +       When using `-u` with `-m` or `--reset` `--protect-untracked` 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
>> diff --git a/builtin/am.c b/builtin/am.c
>> index 912d9821b1..a92394b682 100644
>> --- a/builtin/am.c
>> +++ b/builtin/am.c
>> @@ -1854,7 +1854,8 @@ static void am_resolve(struct am_state *state)
>>    * true, any unmerged entries will be discarded. Returns 0 on success, -1 on
>>    * failure.
>>    */
>> -static int fast_forward_to(struct tree *head, struct tree *remote, int reset)
>> +static int fast_forward_to(struct tree *head, struct tree *remote,
>> +                          enum unpack_trees_reset_type reset)
>>   {
>>          struct lock_file lock_file = LOCK_INIT;
>>          struct unpack_trees_options opts;
>> @@ -1942,7 +1943,8 @@ static int clean_index(const struct object_id *head, const struct object_id *rem
>>
>>          read_cache_unmerged();
>>
>> -       if (fast_forward_to(head_tree, head_tree, 1))
>> +       if (fast_forward_to(head_tree, head_tree,
>> +                           UNPACK_RESET_OVERWRITE_UNTRACKED))
>>                  return -1;
>>
>>          if (write_cache_as_tree(&index, 0, NULL))
>> @@ -1952,7 +1954,7 @@ static int clean_index(const struct object_id *head, const struct object_id *rem
>>          if (!index_tree)
>>                  return error(_("Could not parse object '%s'."), oid_to_hex(&index));
>>
>> -       if (fast_forward_to(index_tree, remote_tree, 0))
>> +       if (fast_forward_to(index_tree, remote_tree, UNPACK_NO_RESET))
>>                  return -1;
>>
>>          if (merge_tree(remote_tree))
>> diff --git a/builtin/checkout.c b/builtin/checkout.c
>> index ffa776c6e1..e9e70018f9 100644
>> --- a/builtin/checkout.c
>> +++ b/builtin/checkout.c
>> @@ -506,7 +506,7 @@ static int reset_tree(struct tree *tree, const struct checkout_opts *o,
>>          opts.head_idx = -1;
>>          opts.update = worktree;
>>          opts.skip_unmerged = !worktree;
>> -       opts.reset = 1;
>> +       opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
>>          opts.merge = 1;
>>          opts.fn = oneway_merge;
>>          opts.verbose_update = o->show_progress;
>> diff --git a/builtin/read-tree.c b/builtin/read-tree.c
>> index 5c9c082595..23735adde9 100644
>> --- a/builtin/read-tree.c
>> +++ b/builtin/read-tree.c
>> @@ -114,6 +114,7 @@ static int git_read_tree_config(const char *var, const char *value, void *cb)
>>   int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
>>   {
>>          int i, stage = 0;
>> +       int protect_untracked = -1;
>>          struct object_id oid;
>>          struct tree_desc t[MAX_UNPACK_TREES];
>>          struct unpack_trees_options opts;
>> @@ -140,6 +141,8 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
>>                    PARSE_OPT_NONEG },
>>                  OPT_BOOL('u', NULL, &opts.update,
>>                           N_("update working tree with merge result")),
>> +               OPT_BOOL(0, "protect-untracked", &protect_untracked,
>> +                        N_("do not overwrite untracked files")),
>>                  { OPTION_CALLBACK, 0, "exclude-per-directory", &opts,
>>                    N_("gitignore"),
>>                    N_("allow explicitly ignored files to be overwritten"),
>> @@ -209,8 +212,17 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
>>          if ((opts.update || opts.index_only) && !opts.merge)
>>                  die("%s is meaningless without -m, --reset, or --prefix",
>>                      opts.update ? "-u" : "-i");
>> +       if (protect_untracked >= 0) {
>> +               if (!opts.reset || !opts.update)
>> +                       die("-%s-protect-untracked requires --reset and -u",
>> +                           protect_untracked ? "" : "-no");
>> +               opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
>> +       }
>>          if ((opts.dir && !opts.update))
>>                  die("--exclude-per-directory is meaningless unless -u");
>> +       if (opts.dir && opts.reset == UNPACK_RESET_OVERWRITE_UNTRACKED)
>> +               warning("--exclude-per-directory without --preserve-untracked "
>> +                       "has no effect");
>>          if (opts.merge && !opts.index_only)
>>                  setup_work_tree();
>>
>> diff --git a/builtin/rebase.c b/builtin/rebase.c
>> index 2e41ad5644..feb30a71f5 100644
>> --- a/builtin/rebase.c
>> +++ b/builtin/rebase.c
>> @@ -398,7 +398,7 @@ static int reset_head(struct object_id *oid, const char *action,
>>          unpack_tree_opts.update = 1;
>>          unpack_tree_opts.merge = 1;
>>          if (!detach_head)
>> -               unpack_tree_opts.reset = 1;
>> +               unpack_tree_opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
>>
>>          if (repo_read_index_unmerged(the_repository) < 0) {
>>                  ret = error(_("could not read index"));
>> diff --git a/builtin/reset.c b/builtin/reset.c
>> index 26ef9a7bd0..a39dd92fb2 100644
>> --- a/builtin/reset.c
>> +++ b/builtin/reset.c
>> @@ -70,7 +70,7 @@ static int reset_index(const struct object_id *oid, int reset_type, int quiet)
>>                  opts.update = 1;
>>                  /* fallthrough */
>>          default:
>> -               opts.reset = 1;
>> +               opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
>>          }
>>
>>          read_cache_unmerged();
>> diff --git a/builtin/stash.c b/builtin/stash.c
>> index 2a8e6d09b4..175d1b62d3 100644
>> --- a/builtin/stash.c
>> +++ b/builtin/stash.c
>> @@ -227,7 +227,8 @@ static int clear_stash(int argc, const char **argv, const char *prefix)
>>          return do_clear_stash();
>>   }
>>
>> -static int reset_tree(struct object_id *i_tree, int update, int reset)
>> +static int reset_tree(struct object_id *i_tree, int update,
>> +                     enum unpack_trees_reset_type reset)
>>   {
>>          int nr_trees = 1;
>>          struct unpack_trees_options opts;
>> @@ -461,7 +462,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
>>          }
>>
>>          if (has_index) {
>> -               if (reset_tree(&index_tree, 0, 0))
>> +               if (reset_tree(&index_tree, 0, UNPACK_NO_RESET))
>>                          return -1;
>>          } else {
>>                  struct strbuf out = STRBUF_INIT;
>> @@ -471,7 +472,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
>>                          return -1;
>>                  }
>>
>> -               if (reset_tree(&c_tree, 0, 1)) {
>> +               if (reset_tree(&c_tree, 0, UNPACK_RESET_OVERWRITE_UNTRACKED)) {
>>                          strbuf_release(&out);
>>                          return -1;
>>                  }
>> diff --git a/t/lib-read-tree.sh b/t/lib-read-tree.sh
>> index b95f485606..22f516d91f 100644
>> --- a/t/lib-read-tree.sh
>> +++ b/t/lib-read-tree.sh
>> @@ -39,3 +39,14 @@ read_tree_u_must_fail () {
>>          test_cmp pre-dry-run-wt post-dry-run-wt &&
>>          test_must_fail git read-tree "$@"
>>   }
>> +
>> +read_tree_u_must_fail_save_err () {
>> +       git ls-files -s >pre-dry-run &&
>> +       git diff-files -p >pre-dry-run-wt &&
>> +       test_must_fail git read-tree -n "$@" &&
>> +       git ls-files -s >post-dry-run &&
>> +       git diff-files -p >post-dry-run-wt &&
>> +       test_cmp pre-dry-run post-dry-run &&
>> +       test_cmp pre-dry-run-wt post-dry-run-wt &&
>> +       test_must_fail git read-tree "$@" 2>actual-err
>> +}
>> diff --git a/t/t1005-read-tree-reset.sh b/t/t1005-read-tree-reset.sh
>> index 83b09e1310..6c9dd6805b 100755
>> --- a/t/t1005-read-tree-reset.sh
>> +++ b/t/t1005-read-tree-reset.sh
>> @@ -19,15 +19,48 @@ test_expect_success 'setup' '
>>          git add df &&
>>          echo content >new &&
>>          git add new &&
>> -       git commit -m two
>> +       git commit -m two &&
>> +       git ls-files >expect-two
>>   '
>>
>> -test_expect_success 'reset should work' '
>> +test_expect_success '--protect-untracked option sanity checks' '
>> +       read_tree_u_must_fail --reset --protect-untracked HEAD &&
>> +       read_tree_u_must_fail --reset --no-protect-untracked HEAD &&
>> +       read_tree_u_must_fail -m -u --protect-untracked HEAD &&
>> +       read_tree_u_must_fail -m -u --no-protect-untracked
>> +'
>> +
>> +test_expect_success 'reset should reset worktree' '
>> +       echo changed >df &&
>>          read_tree_u_must_succeed -u --reset HEAD^ &&
>>          git ls-files >actual &&
>>          test_cmp expect actual
>>   '
>>
>> +test_expect_success 'reset --protect-untracked protects untracked file' '
>> +       echo changed >new &&
>> +       read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
>> +       echo "error: Untracked working tree file '\'new\'' would be overwritten by merge." >expected-err &&
>> +       test_cmp expected-err actual-err
>> +'
>> +
>> +test_expect_success 'reset --protect-untracked protects untracked directory' '
>> +       rm new &&
>> +       mkdir new &&
>> +       echo untracked >new/untracked &&
>> +       read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
>> +       echo "error: Updating '\'new\'' would lose untracked files in it" >expected-err &&
>> +       test_cmp expected-err actual-err
>> +'
>> +
>> +test_expect_success 'reset --protect-untracked resets' '
>> +       rm -rf new &&
>> +       echo changed >df/file &&
>> +       read_tree_u_must_succeed -u --reset --protect-untracked HEAD &&
>> +       git ls-files >actual-two &&
>> +       test_cmp expect-two actual-two
>> +'
>> +
>>   test_expect_success 'reset should remove remnants from a failed merge' '
>>          read_tree_u_must_succeed --reset -u HEAD &&
>>          git ls-files -s >expect &&
>> diff --git a/unpack-trees.c b/unpack-trees.c
>> index 50189909b8..b1722730fe 100644
>> --- a/unpack-trees.c
>> +++ b/unpack-trees.c
>> @@ -1917,7 +1917,8 @@ static int verify_absent_1(const struct cache_entry *ce,
>>          int len;
>>          struct stat st;
>>
>> -       if (o->index_only || o->reset || !o->update)
>> +       if (o->index_only || o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED ||
>> +           !o->update)
>>                  return 0;
>>
>>          len = check_leading_path(ce->name, ce_namelen(ce));
>> diff --git a/unpack-trees.h b/unpack-trees.h
>> index d344d7d296..732b262c4d 100644
>> --- a/unpack-trees.h
>> +++ b/unpack-trees.h
>> @@ -41,9 +41,15 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
>>    */
>>   void clear_unpack_trees_porcelain(struct unpack_trees_options *opts);
>>
>> +enum unpack_trees_reset_type {
>> +       UNPACK_NO_RESET = 0,
>> +       UNPACK_RESET_OVERWRITE_UNTRACKED,
>> +       UNPACK_RESET_PROTECT_UNTRACKED
>> +};
>> +
>>   struct unpack_trees_options {
>> -       unsigned int reset,
>> -                    merge,
>> +       enum unpack_trees_reset_type reset;
>> +       unsigned int merge,
>>                       update,
>>                       clone,
>>                       index_only,
>> --
>> 2.21.0
>>
> 
>

Patch
diff mbox series

diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt
index d271842608..67864c6bbc 100644
--- a/Documentation/git-read-tree.txt
+++ b/Documentation/git-read-tree.txt
@@ -40,12 +40,18 @@  OPTIONS
 --reset::
 	Same as -m, except that unmerged entries are discarded instead
 	of failing. When used with `-u`, updates leading to loss of
-	working tree changes will not abort the operation.
+	working tree changes will not abort the operation and
+	untracked files will be overwritten. Use `--protect-untracked`
+	to avoid overwriting untracked files.
 
 -u::
 	After a successful merge, update the files in the work
 	tree with the result of the merge.
 
+--protect-untracked::
+	Prevent `--reset` `-u` from overwriting untracked files. Requires
+	`--reset` and `-u` (`-m` never overwrites untracked files).
+
 -i::
 	Usually a merge requires the index file as well as the
 	files in the working tree to be up to date with the
@@ -89,7 +95,7 @@  OPTIONS
 	existed in the original index file.
 
 --exclude-per-directory=<gitignore>::
-	When running the command with `-u` and `-m` options, the
+	When using `-u` with `-m` or `--reset` `--protect-untracked` 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
diff --git a/builtin/am.c b/builtin/am.c
index 912d9821b1..a92394b682 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -1854,7 +1854,8 @@  static void am_resolve(struct am_state *state)
  * true, any unmerged entries will be discarded. Returns 0 on success, -1 on
  * failure.
  */
-static int fast_forward_to(struct tree *head, struct tree *remote, int reset)
+static int fast_forward_to(struct tree *head, struct tree *remote,
+			   enum unpack_trees_reset_type reset)
 {
 	struct lock_file lock_file = LOCK_INIT;
 	struct unpack_trees_options opts;
@@ -1942,7 +1943,8 @@  static int clean_index(const struct object_id *head, const struct object_id *rem
 
 	read_cache_unmerged();
 
-	if (fast_forward_to(head_tree, head_tree, 1))
+	if (fast_forward_to(head_tree, head_tree,
+			    UNPACK_RESET_OVERWRITE_UNTRACKED))
 		return -1;
 
 	if (write_cache_as_tree(&index, 0, NULL))
@@ -1952,7 +1954,7 @@  static int clean_index(const struct object_id *head, const struct object_id *rem
 	if (!index_tree)
 		return error(_("Could not parse object '%s'."), oid_to_hex(&index));
 
-	if (fast_forward_to(index_tree, remote_tree, 0))
+	if (fast_forward_to(index_tree, remote_tree, UNPACK_NO_RESET))
 		return -1;
 
 	if (merge_tree(remote_tree))
diff --git a/builtin/checkout.c b/builtin/checkout.c
index ffa776c6e1..e9e70018f9 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -506,7 +506,7 @@  static int reset_tree(struct tree *tree, const struct checkout_opts *o,
 	opts.head_idx = -1;
 	opts.update = worktree;
 	opts.skip_unmerged = !worktree;
-	opts.reset = 1;
+	opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
 	opts.merge = 1;
 	opts.fn = oneway_merge;
 	opts.verbose_update = o->show_progress;
diff --git a/builtin/read-tree.c b/builtin/read-tree.c
index 5c9c082595..23735adde9 100644
--- a/builtin/read-tree.c
+++ b/builtin/read-tree.c
@@ -114,6 +114,7 @@  static int git_read_tree_config(const char *var, const char *value, void *cb)
 int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
 {
 	int i, stage = 0;
+	int protect_untracked = -1;
 	struct object_id oid;
 	struct tree_desc t[MAX_UNPACK_TREES];
 	struct unpack_trees_options opts;
@@ -140,6 +141,8 @@  int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
 		  PARSE_OPT_NONEG },
 		OPT_BOOL('u', NULL, &opts.update,
 			 N_("update working tree with merge result")),
+		OPT_BOOL(0, "protect-untracked", &protect_untracked,
+			 N_("do not overwrite untracked files")),
 		{ OPTION_CALLBACK, 0, "exclude-per-directory", &opts,
 		  N_("gitignore"),
 		  N_("allow explicitly ignored files to be overwritten"),
@@ -209,8 +212,17 @@  int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
 	if ((opts.update || opts.index_only) && !opts.merge)
 		die("%s is meaningless without -m, --reset, or --prefix",
 		    opts.update ? "-u" : "-i");
+	if (protect_untracked >= 0) {
+		if (!opts.reset || !opts.update)
+			die("-%s-protect-untracked requires --reset and -u",
+			    protect_untracked ? "" : "-no");
+		opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
+	}
 	if ((opts.dir && !opts.update))
 		die("--exclude-per-directory is meaningless unless -u");
+	if (opts.dir && opts.reset == UNPACK_RESET_OVERWRITE_UNTRACKED)
+		warning("--exclude-per-directory without --preserve-untracked "
+			"has no effect");
 	if (opts.merge && !opts.index_only)
 		setup_work_tree();
 
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 2e41ad5644..feb30a71f5 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -398,7 +398,7 @@  static int reset_head(struct object_id *oid, const char *action,
 	unpack_tree_opts.update = 1;
 	unpack_tree_opts.merge = 1;
 	if (!detach_head)
-		unpack_tree_opts.reset = 1;
+		unpack_tree_opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
 
 	if (repo_read_index_unmerged(the_repository) < 0) {
 		ret = error(_("could not read index"));
diff --git a/builtin/reset.c b/builtin/reset.c
index 26ef9a7bd0..a39dd92fb2 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -70,7 +70,7 @@  static int reset_index(const struct object_id *oid, int reset_type, int quiet)
 		opts.update = 1;
 		/* fallthrough */
 	default:
-		opts.reset = 1;
+		opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
 	}
 
 	read_cache_unmerged();
diff --git a/builtin/stash.c b/builtin/stash.c
index 2a8e6d09b4..175d1b62d3 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -227,7 +227,8 @@  static int clear_stash(int argc, const char **argv, const char *prefix)
 	return do_clear_stash();
 }
 
-static int reset_tree(struct object_id *i_tree, int update, int reset)
+static int reset_tree(struct object_id *i_tree, int update,
+		      enum unpack_trees_reset_type reset)
 {
 	int nr_trees = 1;
 	struct unpack_trees_options opts;
@@ -461,7 +462,7 @@  static int do_apply_stash(const char *prefix, struct stash_info *info,
 	}
 
 	if (has_index) {
-		if (reset_tree(&index_tree, 0, 0))
+		if (reset_tree(&index_tree, 0, UNPACK_NO_RESET))
 			return -1;
 	} else {
 		struct strbuf out = STRBUF_INIT;
@@ -471,7 +472,7 @@  static int do_apply_stash(const char *prefix, struct stash_info *info,
 			return -1;
 		}
 
-		if (reset_tree(&c_tree, 0, 1)) {
+		if (reset_tree(&c_tree, 0, UNPACK_RESET_OVERWRITE_UNTRACKED)) {
 			strbuf_release(&out);
 			return -1;
 		}
diff --git a/t/lib-read-tree.sh b/t/lib-read-tree.sh
index b95f485606..22f516d91f 100644
--- a/t/lib-read-tree.sh
+++ b/t/lib-read-tree.sh
@@ -39,3 +39,14 @@  read_tree_u_must_fail () {
 	test_cmp pre-dry-run-wt post-dry-run-wt &&
 	test_must_fail git read-tree "$@"
 }
+
+read_tree_u_must_fail_save_err () {
+	git ls-files -s >pre-dry-run &&
+	git diff-files -p >pre-dry-run-wt &&
+	test_must_fail git read-tree -n "$@" &&
+	git ls-files -s >post-dry-run &&
+	git diff-files -p >post-dry-run-wt &&
+	test_cmp pre-dry-run post-dry-run &&
+	test_cmp pre-dry-run-wt post-dry-run-wt &&
+	test_must_fail git read-tree "$@" 2>actual-err
+}
diff --git a/t/t1005-read-tree-reset.sh b/t/t1005-read-tree-reset.sh
index 83b09e1310..6c9dd6805b 100755
--- a/t/t1005-read-tree-reset.sh
+++ b/t/t1005-read-tree-reset.sh
@@ -19,15 +19,48 @@  test_expect_success 'setup' '
 	git add df &&
 	echo content >new &&
 	git add new &&
-	git commit -m two
+	git commit -m two &&
+	git ls-files >expect-two
 '
 
-test_expect_success 'reset should work' '
+test_expect_success '--protect-untracked option sanity checks' '
+	read_tree_u_must_fail --reset --protect-untracked HEAD &&
+	read_tree_u_must_fail --reset --no-protect-untracked HEAD &&
+	read_tree_u_must_fail -m -u --protect-untracked HEAD &&
+	read_tree_u_must_fail -m -u --no-protect-untracked
+'
+
+test_expect_success 'reset should reset worktree' '
+	echo changed >df &&
 	read_tree_u_must_succeed -u --reset HEAD^ &&
 	git ls-files >actual &&
 	test_cmp expect actual
 '
 
+test_expect_success 'reset --protect-untracked protects untracked file' '
+	echo changed >new &&
+	read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
+	echo "error: Untracked working tree file '\'new\'' would be overwritten by merge." >expected-err &&
+	test_cmp expected-err actual-err
+'
+
+test_expect_success 'reset --protect-untracked protects untracked directory' '
+	rm new &&
+	mkdir new &&
+	echo untracked >new/untracked &&
+	read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
+	echo "error: Updating '\'new\'' would lose untracked files in it" >expected-err &&
+	test_cmp expected-err actual-err
+'
+
+test_expect_success 'reset --protect-untracked resets' '
+	rm -rf new &&
+	echo changed >df/file &&
+	read_tree_u_must_succeed -u --reset --protect-untracked HEAD &&
+	git ls-files >actual-two &&
+	test_cmp expect-two actual-two
+'
+
 test_expect_success 'reset should remove remnants from a failed merge' '
 	read_tree_u_must_succeed --reset -u HEAD &&
 	git ls-files -s >expect &&
diff --git a/unpack-trees.c b/unpack-trees.c
index 50189909b8..b1722730fe 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -1917,7 +1917,8 @@  static int verify_absent_1(const struct cache_entry *ce,
 	int len;
 	struct stat st;
 
-	if (o->index_only || o->reset || !o->update)
+	if (o->index_only || o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED ||
+	    !o->update)
 		return 0;
 
 	len = check_leading_path(ce->name, ce_namelen(ce));
diff --git a/unpack-trees.h b/unpack-trees.h
index d344d7d296..732b262c4d 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -41,9 +41,15 @@  void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
  */
 void clear_unpack_trees_porcelain(struct unpack_trees_options *opts);
 
+enum unpack_trees_reset_type {
+	UNPACK_NO_RESET = 0,
+	UNPACK_RESET_OVERWRITE_UNTRACKED,
+	UNPACK_RESET_PROTECT_UNTRACKED
+};
+
 struct unpack_trees_options {
-	unsigned int reset,
-		     merge,
+	enum unpack_trees_reset_type reset;
+	unsigned int merge,
 		     update,
 		     clone,
 		     index_only,