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?
-
12`git reset --soft @{1}` – user4003407 Jun 23 '16 at 20:20
-
1`git reset --soft @{1}` doesn't work for me. I just revert the commit, then squash them. solved this problem. – Zhang May 16 '19 at 06:23
-
The repeat of this question led to even better information, just saying – Jwan622 Jul 15 '21 at 19:59
1 Answers
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.)
-
1
-
1@PetSerAl: Aha, you're right! Though if HEAD is detached it works as `HEAD@{1}`. This seems a bit inconsistent since `@` means `HEAD`, but I suppose `HEAD@{0}` and `currentbranch@{0}` are necessarily synonymous, if there is a current branch at all. Thanks, I'll fix the answer. – torek Jun 23 '16 at 23:17
-
@andrybak: test it out by moving `HEAD` and/or some branches a few times, detaching HEAD, moving it some more, and reattaching it. The semantics are more convoluted than the syntax. – torek Jun 23 '16 at 23:32
-
1@andrybak Even so `HEAD` is symbolic ref, `@{1}` and `@@{1}` have different meaning. – user4003407 Jun 23 '16 at 23:35
-
-
-
2@8bitjunkie: Don't just blindly use `@{1}` because if you've done anything *since* the `git commit --amend`, it won't be `1` any more. Run `git reflog` if needed, to find the right hash ID or number for the `@{...}` syntax. – torek Oct 12 '20 at 20:26
-