I have stashed commit on a branch like so
a -> b -> c <- main
\
x <- stash@{0}
I want to use the stashed commit so I created a branch from it
git branch tmp stash@{0}
I expected the history to look like this
a -> b -> c <- main
\
x <- tmp
But instead, there are now two new parent commits to the branch like this
a -> b -> c <- main
\
y
\
x <- tmp
/
z
where <y>
has the commit message index on main: <c_hash> <c_message>
and <z>
has the commit message untracked files on main: <c_hash> <c_message>
. In my case both commits are empty.
How can I get rid of them?
To give more background. The stashed commit has some useful changes that I want to use as part of main
. My actual history is more complicated as there have been more commits added to main
in the meantime (e.g. d
, e
, f
, etc.). I know I could pop the stashed commit on main
and then use interactive rebase to move it down the history to where it was before. But this time I created a branch from the stashed commit and rebased main
onto it, which left me with these two undesired parent commits that I don't know how to get rid of now.
CodePudding user response:
The easiest way to have c
become the sole parent of x
is to simply recreate it on top of main
:
# Check out the 'tmp' branch
git switch tmp
# Move HEAD to the 'main' branch, but keep the changes
# introduced by 'x' in the index and in the working directoy
git reset --soft main
# Commit the changes again, this time on top of 'main'
git commit -m 'x'
The resulting history is going to look like this:
a---b---c-----x
^ ^
main tmp
CodePudding user response:
Enrico Campidoglio's answer talks about what you should probably be doing instead of what you are doing, but let's talk here about what you are doing and why it produces such a weird-looking result.
git branch tmp stash@{0}
We will need to take this apart a bit, but first:
a -> b -> c <- main \ x <- stash@{0}
This drawing is wrong in a couple of subtle but important ways. One is that inside Git, all the arrows "go backwards", so we should draw our commits like this:
A <-B <-C <-- main
(uppercase vs lowercase is a choice, I just use uppercase as I think it looks better when referring to them later in text). Or we can be lazy and not bother with the internal arrows, since they're immutable and always point backwards. The arrow coming out of the branch name, though, does move, so it's best to keep on drawing it:
A--B--C <-- main (HEAD)
(I've added HEAD
here to indicate that we're on main
so that C
is the current commit.)
The more important mistake in the drawing is that git stash
does not make one commit. It makes either two commits, or—with -u
or -a
—three commits. The first and last commits stash makes are, in order, the index i
and working-tree w
commits. Commit w
has the form of a merge commit. If git stash
is making three commits total, the second commit is the untracked-files commit u
. The final w
commit has either two parents, the current commit and i
(in that order), or three parents: the current commit, i
, and u
(in that order). So we get either:
A--B--C <-- main (HEAD)
|\
i-w <-- stash
or:
A--B--C <-- main (HEAD)
|\
i-w <-- stash
/
u
The stash
ref (refs/stash
) is not a branch name but stash@{0}
is an alias for this stash
ref, so it works like one, and once you make your new tmp
branch you then have:
A--B--C <-- main (HEAD)
|\
i-w <-- stash, tmp
/
u
At this point you're probably going aha, so that's it, but just in case you're not, we now note that:
git branch tmp <commit-specifier>
for any valid commit-specifier just creates, or tries to create, a new branch name tmp
pointing to the specified commit. The commit already exists, and it's the w
(working tree) commit made by git stash
in this case; that commit already has three parents, namely C
, i
, and u
in that order. The fact that you see the third commit means that you must have run git stash -u
or git stash -a
, so as to make that third commit.
My actual history is more complicated as there have been more commits added to
main
in the meantime ... But this time I created a branch from the stashed commit and rebasedmain
onto it, which left me with these two undesired parent commits that I don't know how to get rid of now.
The rebase was a bad idea, and you should undo it using the reflog. Put main
back the way it was before, if at all possible, and drop the branch name tmp
, keeping the stash commit. (If you've dropped the stash, keep the branch name tmp
around so that you can find the stash commit easily.)
Once you're back in this situation:
A--B--C--D--E--F <-- main (HEAD)
|\
i-w <-- stash
/
u
you can use git stash branch
to create a new branch. This command deals with the funky structure of stash commits.
(Note: before using git stash branch
, make sure that git status
reports no untracked files if possible. You mention that the u
commit seems to be empty, and if it is, that's good, because git stash
can't un-stash a stash that has a u
commit if it can't create all the files that are in the u
commit. I consider this a bug in git stash
: I think you should be able to tell it ignore the u
commit even if it exists. But you can't, at least not today.)
What git stash branch
does is:
- create a branch pointing to the then-HEAD commit, i.e., commit
C
in this case; - use commit
i
, the saved index, to restore Git's index; - use commit
w
, the saved working tree, to restore your working tree; and - if it exists, use commit
u
, the saved untracked files, to restore untracked files (including untracked-but-ignored files if the stash was made with-a
).
Having done all that successfully, git stash branch
then drops the stash.
The git stash branch
command can make the name of a stash, e.g., git stash branch stash@{3}
, and can use a branch or tag name as long as that branch or tag name points to a stash commit. (It will try to use that commit even if it's not actually a stash commit: it just has to resemble one, i.e., be a merge commit.)
So, in this case you might use:
git stash branch newbranch tmp
You will be left in this situation:
D--E--F <-- main
/
A--B--C <-- newbranch (HEAD)
|\
i-w <-- tmp
/
u
Git's index, and your working tree, are now restored from the stash, so you can now commit (saving the same tree as stored in the i
commit) or run git add
and git commit
as desired. If commit i
's saved tree matches commit C
's saved tree, nothing is as yet "staged for commit" (as git status
would say) and you'll just want to git add
things and commit:
G <-- newbranch (HEAD)
/
A--B--C--D--E--F <-- main
|\
i-w <-- tmp
/
u
You now have a normal commit and can use all of Git's normal machinery, including cherry-pick or rebase, without getting all the weirdness that stashes impose.
Conclusion
The real conclusion you should take from all of this is that git stash
is a pretty poor tool. Try not to use it. It's not completely awful and is OK at certain small tasks, but use it sparingly. It's not good for anything long-term. Converting a stash to a branch, with git stash branch
, is the way to deal with a stash that has been around too long.