On Wed, May 1, 2019 at 5:14 PM Phillip Wood <phillip.wood123@xxxxxxxxx> wrote: > > From: Phillip Wood <phillip.wood@xxxxxxxxxxxxx> > > 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@xxxxxxxxxxxxxx/ > > Signed-off-by: Phillip Wood <phillip.wood@xxxxxxxxxxxxx> > --- > > 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 > -- Duy