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:
- Read the ID (SHA-1 hash, like
a123456...
) of the current commit (viaHEAD
, 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). - Turn the index (aka staging-area) into a tree. This produces another ID; let's call this ID T (for Tree).
- Write a new commit with parent = C and tree = T. This new commit gets another ID. Let's call this N (for New).
- 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.)