57

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?

3
  • 12
    git reset --soft @{1} Commented Jun 23, 2016 at 20:20
  • 1
    git reset --soft @{1} doesn't work for me. I just revert the commit, then squash them. solved this problem. Commented May 16, 2019 at 6:23
  • The repeat of this question led to even better information, just saying Commented Jul 15, 2021 at 19:59

1 Answer 1

162

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.)

Sign up to request clarification or add additional context in comments.

9 Comments

AFAIK, @{1} is short for currentbranch@{1} not HEAD@{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.
@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.
@andrybak Even so HEAD is symbolic ref, @{1} and @@{1} have different meaning.
@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.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.