How to undo a git commit --amend [duplicate]

I accidentally typed in a git commit --amend. This is a mistake because I realized that the commit is actually entirely new and it should be committed with a new message. I want to make a new commit. How do I undo this?


Solution 1:

PetSerAl's comment is the key. Here's the two command sequence to do just what you want:

git reset --soft @{1}
git commit -C @{1}

and an explanation of how this works.

Description

When you make a new commit, Git usually1 uses this sequence of events:

  1. Read the ID (SHA-1 hash, like a123456...) of the current commit (via HEAD, which gives us the current branch). Let's call this ID C (for Current). Note that this current commit has a parent commit; let's call its ID P (for Parent).
  2. Turn the index (aka staging-area) into a tree. This produces another ID; let's call this ID T (for Tree).
  3. Write a new commit with parent = C and tree = T. This new commit gets another ID. Let's call this N (for New).
  4. Update the branch with the new commit ID N.

When using --amend Git changes the process a bit. It still writes a new commit as before, but in step 3, instead of writing the new commit with parent = C, it writes it with parent = P.

Picture

Pictorially, we can draw what happened this way. We start with a commit graph that ends in P--C, pointed-to by branch:

...--P--C   <-- branch

When we make the new commit N we get:

...--P--C--N   <-- branch

When we use --amend, we get this instead:

       C
      /
...--P--N   <-- branch

Note that commit C is still in the repository; it's just been shoved aside, up out of the way, so that new commit N can point back to old parent P.

Goal

What you realized you want, after the git commit --amend, is to have the chain look instead like:

...--P--C--N   <-- branch

We can't quite do this—we can't change N; Git can never change any commit (or any other object) once it's stored in the repo—but note that the ...--P--C chain is still in there, fully intact. You can find commit C through the reflogs, and this is what the @{1} syntax does. (Specifically, this is short for currentbranch@{1},2 which means "where currentbranch pointed one step ago", which was "to commit C".)

So, we now run git reset --soft @{1}, which does this:

       C   <-- branch
      /
...--P--N

Now branch points to C, which points back to P.

What happens to N? The same thing that happened to C before: it's saved for a while through the reflog.

We don't really need it (although it may come in handy), because the --soft flag to git reset keeps the index / staging-area untouched (along with the work-tree). This means we can make a new commit again now, by just running another git commit. It will go through the same four steps (read the ID from HEAD, create the tree, create a new commit, and update the branch):

       C--N2   <-- branch
      /
...--P--N

where N2 will be our new new (second new?) commit.

We can even make git commit re-use the commit message from commit N. The git commit command has a --reuse-message argument, also spelled -C; all we have to do is give it something that lets it find the original new commit N from which to copy the message, to make N2 with. How do we do that? The answer is: it's in the reflog, just as C was when we needed to do the git reset.

In fact, it's the same @{1}!

Remember, @{1} means "where it was just a moment ago", and git reset just updated it, moving it from C to N. We haven't yet made new commit N2. (Once we do that, N will be @{2}, but we haven't yet.)

So, putting it all together, we get:

git reset --soft @{1}
git commit -C @{1}

1The places this description breaks down include when you're amending a merge, when you're on a detached HEAD, and when you use an alternative index. Even then, though, it's pretty obvious how to modify the description.

2If HEAD is detached, so that there is no current branch, the meaning becomes HEAD@{1}. Note that @ by itself is short for HEAD, so the fact that @{n} refers to the current branch, rather than to HEAD itself, is a bit inconsistent.

To see how they differ, consider git checkout develop followed by git checkout master (assuming both branches exist). The first checkout changes HEAD to point to develop, and the second changes HEAD to point to master. This means that master@{1} is whatever commit master pointed to, before the last update to master; but HEAD@{1} is the commit develop points to now—probably some other commit.

(Recap: after these two git checkout commands, @{1} means master@{1} now, HEAD@{1} means the same commit as develop now, and @ means HEAD. If you're confused, well, so was I, and apparently I am not alone: see the comments.)