How Can Git Stash Cause a Conflict?
Even though it might seem unexpected, there's a reason for it.
Published on
-/- lines long
I've had several occasions where I'd receive a conflict after popping a git stash. But how does it make any sense? A stash is not attached to any commit. It's just the previously saved state of one file, slapped on top of the current state of the file.
As answered on Stack Overflow(opens in new tab):
To get a merge conflict within one file in the work-tree, Git must see the same line changed by both left and right side versions, but changed in different ways.
On the surface, that doesn't apply here, because there is no "left" and "right" side. You're not merging one branch (left) with another branch (right). You are instead applying a stash, which is not related to any branch or commit. Why couldn't Git just "paste" the stashed contents of the file on top of the current ones?
The git stash documentation(opens in new tab) states:
Use
git stash
when you want to record the current state of the working directory and the index, but want to go back to a clean working directory.
The answer lies in what "current state" means. It implies that a stash simply stores the current state of the file. But that's not what actually happens. It stores the changes inside the file. And a change represents the transition between two distinct states. This means that for each file, a stash relates to both the stashed change and the original version of the file at the time of stashing.
Let's say you have two branches, both containing a file with the contents "foo". You're on feature branch "feat", make some changes, stash them, then pop them on branch "main". It might look like this:
[main] | [feat]
foo | foo
| foo -> bar // file change
| $ git stash // stash that "foo" becomes "bar"
$ git stash pop | // pop the stash on branch "main"
foo -> bar | // "foo" changes to "bar"
In this case, everything worked alright. But let's suppose that we make a commit on branch "feat" that changes the content, before stashing a new change and popping it on "main":
[main] | [feat]
foo | foo
| foo -> bar2 // file change
| $ git commit // commit "foo" changing to "bar2"
| bar2 -> bar3 // file change
| $ git stash // stash that "2" becomes "3"
$ git stash pop | // pop the stash on branch "main"
foo [CONFLICT] | // change "2" to "3" in "foo"?
Now, we get a conflict. If it were an actual file, we'd see this:
<<<<<<< Updated upstream
foo
=======
bar3
>>>>>>> Stashed changes
Why does that happen? Well, in the first scenario, we:
-
Stash that "foo" becomes "bar"
-
Pop it on "main", where we still have "foo"
-
Git can successfully change "foo" to "bar"
However, in the second scenario, we:
-
Commit that "foo" becomes "bar2"
-
Stash that "2" becomes "3"
-
Pop it on "main", where we still have "foo"
-
Git can't change "2" to "3" because there is no "2"!
So this is where the left and right side thing mentioned earlier comes into play. We have "foo" in our worktree on "main" (the left side), "bar2" as the original content of the stashed file (the right side), and a stash that states "2" turns to "3":
[left (main)] [stash (change)] [right (original)]
foo 2 -> 3 bar2
This confuses Git, because changing "2" to "3" only makes sense on the right side. On the left, there is no "2".
On the other hand, in the first example, we have "foo" on both sides, and the change from "foo" to "bar" in the stash:
[left (main)] [stash (change)] [right (original)]
foo foo -> bar foo
Here, Git has no issues, because it compares apples to apples and allows you to turn them into oranges. It's not trying to turn oranges into tangerines, when all you have is apples.
Here's how the second scenario would pan out if we didn't commit:
[main] | [feat]
foo | foo
| foo -> bar2 // file change
| // do not commit "foo" to "bar2"
| bar2 -> bar3 // file change
| $ git stash // stash that "foo" becomes "bar3"
$ git stash pop | // pop the stash on branch "main"
foo -> bar3 | // "foo" changes to "bar3"
Now, we:
-
Stash that "foo" becomes "bar3",
rather than the "2" in "bar2" becoming a "3" -
Pop it on "main", where we still have "foo"
-
Git can successfully change "foo" to "bar3"
We're comparing apples to apples again:
[left (main)] [stash (change)] [right (original)]
foo foo -> bar3 foo
…and Git can yet again apply the stash successfully.
Conclusion
When stashing, you don't just save the current state of the file. You save the transition from one initial state to another state. If you pop the stash someplace where the current state differs from the initial state of the stash, you get a conflict.