You committed too early, wrote a bad message, or included the wrong file. Here's the decision tree for undoing a Git commit safely, by what you actually want to keep.
Almost everyone reaches for the same search when they realize the commit they just made was wrong. And almost everyone gets scared off by an answer that shows git reset --hard without saying what it deletes. So before any commands: the right undo depends entirely on one question. Do you want to keep the work in that commit, or throw it away? Answer that first and the rest is easy.
Here's the fast map. Read your case, run the command.
You committed too early. The code is fine, you just want it back out of the commit and sitting in the staging area so you can regroup.
git reset --soft HEAD~1
This moves the branch pointer back one commit and leaves everything staged, exactly as it was the moment before you typed git commit. Nothing is lost. Run git status right after and you'll see your files under "Changes to be committed." This is the gentlest undo Git has, and it's the one I use most.
Same as above, except you also want the files pulled out of staging so you can pick and choose what goes into the next commit.
git reset --mixed HEAD~1
# or just:
git reset HEAD~1
--mixed is the default, so the bare git reset HEAD~1 does the same thing. The commit is gone, the changes are back in your working directory as modified files, and staging is clean. You lose nothing but the commit itself. This is the right call when you committed a big blob and want to split it into two.
The commit was garbage and so is the work in it. You want the tree back to how it looked one commit ago, with no trace.
git reset --hard HEAD~1
Read this line twice before running it. --hard throws away the commit AND every uncommitted change in your working directory. Not just the last commit's changes, everything that isn't committed. If you had unrelated edits open, they go too. There is no undo prompt. The only safety net is the reflog, and only if the work was committed at some point.
My rule: never run --hard on a dirty tree you haven't looked at. Run git status first. If there's anything in there you can't afford to lose, stash it before you reset.
Don't reset for this. The commit is fine, you just fumbled it.
# fix the message
git commit --amend -m "the message you meant to write"
# forgot to stage a file? add it, then fold it in
git add forgotten-file.js
git commit --amend --no-edit
--amend rewrites the last commit in place. --no-edit keeps the existing message while pulling in whatever's now staged. Clean and quick. One catch: amend creates a new commit with a new hash, which matters the moment the old one is already pushed. Hold that thought.
This is where people get hurt, so here's the rule with no hedging: if the commit is on a shared branch that other people pull, use revert, not reset.
git revert HEAD
revert doesn't erase history. It creates a new commit that undoes the changes of the target commit. The bad change is neutralized, but the timeline stays intact, so nobody else's clone breaks. Push it like a normal commit and you're done. If you want the deeper contrast on why this beats rewriting, we wrote up revert vs reset separately.
You can still reset a pushed commit and force-push over it:
git reset --hard HEAD~1
git push --force-with-lease
But understand the trade. Anyone who already pulled that commit now has history that disagrees with the remote, and their next pull turns into a mess you'll be untangling in Slack. Use --force-with-lease instead of plain --force so the push refuses if someone else has pushed in the meantime. On a solo feature branch nobody else touches, a force-push is fine and honestly cleaner. On main or anything shared, revert.
The HEAD~1 is just "one commit back." Change the number to reach further:
git reset --soft HEAD~3 # undo last 3 commits, keep all their changes staged
git reset --hard HEAD~3 # undo last 3 commits and delete their changes
For pushed commits, revert a range instead:
git revert --no-commit HEAD~2..HEAD
git commit -m "revert the last three commits"
--no-commit stacks the reversals so you get one clean commit instead of three.
This is the part nobody tells you, and it's the reason reset is less scary than it looks. Git keeps a log of everywhere HEAD has been, even after a hard reset.
git reflog
You'll get a list like a1b2c3d HEAD@{1}: commit: the thing you thought you nuked. Find the hash of the commit you want back and either check it out or reset to it:
git reset --hard a1b2c3d
The reflog holds entries for about 90 days by default, so a commit you made and then reset away is almost always recoverable. The one thing it can't save is work that was never committed, changes that only ever lived in your working directory before a --hard. That's the real reason to commit early and often: a commit is recoverable, an unsaved edit is not.
If you get stuck on something outside this tree, our broader Git troubleshooting guide covers the neighboring failures like rejected pushes and detached HEAD.
Ask one question before you touch anything: is this commit pushed to a branch other people use? If yes, git revert and move on, no exceptions worth the risk. If no, pick your reset by what you want to keep: --soft to hold the work staged, default --mixed to hold it unstaged, --hard only after git status confirms there's nothing in the working tree you'll cry over. And keep git reflog in your back pocket, because the safety net has been there the whole time.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
We've shipped production pipelines on both. Here's where GitHub Actions wins, where GitLab CI wins, and how to pick without regretting it in six months.
How pods can talk to AWS, GCP, and Azure with no static keys — using audience-bound projected ServiceAccount tokens and the cluster OIDC issuer.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.