How to undo your last Git commit, every scenario
git reset --soft HEAD~1 undoes a commit and keeps your work. Here is the version for every situation, including pushed commits, and how to undo the undo.

To undo your last Git commit and keep the changes, run:
git reset --soft HEAD~1
The commit disappears, your files are untouched, and everything that was in it is sitting staged and ready to commit again. This is the one you want about 80% of the time.
But "undo" means four different things depending on your situation, and picking the wrong one either loses work or rewrites history other people are already using. Every command and output below was run in a real repository, so what you see is what Git prints.
Answer two questions first
| Have you pushed it? | Do you want the changes? | Command |
|---|---|---|
| No | Yes, ready to recommit | git reset --soft HEAD~1 |
| No | Yes, but let me re-choose what to stage | git reset HEAD~1 |
| No | No, delete them | git reset --hard HEAD~1 |
| No | The commit is fine, the message is not | git commit --amend |
| Yes | Anything | git revert HEAD |
The bold row is the one that matters most. Once a commit is on a shared branch, reset is the wrong tool, and the reason is at the bottom of this post.
Keep the changes, staged: reset --soft
git reset --soft HEAD~1
HEAD~1 means "one commit before where I am". --soft moves the branch pointer back and leaves everything else exactly as it was.
Before:
ad59cfc Add b.txt
20dac89 Add a.txt
After:
20dac89 Add a.txt
And git status --short:
A b.txt
A in the first column means staged. Your work is intact and already in the staging area. Fix whatever was wrong, git add anything new, and commit again.
Use this when you committed too early, forgot a file, or want to split one commit into two.
Keep the changes, unstaged: reset
git reset HEAD~1
No flag means --mixed, the default. It undoes the commit and unstages everything, leaving the changes in your working directory.
Git tells you what it did:
Unstaged changes after reset:
M a.txt
And status shows an unstaged modification:
M a.txt
Note the leading space. M in the second column is "modified, not staged".
One detail worth knowing: if the commit added a brand new file, a mixed reset leaves it untracked, not modified:
?? b.txt
Nothing is lost. Git just no longer knows the file, because knowing it was the thing you undid. Run git add again.
Use --mixed when you want to rebuild the commit from scratch and choose what goes in.
Throw the changes away: reset --hard
git reset --hard HEAD~1
This undoes the commit and deletes the changes. Git confirms where you landed:
HEAD is now at 20dac89 Add a.txt
The file the commit added is gone from disk. git status is clean.
Two warnings, and they are different sizes:
- The commit itself is recoverable for a while, through the reflog. See below.
- Uncommitted work in your working directory is not.
--hardwipes it with no record anywhere, because Git never had a copy.
So the dangerous case is not the commit you meant to delete. It is the half-finished edit sitting next to it. Run git status before every --hard and make it a reflex.
If you want the commit gone but the working directory kept, that is --soft or --mixed, not --hard.
The message is wrong, or you forgot a file: amend
Not every undo needs a reset. If the commit is the right idea and just came out wrong:
git commit --amend -m "Add b.txt with notes"
0887b68 Add b.txt with notes
20dac89 Add a.txt
Same position in history, new message, and a new hash. To include a file you forgot, stage it first and amend without a message:
git add forgotten-file.js
git commit --amend --no-edit
--no-edit keeps the existing message so no editor opens.
The new hash is the catch. Amending replaces the commit, so if you have already pushed, amending puts you in the same position as a reset, and the pushed-commit rules apply.
Already pushed: revert
This is the branch of the decision tree people get wrong, and the cost lands on their teammates rather than themselves.
reset and --amend rewrite history. They change which commits exist. That is fine while the commits live only on your machine. Once they are on a shared branch, other people have them, and rewriting means your branch and theirs disagree about the past. Pushing that requires a force push, which can silently destroy work anyone else pushed in the meantime.
git revert avoids all of it by adding rather than removing:
git revert HEAD
[main 6801c0c] Revert "Add b.txt with notes"
1 file changed, 2 deletions(-)
delete mode 100644 b.txt
6801c0c Revert "Add b.txt with notes"
0887b68 Add b.txt with notes
20dac89 Add a.txt
The original commit is still there. A new commit sits on top of it undoing its changes. History is honest, nothing is rewritten, and everyone else can just pull.
Add --no-edit to skip the editor and accept the default message.
Revert is not a lesser option. On a shared branch it is the correct one, and "we reverted that" is a normal, boring sentence in every team that ships.
If you genuinely must rewrite pushed history, for example because a commit contained a secret, use git push --force-with-lease rather than --force. It refuses if the remote has commits you have not seen, which is exactly the case that loses someone else's work. Tell your team before you do it. A leaked credential also needs rotating, because it is in the remote's history and in every clone, and removing the commit does not un-leak it.
The undo of the undo: reflog
Reset the wrong commit? It is almost certainly still there.
git log shows commits reachable from your branch. git reflog shows everywhere HEAD has been, including the places you reset away from:
git reflog
20dac89 HEAD@{0}: reset: moving to HEAD~1
ad59cfc HEAD@{1}: commit: Add b.txt
20dac89 HEAD@{2}: reset: moving to HEAD~1
ad59cfc HEAD@{3}: commit: Add b.txt
20dac89 HEAD@{4}: commit (initial): Add a.txt
Read it as a history of your moves. HEAD@{1} is where you were before the last reset, and ad59cfc is the commit you thought you deleted.
To get it back:
git reset --hard ad59cfc
ad59cfc Add b.txt
20dac89 Add a.txt
Recovered.
The reflog is local, it is per repository, and entries expire eventually, 90 days by default for reachable ones. It will not save you from a --hard over uncommitted changes, because those were never a commit. But for anything you committed, git reflog means a mistake is a five-second fix rather than a lost afternoon. It is the single most reassuring Git command there is.
Undoing more than one commit
HEAD~1 is "one commit back", so the number is the only thing that changes:
git reset --soft HEAD~3
That undoes the last three commits and leaves every change from all three staged together, ready to be recommitted as one. It is the simplest way to squash work in progress before opening a pull request, and it does not require interactive rebase.
You will also see HEAD^. On a normal commit the two are identical:
git rev-parse HEAD~1 # 0887b68c8468a079975a4e7773cad907fce59f99
git rev-parse HEAD^ # 0887b68c8468a079975a4e7773cad907fce59f99
They only diverge on merge commits, which have two parents. There HEAD^1 and HEAD^2 pick a parent, while HEAD~1 always follows the first. Until you are undoing merges, use HEAD~n and do not think about it.
To revert several pushed commits, revert them newest first, or let Git do it in one go:
git revert --no-commit HEAD~2..HEAD
git commit -m "Revert the last three commits"
Undoing changes you have not committed
Half the people looking for this page do not have a commit to undo. They have edits they want gone, and reset is the wrong tool because there is nothing to reset to.
Discard edits to a file:
git restore a.txt
Status goes from M a.txt to clean. The file is back to its last committed state, and the edits are gone for good, because Git never had them.
Unstage a file without losing the edits:
git restore --staged a.txt
Status goes from M a.txt (staged) to M a.txt (not staged). This is the modern replacement for git reset HEAD a.txt, and it says what it does.
Keep the edits for later without committing them:
git stash
Working directory clean, changes parked. git stash pop brings them back. Reach for this when you need to switch branches mid-thought.
Undoing the very first commit
HEAD~1 needs a previous commit to point at. On the initial commit there isn't one:
git reset --soft HEAD~1
fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree.
fatal: your current branch 'main' does not have any commits yet
The fix is to delete the branch reference instead:
git update-ref -d HEAD
Your files stay, staged and ready:
A f.txt
Rare, but it happens on day one of a project, and the error message does not hint at the answer.
Quick reference
# Undo the commit, keep the changes staged
git reset --soft HEAD~1
# Undo the commit, unstage the changes
git reset HEAD~1
# Undo the commit and delete the changes
git reset --hard HEAD~1
# Undo the last three commits, keep all the work
git reset --soft HEAD~3
# Fix the message only
git commit --amend -m "A better message"
# Add a forgotten file to the last commit
git add file.js && git commit --amend --no-edit
# Undo a pushed commit safely
git revert HEAD
# See where HEAD has been, and recover
git reflog
git reset --hard <hash>
Two habits that prevent most of this
Run git status before anything destructive. It takes two seconds and it is the only thing standing between --hard and work Git has no copy of.
Commit more often, in smaller pieces. Undoing a commit that changed one thing is trivial. Undoing a commit that changed forty files is a project. Small commits also make git revert a genuine option rather than a scary one.
Practice where mistakes cost nothing
Git is the one tool people avoid experimenting with, because experimenting is exactly where the horror stories come from. That is backwards: the commands are only frightening until you have run them and watched what they do.
Git Basics gives you a real Git sandbox in the browser, with a working terminal and a live commit graph. You can reset, revert, delete a branch, and dig it back out of the reflog, all against a throwaway repository where the worst outcome is a page refresh.
Related reading: How Websites Work for where your code goes after you commit it, and Vibe Coding, and Exactly Where It Breaks on why "I'll just let the AI fix it" is a poor recovery plan when history is involved.
More from the blog

Vibe coding, what it is and exactly where it breaks
Vibe coding means describing what you want and shipping what the AI writes without reading it. It works for four things and fails badly at five others.
Read more
SQL joins explained, which rows survive and why
INNER, LEFT, RIGHT, FULL and CROSS joins on the same two tables, with the real result rows. Plus why the Venn diagram everyone shows you is misleading.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.