How Can Git Stash Cause a Conflict?

dodov

Hristiyan Dodov

Posted on November 27, 2022

How Can Git Stash Cause a Conflict?

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:

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 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"
Enter fullscreen mode Exit fullscreen mode

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"?
Enter fullscreen mode Exit fullscreen mode

Now, we get a conflict. If it were an actual file, we'd see this:

<<<<<<< Updated upstream
foo
=======
bar3
>>>>>>> Stashed changes
Enter fullscreen mode Exit fullscreen mode

Why does that happen? Well, in the first scenario, we:

  1. Stash that "foo" becomes "bar"
  2. Pop it on "main", where we still have "foo"
  3. Git can successfully change "foo" to "bar"

However, in the second scenario, we:

  1. Commit that "foo" becomes "bar2"
  2. Stash that "2" becomes "3"
  3. Pop it on "main", where we still have "foo"
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Now, we:

  1. Stash "foo" becoming "bar3", not the "2" in "bar2" becoming a "3"
  2. Pop it on "main", where we still have "foo"
  3. Git can successfully change "foo" to "bar3"

We're comparing apples to apples again:

[left (main)]  [stash (change)]  [right (original)]
foo            foo -> bar3       foo
Enter fullscreen mode Exit fullscreen mode

…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.

If you liked my explanation, you can follow @yandodov on Twitter. I enjoy dissecting problems like these and sharing my conclusions! 🤙

💖 💪 🙅 🚩
dodov
Hristiyan Dodov

Posted on November 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related