How to Undo the Last Git Commit (and Recover From It)
Every Git user eventually commits something they did not mean to: the wrong file, a typo in the message, a debug flag, or a password. Git has at least five ways to undo it, and they are not interchangeable. Which one is right depends on two questions: has the commit been pushed, and do you want to keep the changes it contained?
Rather than describe the commands in theory, I built a temporary test repository and ran each one. Everything below is real output from Git 2.43. The hashes will differ on your machine, but the behaviour will not.
The setup
The repository has two good commits. Then I made a common mistake: I added a debug line to config.ini and, in the same commit, a file with a password in it.
$ git add config.ini secret.txt
$ git commit -m 'Enable debug'
[main 30e4bbb] Enable debug
2 files changed, 2 insertions(+)
create mode 100644 secret.txt
$ git log --oneline
30e4bbb Enable debug
e91c739 Set port
f70ec01 Initial commitFix the last commit with --amend
If the commit is mostly right but has a small error, you do not need to undo it at all. git commit --amend replaces the last commit with a new one built from the current index. Here I take secret.txt out of the index, but keep it on disk with --cached. Then I add a .gitignore for it, and amend:
$ git rm -q --cached secret.txt
$ echo secret.txt > .gitignore
$ git add .gitignore
$ git commit --amend --no-edit
[main 035d44a] Enable debug
Date: Mon Sep 14 10:04:00 2026 +0800
2 files changed, 2 insertions(+)
create mode 100644 .gitignore
$ git show --stat --format=%s HEAD
Enable debug
.gitignore | 1 +
config.ini | 1 +
2 files changed, 2 insertions(+)--no-edit keeps the message. Without it, Git opens your editor. You can also use -m to fix just the message. Notice the hash changed from 30e4bbb to 035d44a: amending does not edit a commit, it creates a replacement. That matters later, when we talk about pushed commits.
Note: if a secret was ever pushed, amending only hides it from the current branch. Treat the password as leaked and change it.
git reset: three modes, three different results
git reset HEAD~1 moves the current branch back one commit. The mode decides what happens to the changes that commit contained. Git tracks three things: the commit the branch points to, the index (staging area), and the files in your working tree. Each mode resets more of them.
| Command | Branch moves | Index | Working tree |
|---|---|---|---|
reset --soft | yes | kept (changes stay staged) | kept |
reset / --mixed | yes | reset (changes unstaged) | kept |
reset --hard | yes | reset | reset (changes gone) |
--soft: undo the commit, keep everything staged
$ git reset --soft HEAD~1
$ git status --short
A .gitignore
M config.ini
$ git log --oneline
e91c739 Set port
f70ec01 Initial commitThe commit is gone from the log, but the status letters are in the first column, which means staged. Running git commit now would recreate it. I use --soft when I want to squash the last few commits into one: git reset --soft HEAD~3 followed by a single commit.
--mixed: undo the commit and unstage
Mixed is the default, so plain git reset HEAD~1 does it:
$ git reset HEAD~1
Unstaged changes after reset:
M config.ini
$ git status --short
M config.ini
?? .gitignoreThe changes are still in the files, but nothing is staged: the M moved to the second column and the new file is untracked again. This is the mode for "I committed too much at once, let me split it up": you re-add and commit in smaller pieces.
--hard: throw the commit and the changes away
Before this step I recommitted, noted the hash, and also made an uncommitted edit, to show what --hard does to work in progress:
$ git rev-parse --short HEAD
e235b33
$ echo "timeout=30" >> config.ini
$ git reset --hard HEAD~1
HEAD is now at e91c739 Set port
$ git status --short
?? secret.txt
$ cat config.ini
server=web1
port=8080Both the debug line from the commit and the uncommitted timeout=30 line are gone. Two details are worth knowing. First, --hard does not touch untracked files. secret.txt has not been tracked since the amend, so it is still there. It now shows as untracked, because the .gitignore was removed with the commit. Second, the uncommitted edit is lost permanently. Git never stored it, so nothing below can bring it back. Run git stash first if you are not sure.
Recovering from a bad reset with the reflog
The committed work is different. A reset only moves a branch pointer. The commit object stays in the repository. Git records every position HEAD has had in the reflog:
$ git reflog -6
e91c739 HEAD@{0}: reset: moving to HEAD~1
e235b33 HEAD@{1}: commit: Enable debug
e91c739 HEAD@{2}: reset: moving to HEAD~1
edf71e7 HEAD@{3}: commit: Enable debug
e91c739 HEAD@{4}: reset: moving to HEAD~1
035d44a HEAD@{5}: commit (amend): Enable debug
$ git reset --hard HEAD@{1}
HEAD is now at e235b33 Enable debug
$ cat config.ini
server=web1
port=8080
debug=trueHEAD@{1} means "where HEAD was one move ago", which is the commit I just threw away. The reflog also shows every intermediate version from this demo, including the original amended commit, so you can go back further if you need to. For the common case there is a shortcut: reset saves the previous position in ORIG_HEAD, so git reset --hard ORIG_HEAD undoes the last reset directly.
The reflog is local to your clone and entries expire (by default after 90 days, or 30 for commits no longer on any branch). It is a last chance to recover, not a backup. But for "I reset ten minutes ago and regret it", it always works.
Pushed commits: use git revert
Everything above rewrites history: the branch ends up pointing to a different chain of commits. On your own unpushed branch that is fine. Once other people have pulled the commit, rewriting it means their history and yours disagree. The next pull then produces confusing merges or arguments about force-pushing.
git revert solves this by adding a new commit that applies the inverse of the old one:
$ git revert --no-edit HEAD
[main 0513e2f] Revert "Enable debug"
Date: Mon Sep 14 10:30:00 2026 +0800
2 files changed, 2 deletions(-)
delete mode 100644 .gitignore
$ git log --oneline
0513e2f Revert "Enable debug"
e235b33 Enable debug
e91c739 Set port
f70ec01 Initial commit
$ cat config.ini
server=web1
port=8080The original commit stays in the log and the revert comes after it. Everyone can pull it normally. You can revert any commit, not just the last, by passing its hash. If later commits touched the same lines you will have to resolve a conflict.
Not committed yet: git restore
Sometimes the mistake has not reached a commit. Since Git 2.23, git restore handles these cases. It replaces git checkout -- file and git reset HEAD file, two commands that already did too many different jobs. To discard edits in a file:
$ echo "oops" >> README
$ git status --short
M README
?? secret.txt
$ git restore README
$ git status --short
?? secret.txtThat is as destructive as reset --hard for that one file: the edit is gone. To unstage a file but keep the edit, add --staged:
$ echo "loglevel=warn" >> config.ini
$ git add config.ini
$ git status --short
M config.ini
?? secret.txt
$ git restore --staged config.ini
$ git status --short
M config.ini
?? secret.txtWhich one to use
- Wrong message or forgot a file, not pushed:
git commit --amend. - Undo the commit, keep the changes staged:
git reset --soft HEAD~1. - Undo the commit, keep the changes unstaged:
git reset HEAD~1. - Undo the commit and discard the changes:
git reset --hard HEAD~1, after checkinggit statusfor uncommitted work. - Already pushed to a shared branch:
git revert HEAD. - Regret a reset:
git reflog, thengit reset --hard HEAD@{n}.
One edge case: in a repository with a single commit, HEAD~1 does not exist and reset fails with fatal: ambiguous argument 'HEAD~1': unknown revision. To uncommit the very first commit and keep the files staged, delete the branch ref instead: git update-ref -d HEAD.
The git-reset manual page has more modes (--keep and --merge) for the rarer situations.