Home > Back-end >  Aborts during checkouts
Aborts during checkouts

Time:04-10

I have the following sequence of actions chronologically:

mkdir gitcommits
cd gitcommits
echo 'a' > filea.txt
echo 'b' > fileb.txt
git init
git add .
git commit -m "Commit0"

This gives rise to the following contents in the working tree.

Commit0: shaid0
filea (I remove the .txt here to indicate whether it has changed or not)
fileb

In working directory, I changed filea.txt to obtain a modified filea*:

echo 'a*' > filea.txt

This was followed by

git stash

This put me back to HEAD pointing at Commit0. Now, I modified fileb to fileb* thus:

echo 'b*' > fileb.txt

Then, I committed this thus:

git add fileb.txt
git commit -m "Commit1"

to obtain in the working tree:

Commit1: shaid1
filea
fileb* (note no .txt suffix to indicate that this is modified fileb.txt)

To this, I applied the stash thus:

git stash apply

to obtain in my working directory filea* and fileb*. I put them into a commit via:

git add filea.txt
git commit -m "Commit2" 

whose working tree is:

Commit2: shaid2
filea*
fileb*

I then switched to Commit1 via:

git checkout HEAD^

This put me in detached head mode with working directory contents:

Commit1: shaid1
filea
fileb*

I now reapplied the stash via:

git stash apply

to obtain:

Working Tree:
filea*
fileb*

From this position, the following command fails:

git checkout master

with

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

However, from this position, applying:

git checkout HEAD^

works in the sense that I move back to Commit0.

My question is, why does git checkout HEAD^ proceed without error, while git checkout master aborts.

CodePudding user response:

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.

CodePudding user response:

In general git tries very hard to not let the user accidentally lose any changes in the working directory which are not yet in version control unless you tell it explicitly to do so with options like --hard or --force.

Unstaged changes are not under version control by definition, so when you try git checkout master in your last step it doesn't matter that the current file contents are identical to the ones in master, it just detects that you have unstaged changes for this file and therefore can not even attempt a simple merge here. If you stage the changes beforehand with git add filea.txt it will work in this particular case though, but will still fail if there are any differences. Checkout won't attempt a 'real' merge.

On the other hand a git checkout HEAD^ at this point is not a problem. Since the file filea.txt doesn't change, git doesn't have to touch this file and can keep your working changes as they are (no matter if staged or unstaged).

  •  Tags:  
  • git
  • Related