"Applying the state" is indeed a three-way merge.
You noted (from the documentation) in your question How is a stash entry a child commit of HEAD commit and index's commit? that:
The ancestry graph looks like this:
.----W
/ /
-----H----I
where H
is the HEAD
commit, I
is a commit that records the state of the index, and W
is a commit that records the state of the working tree.
I'm going to break git stash pop
into its two component parts, which are git stash apply
followed by git stash drop
. If the apply step succeeds, pop
executes the drop
step as well. If not—if it stops with a conflict, for instance—pop
skips the drop
. The git stash
code is currently literally a shell script—I'd say "just" a shell script, but it is a pretty big and complex one weighing in at well over 700 lines—and the pop
code literally reads this way:
pop_stash() {
assert_stash_ref "$@"
if apply_stash "$@"
then
drop_stash "$@"
else
status=$?
say "$(gettext "The stash entry is kept in case you need it again.")"
exit $status
fi
}
which as you can see says apply, and if that works, drop; if it doesn't work, print an informative line and quit with the same status code that apply would have quit with.
Applying the index (I
) commit
Because each stash has at least two commits—the I
index-state and the W
work-tree-state—the apply step can apply both of them, or not. At the time you run git stash apply
or git stash pop
, you choose whether to apply both, or just the W
state. If you choose to apply both, the I
state is applied with what amounts to:
git diff <hash-of-H> <hash-of-I> | git apply --cached
(though the actual line of code is slightly different, to allow for binary files in the index, and is preceded and followed by some rather tricky magic to handle various corner and hard cases).
Because this is just diff | patch
it is not a true three-way merge. For details on what this implies, see What is the difference between git cherry-pick and git format-patch | git am? (both Mark Adelsberger's accepted answer and my own). In any case, the patch may fail. If it does fail, you have the option of using git stash branch
instead of git stash apply --index
, which is guaranteed to work.
Applying the work-tree (W
) commit
In any case, assuming you've either chosen to ignore the I
commit, or have successfully applied it and Git has squirreled the result away for later in the apply process, Git now moves on to the three-way merge you were asking about.
This part is pretty tricky. The heart of it, though, is this line:
if git merge-recursive $b_tree -- $c_tree $w_tree
which runs a three-way merge with the merge base being commit H
, the --ours
commit being whatever was in the index when you started the merge, and --theirs
commit being the contents of the W
commit.
Note: git merge-recursive
tries to accommodate whatever is in the work-tree when you start the merge, even if it does not match the index contents that act as the ours
tree. This is not always successful, which is why it's often a bad idea to git stash pop
into a dirty work-tree.
If there are merge conflicts, these three trees—$b_tree
from the H
commit, $c_tree
from a tree that git stash
made from your index before running git merge-recursive
, and $w_tree
from commit W
—are the three sources for files that go into the index in the merge-conflict staging slots, as I described in my answer to Git Stash Pop - Resolve Conflicts with mergetool.
Note that if you do use git mergetool
to merge them, this generally throws away any unstaged changes you had in your work-tree when you started the merge. This, too, is a reason it is generally unwise to start a git stash pop
or git stash apply
with a dirty work-tree. If your work-tree matches your index when you start git stash pop
, you will be in much better shape if a merge conflict occurs.
(Edit, Jun 2022: git stash
is now C code, so that it's much harder to see what it does. It now uses git merge-ort
instead of git merge-recursive
internally. But the essentials are the same, including the problems that come up here. I generally recommend avoiding git stash
as much as you can, as it's really easy to have things go wrong. Regular commits are much more user-friendly, if anything in Git can be called "user friendly" without a lot of loud sarcastic laughter.)