[v3,7/8] checkout: introduce --{,no-}overlay option
diff mbox series

Message ID 20190108215225.3077-8-t.gummerer@gmail.com
State New
Headers show
Series
  • introduce no-overlay mode in git checkout
Related show

Commit Message

Thomas Gummerer Jan. 8, 2019, 9:52 p.m. UTC
Currently 'git checkout' is defined as an overlay operation, which
means that if in 'git checkout <tree-ish> -- [<pathspec>]' we have an
entry in the index that matches <pathspec>, but that doesn't exist in
<tree-ish>, that entry will not be removed from the index or the
working tree.

Introduce a new --{,no-}overlay option, which allows using 'git
checkout' in non-overlay mode, thus removing files from the working
tree if they do not exist in <tree-ish> but match <pathspec>.

Note that 'git checkout -p <tree-ish> -- [<pathspec>]' already works
this way, so no changes are needed for the patch mode.  We disallow
'git checkout --overlay -p' to avoid confusing users who would expect
to be able to force overlay mode in 'git checkout -p' this way.

Untracked files are not affected by this change, so 'git checkout
--no-overlay HEAD -- untracked' will not remove untracked from the
working tree.  This is so e.g. 'git checkout --no-overlay HEAD -- dir/'
doesn't delete all untracked files in dir/, but rather just resets the
state of files that are known to git.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Thomas Gummerer <t.gummerer@gmail.com>
---
 Documentation/git-checkout.txt | 10 ++++++
 builtin/checkout.c             | 66 +++++++++++++++++++++++++++++-----
 t/t2025-checkout-no-overlay.sh | 47 ++++++++++++++++++++++++
 t/t9902-completion.sh          |  1 +
 4 files changed, 116 insertions(+), 8 deletions(-)
 create mode 100755 t/t2025-checkout-no-overlay.sh

Comments

Jonathan Nieder Jan. 22, 2019, 11:53 p.m. UTC | #1
Hi,

Thomas Gummerer wrote:

> Currently 'git checkout' is defined as an overlay operation, which
> means that if in 'git checkout <tree-ish> -- [<pathspec>]' we have an
> entry in the index that matches <pathspec>, but that doesn't exist in
> <tree-ish>, that entry will not be removed from the index or the
> working tree.
>
> Introduce a new --{,no-}overlay option, which allows using 'git
> checkout' in non-overlay mode, thus removing files from the working
> tree if they do not exist in <tree-ish> but match <pathspec>.

This patch just hit my workstation.  Some initial thoughts:

I had no idea what --overlay would mean and am still not clear on it.
Is this analogous to "git add --ignore-removal"?  If so, can we just
call it --ignore-removal?

Thank you thank you thank you for working on this.  I run into this
all the time and am super excited about the "default to
--no-ignore-removal" future.

I'm nervous about the config with no associated warning or plan for
phasing it out.  It means that scripts using "git checkout" don't
get a consistent behavior unless they explicitly pass this option,
which didn't exist in older versions of Git --- in other words,
scripts have no real good option.  Can we plan a transition to
making --no-ignore-removal the default, in multiple steps?  For
example:

 1. First introduce the commandline option, as in this series

 2. Next, change the default to warn whenever the difference would
    matter, printing a hint about how to configure to explicitly
    request the old or new behavior.

 3. After a release or two has passed so people get a chance
    to update their scripts, flip the default.

 4. Finally, remove the warning.

 5. Warn whenver the difference would matter when a user has
    requested the old behavior through config, in preparation
    for removing the config.

 6. Remove the config.

Steps 5 and 6 are optional but might be nice.

What do you think?

Thanks,
Jonathan
Junio C Hamano Jan. 23, 2019, 7:05 p.m. UTC | #2
Jonathan Nieder <jrnieder@gmail.com> writes:

> I'm nervous about the config with no associated warning or plan for
> phasing it out.

This was discussed long ago (in my panda-brain timescale) but my
recollection is to keep "checkout" default to the traditional
"overlay what was read from the tree on top of the current index"
behaviour, while the new "checkout-paths" subcommand (split from the
"checkout" subcommand to produce two subcommands, the other one
being the "checkout-branch" subcommand) would default to the new "no
overlay" behaviour.

So I am not sure if we even need a detailed transition plan.

If we were to make "checkout" pay attention to a local
configuration, that is a different story, as scripts that have
always assumed the overlay behaviour will be broken by such a
configuration variable.  But with the introduction of two new
subcommands in the picture to help interactive end users, I am not
sure if it is even worth considering to allow "checkout" to change
behaviour based on a configuration.  Those who want no-overlay
behaviour can switch to checkout-paths and be done with it, while
scripts can keep relying on the overlay behaviour, no?
Thomas Gummerer Jan. 23, 2019, 8:21 p.m. UTC | #3
On 01/22, Jonathan Nieder wrote:
> Hi,
> 
> Thomas Gummerer wrote:
> 
> > Currently 'git checkout' is defined as an overlay operation, which
> > means that if in 'git checkout <tree-ish> -- [<pathspec>]' we have an
> > entry in the index that matches <pathspec>, but that doesn't exist in
> > <tree-ish>, that entry will not be removed from the index or the
> > working tree.
> >
> > Introduce a new --{,no-}overlay option, which allows using 'git
> > checkout' in non-overlay mode, thus removing files from the working
> > tree if they do not exist in <tree-ish> but match <pathspec>.
> 
> This patch just hit my workstation.  Some initial thoughts:
> 
> I had no idea what --overlay would mean and am still not clear on it.
> Is this analogous to "git add --ignore-removal"?  If so, can we just
> call it --ignore-removal?

Yes, it seems like they are very similar.  I'm happy to rename the
option.  The topic seems to have made it to 'next' already, so I'll
submit the patches on top, unless reverting the topic out of next and
replacing it is preferred?

> Thank you thank you thank you for working on this.  I run into this
> all the time and am super excited about the "default to
> --no-ignore-removal" future.

:)

> I'm nervous about the config with no associated warning or plan for
> phasing it out.  It means that scripts using "git checkout" don't
> get a consistent behavior unless they explicitly pass this option,
> which didn't exist in older versions of Git --- in other words,
> scripts have no real good option.  Can we plan a transition to
> making --no-ignore-removal the default, in multiple steps?  For
> example:

As Junio mentioned, the plan was to just have this mode default when
we introduce the new checkout-paths command.

As checkout is a porcelain command, I had hoped it would be okay to
also have this as a configuration option, for the time before
'checkout-paths' exists and while I'm getting used to actually typing
'checkout-paths' instead of 'checkout'.  However I get that there may
be scripts that are using git checkout, and expect the previous
behaviour, so I'm also okay with dropping the config option for now.

If we still want to make this the default even after 'checkout-paths'
exists, the plan you outline below sounds good to me, though maybe we
can make the "flip the default" step once we decide to release git
3.0.

>  1. First introduce the commandline option, as in this series
> 
>  2. Next, change the default to warn whenever the difference would
>     matter, printing a hint about how to configure to explicitly
>     request the old or new behavior.
> 
>  3. After a release or two has passed so people get a chance
>     to update their scripts, flip the default.
> 
>  4. Finally, remove the warning.
> 
>  5. Warn whenver the difference would matter when a user has
>     requested the old behavior through config, in preparation
>     for removing the config.
> 
>  6. Remove the config.
> 
> Steps 5 and 6 are optional but might be nice.
> 
> What do you think?
> 
> Thanks,
> Jonathan
Jonathan Nieder Jan. 23, 2019, 8:47 p.m. UTC | #4
Thomas Gummerer wrote:
> On 01/22, Jonathan Nieder wrote:

>> I had no idea what --overlay would mean and am still not clear on it.
>> Is this analogous to "git add --ignore-removal"?  If so, can we just
>> call it --ignore-removal?
>
> Yes, it seems like they are very similar.  I'm happy to rename the
> option.  The topic seems to have made it to 'next' already, so I'll
> submit the patches on top, unless reverting the topic out of next and
> replacing it is preferred?

A patch on top sounds good.

[...]
>> I'm nervous about the config with no associated warning or plan for
>> phasing it out.  It means that scripts using "git checkout" don't
>> get a consistent behavior unless they explicitly pass this option,
>> which didn't exist in older versions of Git --- in other words,
>> scripts have no real good option.  Can we plan a transition to
>> making --no-ignore-removal the default, in multiple steps?  For
>> example:
>
> As Junio mentioned, the plan was to just have this mode default when
> we introduce the new checkout-paths command.
>
> As checkout is a porcelain command, I had hoped it would be okay to
> also have this as a configuration option, for the time before
> 'checkout-paths' exists and while I'm getting used to actually typing
> 'checkout-paths' instead of 'checkout'.  However I get that there may
> be scripts that are using git checkout, and expect the previous
> behaviour, so I'm also okay with dropping the config option for now.

Yes, if we have no plan for flipping the default later, then I would
prefer to eliminate the config option.  Scripts very frequently use
human-facing commands like "git checkout" when they want the command
to produce (unparsable) friendly output to show to humans, and I don't
think we've provided a good alternative for that use case.

> If we still want to make this the default even after 'checkout-paths'
> exists, the plan you outline below sounds good to me, though maybe we
> can make the "flip the default" step once we decide to release git
> 3.0.

I would really like this, so I might write a series for it.  Please
don't wait for me, though --- feel free to send any patches you're
thinking about and we can work together or I can just appreciate your
work. ;-)

Sincerely,
Jonathan
Junio C Hamano Jan. 23, 2019, 9:08 p.m. UTC | #5
Thomas Gummerer <t.gummerer@gmail.com> writes:

>> I had no idea what --overlay would mean and am still not clear on it.
>> Is this analogous to "git add --ignore-removal"?  If so, can we just
>> call it --ignore-removal?
>
> Yes, it seems like they are very similar.

Hmm, I am not sure if the word "removal" makes sense in the context
of "checkout", as "removal" is an _action_ just like "checking out"
itself is, and not a _state_.  You'd check out a state out of a tree
to the index and the working tree, so "checking out absence of a
path" may make sense, though, as "absence of a path" is a state
recorded in that source tree object.

The word "removal" makes little sense in "git add --ignore-removal",
but it and "git add --no-all" outlived their usefulness already, so
it may not be worth _fixing_ it.  But I am mildly opposed to spread
the earlier mistake to a new option.
Jonathan Nieder Jan. 24, 2019, 1:12 a.m. UTC | #6
Hi,

Junio C Hamano wrote:
> Thomas Gummerer <t.gummerer@gmail.com> writes:
>> Jonathan Nieder wrote:

>>> Is this analogous to "git add --ignore-removal"?  If so, can we just
>>> call it --ignore-removal?
>>
>> Yes, it seems like they are very similar.
>
> Hmm, I am not sure if the word "removal" makes sense in the context
> of "checkout", as "removal" is an _action_ just like "checking out"
> itself is, and not a _state_.  You'd check out a state out of a tree
> to the index and the working tree, so "checking out absence of a
> path" may make sense, though, as "absence of a path" is a state
> recorded in that source tree object.

I find --ignore-removal fairly easy to understand, and I had no idea
what --overlay would mean.

I realize this is just one user's experience.  I'd be happy to do a
little informal survey (e.g. taking the description from the manpage
and asking people to name the option) if that's useful.

See also https://dl.acm.org/citation.cfm?id=32212 on this subject.

> The word "removal" makes little sense in "git add --ignore-removal",
> but it and "git add --no-all" outlived their usefulness already, so
> it may not be worth _fixing_ it.  But I am mildly opposed to spread
> the earlier mistake to a new option.

I think that's a good place to end up: once we flip the default for
checkout, then --ignore-removal would be an obscure option in that
command as well.  The consistency with "git add" is just a bonus.

Thanks,
Jonathan
Thomas Gummerer Jan. 24, 2019, 10:02 p.m. UTC | #7
On 01/23, Jonathan Nieder wrote:
> Hi,
> 
> Junio C Hamano wrote:
> > Thomas Gummerer <t.gummerer@gmail.com> writes:
> >> Jonathan Nieder wrote:
> 
> >>> Is this analogous to "git add --ignore-removal"?  If so, can we just
> >>> call it --ignore-removal?
> >>
> >> Yes, it seems like they are very similar.
> >
> > Hmm, I am not sure if the word "removal" makes sense in the context
> > of "checkout", as "removal" is an _action_ just like "checking out"
> > itself is, and not a _state_.  You'd check out a state out of a tree
> > to the index and the working tree, so "checking out absence of a
> > path" may make sense, though, as "absence of a path" is a state
> > recorded in that source tree object.
> 
> I find --ignore-removal fairly easy to understand, and I had no idea
> what --overlay would mean.

What do you think about --[no-]ignore-removed?  That would not be the same
as we are using in 'git add' though, and the slight difference may be
worse than a different option?  Though I suspect not too many people
are using --ignore-removal in 'git add' in the first place.

> I realize this is just one user's experience.  I'd be happy to do a
> little informal survey (e.g. taking the description from the manpage
> and asking people to name the option) if that's useful.

Sure, that sounds like an option if we can't come to an agreement
here.  What would such a survey look like?

> See also https://dl.acm.org/citation.cfm?id=32212 on this subject.

Sorry I don't have access to this, and unfortunately not the time to
read this either at the moment.
Thomas Gummerer Jan. 24, 2019, 10:08 p.m. UTC | #8
On 01/23, Jonathan Nieder wrote:
> Thomas Gummerer wrote:
> > On 01/22, Jonathan Nieder wrote:
> 
> > As checkout is a porcelain command, I had hoped it would be okay to
> > also have this as a configuration option, for the time before
> > 'checkout-paths' exists and while I'm getting used to actually typing
> > 'checkout-paths' instead of 'checkout'.  However I get that there may
> > be scripts that are using git checkout, and expect the previous
> > behaviour, so I'm also okay with dropping the config option for now.
> 
> Yes, if we have no plan for flipping the default later, then I would
> prefer to eliminate the config option.  Scripts very frequently use
> human-facing commands like "git checkout" when they want the command
> to produce (unparsable) friendly output to show to humans, and I don't
> think we've provided a good alternative for that use case.

Ok, I'm happy to drop that for now, and possibly re-introduce that
with another series to start flipping the default.  I'll probably wait
for Duy's checkout-paths command first though, and possibly send a
series later.

Junio, do you just want to revert the patch (1495ff7da5 ("checkout:
introduce checkout.overlayMode config", 2019-01-08)), or would you
prefer me sending a patch for that?

> > If we still want to make this the default even after 'checkout-paths'
> > exists, the plan you outline below sounds good to me, though maybe we
> > can make the "flip the default" step once we decide to release git
> > 3.0.
> 
> I would really like this, so I might write a series for it.  Please
> don't wait for me, though --- feel free to send any patches you're
> thinking about and we can work together or I can just appreciate your
> work. ;-)
> 
> Sincerely,
> Jonathan
Junio C Hamano Jan. 24, 2019, 11:02 p.m. UTC | #9
Jonathan Nieder <jrnieder@gmail.com> writes:

> I find --ignore-removal fairly easy to understand, and I had no idea
> what --overlay would mean.
>
> I realize this is just one user's experience.

Exactly.  My impression was the exact opposite from yours.

The phrase "removal" in the context of checkout does not click for
me at all, and neither it does in the context of add, especially
given that Git tracks states (i.e. snapshots), not changes.
Jonathan Nieder Jan. 25, 2019, 2:26 a.m. UTC | #10
Junio C Hamano wrote:
> Jonathan Nieder <jrnieder@gmail.com> writes:

>> I find --ignore-removal fairly easy to understand, and I had no idea
>> what --overlay would mean.
>>
>> I realize this is just one user's experience.
>
> Exactly.  My impression was the exact opposite from yours.
>
> The phrase "removal" in the context of checkout does not click for
> me at all, and neither it does in the context of add, especially
> given that Git tracks states (i.e. snapshots), not changes.

Thanks.  What do you think of --skip-removals (or --skip-deletions)?
The idea is "among the changes that you would be making to the
worktree, skip any unlink() steps".

If that seems sensible, we can use it for "git checkout" and,
optionally, add it as a synonym for --ignore-removal to "git add" as
well.

Jonathan
Duy Nguyen Jan. 25, 2019, 9:24 a.m. UTC | #11
On Fri, Jan 25, 2019 at 9:26 AM Jonathan Nieder <jrnieder@gmail.com> wrote:
>
> Junio C Hamano wrote:
> > Jonathan Nieder <jrnieder@gmail.com> writes:
>
> >> I find --ignore-removal fairly easy to understand, and I had no idea
> >> what --overlay would mean.
> >>
> >> I realize this is just one user's experience.
> >
> > Exactly.  My impression was the exact opposite from yours.
> >
> > The phrase "removal" in the context of checkout does not click for
> > me at all, and neither it does in the context of add, especially
> > given that Git tracks states (i.e. snapshots), not changes.
>
> Thanks.  What do you think of --skip-removals (or --skip-deletions)?
> The idea is "among the changes that you would be making to the
> worktree, skip any unlink() steps".

Another option from rsync: --delete (and --no-delete of course)
Philip Oakley Feb. 9, 2019, 6:54 p.m. UTC | #12
Hi,

On 24/01/2019 01:12, Jonathan Nieder wrote:
> Hi,
>
> Junio C Hamano wrote:
>> Thomas Gummerer<t.gummerer@gmail.com>  writes:
>>> Jonathan Nieder wrote:
>>>> Is this analogous to "git add --ignore-removal"?  If so, can we just
>>>> call it --ignore-removal?
>>> Yes, it seems like they are very similar.
>> Hmm, I am not sure if the word "removal" makes sense in the context
>> of "checkout", as "removal" is an_action_  just like "checking out"
>> itself is, and not a_state_.  You'd check out a state out of a tree
>> to the index and the working tree, so "checking out absence of a
>> path" may make sense, though, as "absence of a path" is a state
>> recorded in that source tree object.
> I find --ignore-removal fairly easy to understand, and I had no idea
> what --overlay would mean.

I too had difficulty initially as to what 'overlay' meant, or that there 
were options.


>
> I realize this is just one user's experience.  I'd be happy to do a
> little informal survey (e.g. taking the description from the manpage
> and asking people to name the option) if that's useful.
>
> See alsohttps://dl.acm.org/citation.cfm?id=32212  on this subject.

I did locate a copy at 
http://zhang.ist.psu.edu/teaching/501/readings/Furnas.pdf

The whole word choosing problem does smack a bit of Orwell's Vocabulary 
C (OVC) where:

"The [Newspeak] C vocabulary encompasses words that relate specifically 
to science and to technical fields and disciplines. It is designed to 
ensure that technical knowledge remains segmented among many fields, so 
that no one individual can gain access to too much knowledge. In fact, 
there is no word for “science” " [1]

Most of the DVCS concepts have a newness to them that means that we 
don't have good words yet, hence the difficulties. Just my 2 cents.

>> The word "removal" makes little sense in "git add --ignore-removal",
>> but it and "git add --no-all" outlived their usefulness already, so
>> it may not be worth_fixing_  it.  But I am mildly opposed to spread
>> the earlier mistake to a new option.
> I think that's a good place to end up: once we flip the default for
> checkout, then --ignore-removal would be an obscure option in that
> command as well.  The consistency with "git add" is just a bonus.
>
> Thanks,
> Jonathan

Philip

[1] https://www.sparknotes.com/lit/1984/section11/

Patch
diff mbox series

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 801de2f764..24e52b01e1 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -260,6 +260,9 @@  the conflicted merge in the specified paths.
 This means that you can use `git checkout -p` to selectively discard
 edits from your current working tree. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
++
+Note that this option uses the no overlay mode by default (see also
+`--[no-]overlay`), and currently doesn't support overlay mode.
 
 --ignore-other-worktrees::
 	`git checkout` refuses when the wanted ref is already checked
@@ -276,6 +279,13 @@  section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 	Just like linkgit:git-submodule[1], this will detach the
 	submodules HEAD.
 
+--[no-]overlay::
+	In the default overlay mode, `git checkout` never
+	removes files from the index or the working tree.  When
+	specifying `--no-overlay`, files that appear in the index and
+	working tree, but not in <tree-ish> are removed, to make them
+	match <tree-ish> exactly.
+
 <branch>::
 	Branch to checkout; if it refers to a branch (i.e., a name that,
 	when prepended with "refs/heads/", is a valid ref), then that
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 32c4b7f897..0c5fe948ef 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -44,6 +44,7 @@  struct checkout_opts {
 	int ignore_skipworktree;
 	int ignore_other_worktrees;
 	int show_progress;
+	int overlay_mode;
 	/*
 	 * If new checkout options are added, skip_merge_working_tree
 	 * should be updated accordingly.
@@ -132,7 +133,8 @@  static int skip_same_name(const struct cache_entry *ce, int pos)
 	return pos;
 }
 
-static int check_stage(int stage, const struct cache_entry *ce, int pos)
+static int check_stage(int stage, const struct cache_entry *ce, int pos,
+		       int overlay_mode)
 {
 	while (pos < active_nr &&
 	       !strcmp(active_cache[pos]->name, ce->name)) {
@@ -140,6 +142,8 @@  static int check_stage(int stage, const struct cache_entry *ce, int pos)
 			return 0;
 		pos++;
 	}
+	if (!overlay_mode)
+		return 0;
 	if (stage == 2)
 		return error(_("path '%s' does not have our version"), ce->name);
 	else
@@ -165,7 +169,7 @@  static int check_stages(unsigned stages, const struct cache_entry *ce, int pos)
 }
 
 static int checkout_stage(int stage, const struct cache_entry *ce, int pos,
-			  const struct checkout *state)
+			  const struct checkout *state, int overlay_mode)
 {
 	while (pos < active_nr &&
 	       !strcmp(active_cache[pos]->name, ce->name)) {
@@ -173,6 +177,10 @@  static int checkout_stage(int stage, const struct cache_entry *ce, int pos,
 			return checkout_entry(active_cache[pos], state, NULL);
 		pos++;
 	}
+	if (!overlay_mode) {
+		unlink_entry(ce);
+		return 0;
+	}
 	if (stage == 2)
 		return error(_("path '%s' does not have our version"), ce->name);
 	else
@@ -247,9 +255,9 @@  static int checkout_merged(int pos, const struct checkout *state)
 	return status;
 }
 
-static void mark_ce_for_checkout(struct cache_entry *ce,
-				 char *ps_matched,
-				 const struct checkout_opts *opts)
+static void mark_ce_for_checkout_overlay(struct cache_entry *ce,
+					 char *ps_matched,
+					 const struct checkout_opts *opts)
 {
 	ce->ce_flags &= ~CE_MATCHED;
 	if (!opts->ignore_skipworktree && ce_skip_worktree(ce))
@@ -281,6 +289,25 @@  static void mark_ce_for_checkout(struct cache_entry *ce,
 		ce->ce_flags |= CE_MATCHED;
 }
 
+static void mark_ce_for_checkout_no_overlay(struct cache_entry *ce,
+					    char *ps_matched,
+					    const struct checkout_opts *opts)
+{
+	ce->ce_flags &= ~CE_MATCHED;
+	if (!opts->ignore_skipworktree && ce_skip_worktree(ce))
+		return;
+	if (ce_path_match(&the_index, ce, &opts->pathspec, ps_matched)) {
+		ce->ce_flags |= CE_MATCHED;
+		if (opts->source_tree && !(ce->ce_flags & CE_UPDATE))
+			/*
+			 * In overlay mode, but the path is not in
+			 * tree-ish, which means we should remove it
+			 * from the index and the working tree.
+			 */
+			ce->ce_flags |= CE_REMOVE | CE_WT_REMOVE;
+	}
+}
+
 static int checkout_paths(const struct checkout_opts *opts,
 			  const char *revision)
 {
@@ -332,7 +359,14 @@  static int checkout_paths(const struct checkout_opts *opts,
 	 * to be checked out.
 	 */
 	for (pos = 0; pos < active_nr; pos++)
-		mark_ce_for_checkout(active_cache[pos], ps_matched, opts);
+		if (opts->overlay_mode)
+			mark_ce_for_checkout_overlay(active_cache[pos],
+						     ps_matched,
+						     opts);
+		else
+			mark_ce_for_checkout_no_overlay(active_cache[pos],
+							ps_matched,
+							opts);
 
 	if (report_path_error(ps_matched, &opts->pathspec, opts->prefix)) {
 		free(ps_matched);
@@ -353,7 +387,7 @@  static int checkout_paths(const struct checkout_opts *opts,
 			if (opts->force) {
 				warning(_("path '%s' is unmerged"), ce->name);
 			} else if (opts->writeout_stage) {
-				errs |= check_stage(opts->writeout_stage, ce, pos);
+				errs |= check_stage(opts->writeout_stage, ce, pos, opts->overlay_mode);
 			} else if (opts->merge) {
 				errs |= check_stages((1<<2) | (1<<3), ce, pos);
 			} else {
@@ -380,12 +414,14 @@  static int checkout_paths(const struct checkout_opts *opts,
 				continue;
 			}
 			if (opts->writeout_stage)
-				errs |= checkout_stage(opts->writeout_stage, ce, pos, &state);
+				errs |= checkout_stage(opts->writeout_stage, ce, pos, &state, opts->overlay_mode);
 			else if (opts->merge)
 				errs |= checkout_merged(pos, &state);
 			pos = skip_same_name(ce, pos) - 1;
 		}
 	}
+	remove_marked_cache_entries(&the_index, 1);
+	remove_scheduled_dirs();
 	errs |= finish_delayed_checkout(&state);
 
 	if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
@@ -547,6 +583,11 @@  static int skip_merge_working_tree(const struct checkout_opts *opts,
 	 * opts->show_progress only impacts output so doesn't require a merge
 	 */
 
+	/*
+	 * opts->overlay_mode 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
@@ -1183,6 +1224,10 @@  static int checkout_branch(struct checkout_opts *opts,
 		die(_("'%s' cannot be used with switching branches"),
 		    "--patch");
 
+	if (!opts->overlay_mode)
+		die(_("'%s' cannot be used with switching branches"),
+		    "--no-overlay");
+
 	if (opts->writeout_stage)
 		die(_("'%s' cannot be used with switching branches"),
 		    "--ours/--theirs");
@@ -1271,6 +1316,7 @@  int cmd_checkout(int argc, const char **argv, const char *prefix)
 			    "checkout", "control recursive updating of submodules",
 			    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 (default)")),
 		OPT_END(),
 	};
 
@@ -1279,6 +1325,7 @@  int cmd_checkout(int argc, const char **argv, const char *prefix)
 	opts.overwrite_ignore = 1;
 	opts.prefix = prefix;
 	opts.show_progress = -1;
+	opts.overlay_mode = -1;
 
 	git_config(git_checkout_config, &opts);
 
@@ -1302,6 +1349,9 @@  int cmd_checkout(int argc, const char **argv, const char *prefix)
 	if ((!!opts.new_branch + !!opts.new_branch_force + !!opts.new_orphan_branch) > 1)
 		die(_("-b, -B and --orphan are mutually exclusive"));
 
+	if (opts.overlay_mode == 1 && opts.patch_mode)
+		die(_("-p and --overlay are mutually exclusive"));
+
 	/*
 	 * From here on, new_branch will contain the branch to be checked out,
 	 * and new_branch_force and new_orphan_branch will tell us which one of
diff --git a/t/t2025-checkout-no-overlay.sh b/t/t2025-checkout-no-overlay.sh
new file mode 100755
index 0000000000..76330cb5ab
--- /dev/null
+++ b/t/t2025-checkout-no-overlay.sh
@@ -0,0 +1,47 @@ 
+#!/bin/sh
+
+test_description='checkout --no-overlay <tree-ish> -- <pathspec>'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	git commit --allow-empty -m "initial"
+'
+
+test_expect_success 'checkout --no-overlay deletes files not in <tree-ish>' '
+	>file &&
+	mkdir dir &&
+	>dir/file1 &&
+	git add file dir/file1 &&
+	git checkout --no-overlay HEAD -- file &&
+	test_path_is_missing file &&
+	test_path_is_file dir/file1
+'
+
+test_expect_success 'checkout --no-overlay removing last file from directory' '
+	git checkout --no-overlay HEAD -- dir/file1 &&
+	test_path_is_missing dir
+'
+
+test_expect_success 'checkout -p --overlay is disallowed' '
+	test_must_fail git checkout -p --overlay HEAD 2>actual &&
+	test_i18ngrep "fatal: -p and --overlay are mutually exclusive" actual
+'
+
+test_expect_success '--no-overlay --theirs with D/F conflict deletes file' '
+	test_commit file1 file1 &&
+	test_commit file2 file2 &&
+	git rm --cached file1 &&
+	echo 1234 >file1 &&
+	F1=$(git rev-parse HEAD:file1) &&
+	F2=$(git rev-parse HEAD:file2) &&
+	{
+		echo "100644 $F1 1	file1" &&
+		echo "100644 $F2 2	file1"
+	} | git update-index --index-info &&
+	test_path_is_file file1 &&
+	git checkout --theirs --no-overlay -- file1 &&
+	test_path_is_missing file1
+'
+
+test_done
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index d01ad8eb25..5758fffa0d 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -1436,6 +1436,7 @@  test_expect_success 'double dash "git checkout"' '
 	--progress Z
 	--no-quiet Z
 	--no-... Z
+	--overlay Z
 	EOF
 '