On Sun, Dec 9, 2018 at 12:05 PM Thomas Gummerer <t.gummerer@xxxxxxxxx> wrote: > > Add a new --cached option to git checkout, which works only on the > index, but not the working tree, similar to what 'git reset <tree-ish> > -- <pathspec>... does. Indeed the tests are adapted from the 'git > reset' tests. > > In the longer term the idea is to potentially deprecate 'git reset > <tree-ish> -- <pathspec>...', so the 'git reset' command becomes only > about re-pointing the HEAD, and not also about copying entries from > <tree-ish> to the index. > > Note that 'git checkout' by default works in overlay mode, meaning > files that match the pathspec that don't exist in <tree-ish>, but > exist in the index would not be removed. 'git checkout --no-overlay > --cached' can be used to get the same behaviour as 'git reset > <tree-ish> -- <pathspec>'. I think this argues _even more_ that --no-overlay should be the default. Your series is valuable even if we don't push on that, I'm just being noisy about what I think would be an even better world. Also, I don't think I've mentioned it yet, but I'm really excited about this series and what you're doing. It's super cool. (Which I expected when I saw the description of the desired behavior, but I'm also liking and contemplating re-using some code...) > One thing this patch doesn't currently deal with is conflicts. > Currently 'git checkout --{ours,theirs} -- <file-with-conflicts>' > doesn't do anything with the index, so the --cached option just > mirrors that behaviour. But given it doesn't even deal with > conflicts, the '--cached' option doesn't make much sense when no > <tree-ish> is given. As it operates only on the index, it's always a > no-op if no tree-ish is given. > > Signed-off-by: Thomas Gummerer <t.gummerer@xxxxxxxxx> > --- > > Maybe we can just disallow --cached without <tree-ish> given for now, > and possibly later allow it with some different behaviour for > conflicts, not sure what the best way forward here is. We can also > just make it update the index as appropriate, and have it behave > different than 'git checkout' curerntly does when handling conflicts? Huh? "git checkout -- <path>" means update <path> from the index, meaning the index is left alone (it's the source) and only the working tree is touched. When you add a flag named --cached to only update the index and not the working tree, then the index becomes the sole destination. Now we combine: no tree is specified means the index is the source of the writing, and --cached being specified means the index is the sole destination of the writing. Thus, you have a no-op. If the user specifies --cached and no tree, you should immediately exit with a message along the lines of "Nothing to do; no tree given and --cached specified." The presence of conflicts seems completely irrelevant to me here. > builtin/checkout.c | 26 ++++++++-- > t/t2016-checkout-patch.sh | 8 +++ > t/t2026-checkout-cached.sh | 103 +++++++++++++++++++++++++++++++++++++ > t/t9902-completion.sh | 1 + > 4 files changed, 135 insertions(+), 3 deletions(-) > create mode 100755 t/t2026-checkout-cached.sh > > diff --git a/builtin/checkout.c b/builtin/checkout.c > index 0aef35bbc4..6ba85e9de5 100644 > --- a/builtin/checkout.c > +++ b/builtin/checkout.c > @@ -45,6 +45,7 @@ struct checkout_opts { > int ignore_other_worktrees; > int show_progress; > int overlay_mode; > + int cached; > /* > * If new checkout options are added, skip_merge_working_tree > * should be updated accordingly. > @@ -288,6 +289,10 @@ static int checkout_paths(const struct checkout_opts *opts, > die(_("Cannot update paths and switch to branch '%s' at the same time."), > opts->new_branch); > > + if (opts->patch_mode && opts->cached) > + return run_add_interactive(revision, "--patch=reset", > + &opts->pathspec); > + > if (opts->patch_mode) > return run_add_interactive(revision, "--patch=checkout", > &opts->pathspec); > @@ -319,7 +324,9 @@ static int checkout_paths(const struct checkout_opts *opts, > * the current index, which means that it should > * be removed. > */ > - ce->ce_flags |= CE_MATCHED | CE_REMOVE | CE_WT_REMOVE; > + ce->ce_flags |= CE_MATCHED | CE_REMOVE; > + if (!opts->cached) > + ce->ce_flags |= CE_WT_REMOVE; > continue; > } else { > /* > @@ -392,6 +399,9 @@ static int checkout_paths(const struct checkout_opts *opts, > for (pos = 0; pos < active_nr; pos++) { > struct cache_entry *ce = active_cache[pos]; > if (ce->ce_flags & CE_MATCHED) { > + if (opts->cached) { > + continue; > + } > if (!ce_stage(ce)) { > errs |= checkout_entry(ce, &state, NULL); > continue; > @@ -571,6 +581,11 @@ static int skip_merge_working_tree(const struct checkout_opts *opts, > * not tested here > */ > > + /* > + * opts->cached cannot be used with switching branches so is > + * not tested here > + */ > + > /* > * If we aren't creating a new branch any changes or updates will > * happen in the existing branch. Since that could only be updating > @@ -1207,9 +1222,13 @@ static int checkout_branch(struct checkout_opts *opts, > die(_("'%s' cannot be used with switching branches"), > "--patch"); > > - if (!opts->overlay_mode) > + if (opts->overlay_mode != -1) > + die(_("'%s' cannot be used with switching branches"), > + "--overlay/--no-overlay"); > + > + if (opts->cached) > die(_("'%s' cannot be used with switching branches"), > - "--no-overlay"); > + "--cached"); > > if (opts->writeout_stage) > die(_("'%s' cannot be used with switching branches"), > @@ -1300,6 +1319,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) > PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater }, > OPT_BOOL(0, "progress", &opts.show_progress, N_("force progress reporting")), > OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode")), > + OPT_BOOL(0, "cached", &opts.cached, N_("work on the index only")), > OPT_END(), > }; > > diff --git a/t/t2016-checkout-patch.sh b/t/t2016-checkout-patch.sh > index 47aeb0b167..e8774046e0 100755 > --- a/t/t2016-checkout-patch.sh > +++ b/t/t2016-checkout-patch.sh > @@ -108,6 +108,14 @@ test_expect_success PERL 'path limiting works: foo inside dir' ' > verify_state dir/foo head head > ' > > +test_expect_success PERL 'git checkout --cached -p' ' > + set_and_save_state dir/foo work work && > + test_write_lines n y | git checkout --cached -p >output && > + verify_state dir/foo work head && > + verify_saved_state bar && > + test_i18ngrep "Unstage" output > +' > + > test_expect_success PERL 'none of this moved HEAD' ' > verify_saved_head > ' > diff --git a/t/t2026-checkout-cached.sh b/t/t2026-checkout-cached.sh > new file mode 100755 > index 0000000000..1b66192727 > --- /dev/null > +++ b/t/t2026-checkout-cached.sh > @@ -0,0 +1,103 @@ > +#!/bin/sh > + > +test_description='checkout --cached <pathspec>' > + > +. ./test-lib.sh > + > +test_expect_success 'checkout --cached <pathspec>' ' > + echo 1 >file1 && > + echo 2 >file2 && > + git add file1 file2 && > + test_tick && > + git commit -m files && > + git rm file2 && > + echo 3 >file3 && > + echo 4 >file1 && > + git add file1 file3 && > + git checkout --cached HEAD -- file1 file2 && > + test_must_fail git diff --quiet && > + > + cat >expect <<-\EOF && > + diff --git a/file1 b/file1 > + index d00491f..b8626c4 100644 > + --- a/file1 > + +++ b/file1 > + @@ -1 +1 @@ > + -1 > + +4 > + diff --git a/file2 b/file2 > + deleted file mode 100644 > + index 0cfbf08..0000000 > + --- a/file2 > + +++ /dev/null > + @@ -1 +0,0 @@ > + -2 > + EOF > + git diff >actual && > + test_cmp expect actual && > + > + cat >expect <<-\EOF && > + diff --git a/file3 b/file3 > + new file mode 100644 > + index 0000000..00750ed > + --- /dev/null > + +++ b/file3 > + @@ -0,0 +1 @@ > + +3 > + EOF > + git diff --cached >actual && > + test_cmp expect actual > +' > + > +test_expect_success 'checking out an unmodified path is a no-op' ' > + git reset --hard && > + git checkout --cached HEAD -- file1 && > + git diff-files --exit-code && > + git diff-index --cached --exit-code HEAD > +' > + > +test_expect_success 'checking out specific path that is unmerged' ' > + test_commit file3 file3 && > + git rm --cached file2 && > + echo 1234 >file2 && > + F1=$(git rev-parse HEAD:file1) && > + F2=$(git rev-parse HEAD:file2) && > + F3=$(git rev-parse HEAD:file3) && > + { > + echo "100644 $F1 1 file2" && > + echo "100644 $F2 2 file2" && > + echo "100644 $F3 3 file2" > + } | git update-index --index-info && > + git ls-files -u && > + git checkout --cached HEAD file2 && > + test_must_fail git diff --quiet && > + git diff-index --exit-code --cached HEAD > +' > + > +test_expect_success '--cached without --no-overlay does not remove entry from index' ' > + test_must_fail git checkout --cached HEAD^ file3 && > + git ls-files --error-unmatch -- file3 > +' > + > +test_expect_success 'file is removed from the index with --no-overlay' ' > + git checkout --cached --no-overlay HEAD^ file3 && > + test_path_is_file file3 && > + test_must_fail git ls-files --error-unmatch -- file3 > +' > + > +test_expect_success 'test checkout --cached --no-overlay at given paths' ' > + mkdir sub && > + >sub/file1 && > + >sub/file2 && > + git update-index --add sub/file1 sub/file2 && > + T=$(git write-tree) && > + git checkout --cached --no-overlay HEAD sub/file2 && > + test_must_fail git diff --quiet && > + U=$(git write-tree) && Do we need to worry at all about losing the exit status of write-tree in either invocation? In particular, if the second one for U fails somehow, we'd end up with $U being a blank string and we'd still probably get "$T" != "$U" below. You also had some rev-parse invocations hidden in a sub-shell in both this patch and patch 5, but subsequent commands relied on non-empty output out of those, so I figured those were fine. This one might be too, but I thought I'd at least mention it. > + echo "$T" && > + echo "$U" && > + test_must_fail git diff-index --cached --exit-code "$T" && > + test "$T" != "$U" > +' > + > +test_done > diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh > index a3fd9a9630..cbc304ace8 100755 > --- a/t/t9902-completion.sh > +++ b/t/t9902-completion.sh > @@ -1437,6 +1437,7 @@ test_expect_success 'double dash "git checkout"' ' > --no-quiet Z > --no-... Z > --overlay Z > + --cached Z > EOF > ' > > -- > 2.20.0.405.gbc1bbc6f85