2
echo "a" > file1.txt
git add file1.txt
git commit -m "add file1"

echo "b" > file1.txt
git stash

Now

echo "c" > file1.txt
git stash apply

Results in:

error: Your local changes to the following files would be overwritten by merge:
    file1.txt
Please commit your changes or stash them before you merge.
Aborting

Cool! now let's stage file1.txt and apply the stash again:

git add file1.txt

git stash apply

And we have a conflict:

Auto-merging file1.txt
CONFLICT (content): Merge conflict in file1.txt

What's the conflict (cat file1.txt):

<<<<<<< Updated upstream
c
=======
b
>>>>>>> Stashed changes

In both cases, the stash was b and file1.txt in the working directory was c.

So why git stash apply was aborting when file1.txt was not staged and then threw a conflict when it was staged?

Comprehensive responses are appreciated.

I read the whole stashing and cleaning chapter in the git book but couldn't make sense of why this happens.

The question is NOT how to fix this. Just wondering what happens behind the scenes in this scenario.

sinaza
  • 820
  • 6
  • 19
  • 1
    A `stash apply` _is_ a kind of merge (the stash content must be reconciled somehow with the current state of things), so obviously there can be merge conflicts. Moreover a stash entry is _itself_ a kind of merge commit. Making a stash puts both the index and the work area into commits and rolls both back to the head commit state; the index commit then has the current head as parent, and the work tree commit has the index commit and the current head as parents, making the stash’s content a merge commit. You can actually see this structure by doing a `git log` on a stash entry. – matt Jan 12 '21 at 22:04

1 Answers1

4

A stash stores both the state of the index and the state of the working tree, because those are both things you might have in an incoherent or unfinished state. Making a stash puts both the index and the work area into commits and rolls both back to the head commit state; the index commit then has the current head as parent, and the work tree commit has the index commit and the current head as parents, making the stash’s content a merge commit. You can actually see this structure by doing a git log on your stash entry (your identifier numbers will be different of course):

*   501bd7e (refs/stash) WIP on master: f2c4178 add file1
|\  
| * 37ca60e index on master: f2c4178 add file1
|/  
* f2c4178 (HEAD -> master) add file1

Applying a stash, just the other way, requires that Git should update both the state of the index and the state of the working tree. Clearly Git must therefore reconcile stash index with your index and stash working tree with your working tree, and it uses merge logic to do so — so a conflict is perfectly possible.

So:

  • After your "c" change to file1.txt, we have a situation where you have changed the working tree but applying the stash would also change the working tree, so we abort. I think you understand that part.

  • You then copy the "c" change into the index (with git add). I think you understand that part too! Applying the stash would now also change the index in a way that conflicts with that. That's okay, but we enter the same state as with any merge conflict; nothing can now happen until that conflict is resolved or the merge is aborted.

This is one of the reasons that stash is often not the best choice. If the index is in a good state, in particular, it is often better to just to make a real commit to store the current state of things.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • My understanding is that after `git stash apply`, Git tries to "merge" `501bd7e` with the working directory AND `37ca60e` with the index. If that's correct, why it aborts in the first case but raises a conflict in the second? Like why not a conflict (or aborting) in both cases. – sinaza Jan 12 '21 at 22:35
  • It has to do with the mechanism of Git and merges. In reality _all_ merge conflicts have to do with the state of the index. The stash wants to operate on the working tree directly so we get a special case: it just gives up when there's a conflict. – matt Jan 12 '21 at 22:50
  • Thanks, @matt and yeah I understand both parts that you mentioned. Would it be safe to say, Git first tries to update index, if there's a conflict, it stops and demands a resolve. If not, it proceeds to update the working directory, if there is "conflict" (not actual) there, then it aborts? – sinaza Jan 12 '21 at 23:06
  • Yes, I think that is just what you have shown! (By the way, I could rattle on about how a merge works and how a merge conflict works, but that would sort of be outside this particular question.) – matt Jan 12 '21 at 23:20
  • The main point of confusion was that this is not the behaviour with actual `git merge`. With `git merge` it aborts right away if the working directory is not clean. Even if there is going to be a conflict later ;) – sinaza Jan 12 '21 at 23:35
  • 1
    I believe `git stash apply` (the first half of `git stash pop`) actually now completely *ignores* the index-state commit unless you use `git stash apply --index`. The old script did something more complicated, but there did not seem to be a good reason for that, and I think it was simplified as it was converted to C code. Anyway, as you noted, `git stash apply` does the application step as a merge, using `git merge-recursive`, but bypasses the safety checks that `git merge` uses. – torek Jan 13 '21 at 00:18