Home > other >  Branch from stash creates two new parent commits
Branch from stash creates two new parent commits

Time:10-06

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 -athree 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 rebased main 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.

  • Related