Over in Checkout another branch when there are uncommitted changes on the current branch I note that the real answer has to do with what happens to Git's index. So let's see what happens.
First, I turned your reproducer into a shell script, which is here:
#! /bin/sh -e
mkdir gitcommits
cd gitcommits
echo a > filea.txt
echo b > fileb.txt
git init
git add .
git commit -q -m Commit0
echo 'a*' > filea.txt
git stash
echo 'b*' > fileb.txt
git add fileb.txt
git commit -q -m Commit1
git stash apply -q
git add filea.txt
git commit -m Commit2
git switch -q --detach HEAD^
git stash apply -q
echo "expecting failure here"
if git switch -q --detach master; then
echo "bug? did not fail as expected"
fi
echo "expect success if you:"
echo " git switch --detach HEAD^"
echo "please explain -- starting subshell now"
sh -i
Running this gets me an interactive shell:
loginsh$ sh repro.sh
Initialized empty Git repository in [redacted]
Saved working directory and index state WIP on master: 42882c4 Commit0
[master 3e175c7] Commit2
1 file changed, 1 insertion(+), 1 deletion(-)
expecting failure here
error: Your local changes to the following files would be overwritten by checkout:
filea.txt
Please commit your changes or stash them before you switch branches.
Aborting
expect success if you:
git switch --detach HEAD^
please explain -- starting subshell now
$
Let's see what files Git would have to remove-and-replace on each git switch
or git checkout
command. The one that fails is the switch to master
:
$ git diff --cached master
diff --git a/filea.txt b/filea.txt
index d2a71ae..7898192 100644
--- a/filea.txt
+++ b/filea.txt
@@ -1 +1 @@
-a*
+a
That is, filea.txt
must be removed-and-replaced. But git status --short
tells us ...
$ git status --short
M filea.txt
... that filea.txt
has some precious work in it that should not be summarily destroyed. So we get the error we just saw, that we must commit or stash.
Meanwhile, switching to HEAD^
will remove-and-replace the following files:
$ git diff --cached HEAD^
diff --git a/fileb.txt b/fileb.txt
index 6178079..b435762 100644
--- a/fileb.txt
+++ b/fileb.txt
@@ -1 +1 @@
-b
+b*
But fileb.txt
is currently "clean": there are no changes to it that must be saved somewhere. So Git considers it safe to remove-and-replace fileb.txt
while changing commits. And that's what happens if we run the git switch
command:
$ git switch --detach HEAD^
$ git switch --detach HEAD^
M filea.txt
Previous HEAD position was a4a91a5 Commit1
HEAD is now at 42882c4 Commit0
$ head *
==> filea.txt <==
a*
==> fileb.txt <==
b
$
File fileb.txt
, which was safe to clobber, was clobbered by the tree-reading that git switch
did of commit HEAD^
, and so was the working tree copy. Had we forced a switch to master
:
$ exit
loginsh$ rm -rf gitcommits/
loginsh$ sh repro.sh
[redacted, but we get the same messages as last time,
just with different commit hash IDs]
$ git show :filea.txt
a
$ cat filea.txt
a*
(Note how, at this point, the index and working tree versions of filea.txt
differ, which is also what git status --short
showed us with the position of the single M
character.)
$ git switch -f master
Previous HEAD position was 784e81f Commit1
Switched to branch 'master'
$ git show :filea.txt
a*
$ head *
==> filea.txt <==
a*
==> fileb.txt <==
b*
$ git status --short
$
we find that the index copy of filea.txt
has been wrecked by the switch. The working tree copy has not actually been damaged since a*
was the contents of the working tree both before and after the switch; Git's message, though, says Your local changes to the following files ...
and not Your local changes to the following working-tree files
.
With git stash
, things can get even more confusing if you've staged (copied to the index) particular version of particular files. The git stash apply
step drops the index copy but git stash apply --index
restores the staged (index) version! A git stash push
or git stash save
always saves both; it's the restoration (apply or pop) step that may or may not drop the index commit entirely.