Reverting and undoing commits in Git
Sooner or later every Git user needs to undo something — a typo in a commit message, a bug that slipped in, or a change pushed to the wrong branch. Git gives you three different commands for it: git revert, git reset, and git restore. They sound similar, but each one undoes something different and the consequences vary.
This tutorial walks through what each command does, when to reach for it, and a few real-world recipes so you can pick the right one in the moment.
The three "undo" commands at a glance
| Command | What it touches | Safe to use after pushing? |
|---|---|---|
git revert |
Creates a new commit that reverses an earlier one. History is preserved. | ✅ Yes |
git reset |
Moves the branch pointer back in history. Older commits are dropped from the branch. | ⚠️ Only if you haven't pushed (or you understand --force-with-lease) |
git restore |
Restores files in your working tree or staging area without touching commits. | ✅ Yes (file-level only) |
A useful way to remember the difference: revert is the public-friendly undo, reset is the private rewrite, and restore is for files rather than commits.
git revert — undo a commit safely
git revert is the right tool when the commit you want to undo has already been pushed, or when you're working on a branch other people pull from. It doesn't delete the original commit — it appends a brand new one that reverses the changes. That keeps history honest and avoids rewriting anything other developers may have based their work on.
Find the commit you want to undo first. The hash is the long string next to each entry in the commit history:
git log --oneline
8fd3350 Add the correct link to Brie
5eba8ab Remove Cheddar
d240853 Initial commit
To undo the Remove Cheddar commit, pass its hash (or the first few characters) to git revert:
git revert 5eba8ab
Git opens your editor with a prefilled commit message — usually Revert "Remove Cheddar" — which you can edit or accept. Once you save and close, Git creates a new commit that re-adds whatever the original commit removed.
git log --oneline now shows the new commit on top, with the original commit still intact below it:
e74b9a1 Revert "Remove Cheddar"
8fd3350 Add the correct link to Brie
5eba8ab Remove Cheddar
d240853 Initial commit
Push the revert like any other commit and the change reaches your teammates:
git push
Reverting a range of commits
To revert several commits at once, pass a range:
git revert HEAD~3..HEAD
Git walks the range in reverse order and creates one revert commit for each, which keeps the per-commit history clean. If you'd prefer a single combined revert commit instead, add --no-commit and commit manually after Git has finished applying the inverses:
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert last 3 commits"
Reverting a merge commit
Reverting a merge needs an extra hint, because Git has to know which side of the merge to keep. The -m 1 flag tells it to keep the first parent — typically the branch you were merging into:
git revert -m 1 <merge-hash>
Note: Reverting a merge does not delete the merged branch's commits — they're still in history. If you later want to merge that branch again, you'll have to revert the revert (git revert <revert-hash>), or rebase the feature branch.
git reset — move the branch pointer back
git reset is the private undo. It moves your current branch's pointer back to an earlier commit, effectively pretending the more recent commits never happened on this branch. Anyone who has already pulled those commits will still have them, which is why reset is reserved for work that hasn't been shared yet.
It has three modes, controlled by a flag:
| Mode | Moves HEAD | Touches the index (staging) | Touches the working tree | Use when |
|---|---|---|---|---|
--soft |
✓ | ✗ | ✗ | You want to redo the commit but keep the staged changes |
--mixed (default) |
✓ | ✓ (unstages) | ✗ | You want the changes back as edits, before staging |
--hard |
✓ | ✓ | ✓ (discards) | You want the changes gone completely |
Soft reset: redo the last commit
Imagine you just committed but the message is wrong, or you forgot to include a file:
git reset --soft HEAD~1
The commit is gone, but every change it contained is still staged and ready to be committed again. Stage the missing file (or fix whatever needs fixing) and run git commit to produce a clean replacement. For nothing more than fixing the message, git commit --amend is even quicker.
Mixed reset: pull changes back into the working tree
git reset HEAD~1 (with no flag) is the default mixed reset. It removes the commit and unstages the changes — they're still in your working tree as uncommitted edits, ready for you to re-stage selectively.
This is the right choice when you committed too eagerly and want to split the changes into two or more commits.
Hard reset: throw the changes away
git reset --hard is destructive. It moves HEAD, clears the staging area, and overwrites your working tree to match the target commit. Anything that hasn't been committed (or stashed) is lost.
git reset --hard HEAD~2
That command throws away the last two commits and any uncommitted edits. Useful when you've made a mess on a private branch and want a clean slate — dangerous if you forget that uncommitted work disappears too. If you're unsure, stash your changes first.
Recovering from a bad reset
git reset --hard looks final, but Git keeps a safety net called the reflog. Every move of HEAD is logged, so you can almost always get back to where you were:
git reflog
e74b9a1 HEAD@{0}: reset: moving to HEAD~2
8fd3350 HEAD@{1}: commit: Add the correct link to Brie
5eba8ab HEAD@{2}: commit: Remove Cheddar
To restore the branch to the state before the reset, point it back at the relevant entry:
git reset --hard HEAD@{1}
The reflog stays around for about 90 days by default, which is enough rope to recover from most accidents.
git restore — undo file-level changes
git restore was introduced to separate file-level operations from git checkout, which had grown to mean too many things. It doesn't move HEAD or change history — it works only on the files in your working tree and the staging area.
Discard unstaged changes in a file
If you've edited a file and want to throw the edits away:
git restore path/to/file
The file is rewritten to match the version in the last commit. Unstaged changes are gone, so use it intentionally.
Unstage a file
If you accidentally git added something, unstage it without touching the file's contents:
git restore --staged path/to/file
The file goes back to being an unstaged modification, ready to be edited or staged again.
Restore a single file from a previous commit
To bring a single file back to the way it looked in an earlier commit, point --source at that commit:
git restore --source 8fd3350 path/to/file
The file is rewritten to the version from that commit, but Git doesn't make a new commit for you — the change shows up as an unstaged modification you can review, stage, and commit yourself.
This is the cleanest way to undo a single file without affecting the rest of the project, and it's safe to use after pushing because no commits move.
Practical recipes
A short reference for the situations that come up most often:
"I just committed and want to change the message" — git commit --amend. Edits the most recent commit in place. Don't use this on commits you've already pushed.
"I committed and want the changes back as edits, before staging" — git reset HEAD~1 (mixed reset). The commit is gone, the files are back to unstaged.
"I want to throw away my last 2 commits completely" — git reset --hard HEAD~2. Destructive. Make sure those commits aren't on a shared branch.
"I pushed a bad commit and need to undo it on the remote" — git revert <hash> then git push. Safest option — adds a new commit instead of rewriting history.
"I want to discard everything I've changed since the last commit" — git restore . for unstaged edits, or git reset --hard HEAD if you've staged some too.
"I want to bring back one file from an earlier commit" — git restore --source <hash> path/to/file. Other files are untouched.
"I accidentally git added a file" — git restore --staged path/to/file (or the older git reset HEAD path/to/file).
These all work the same way against GitHub, GitLab, and Bitbucket — undo is a local operation, and pushing the result is what propagates it to your remote.
When you have to rewrite pushed history
Sometimes you really do need to use git reset on a branch that has already been pushed — for example, when a commit contains a secret that has to disappear from history entirely. The standard git push will refuse, because it would overwrite commits on the remote. A force-push is the only way:
git push --force-with-lease
--force-with-lease is the safer cousin of --force. It refuses to overwrite the remote if someone else has pushed commits since you last fetched, which prevents you from accidentally wiping out a teammate's work.
Even with that safeguard, force-pushing on a shared branch breaks anyone who has the old history checked out. They'll have to reset their local branch to match the new remote, and any work they had based on the rewritten commits will need to be rebased. As a rule of thumb: rewrite freely on your own branches, never on main, and on shared feature branches only after agreeing with everyone who has pulled.
revert, reset, restore — quick decision table
| What you want to do | Command |
|---|---|
| Undo a pushed commit safely | git revert <hash> |
| Undo a pushed merge commit | git revert -m 1 <merge-hash> |
| Redo the last commit (keep staged changes) | git reset --soft HEAD~1 |
| Bring last commit's changes back as unstaged edits | git reset HEAD~1 |
| Throw the last N commits away entirely | git reset --hard HEAD~N |
Recover from a bad --hard reset |
git reflog then git reset --hard HEAD@{N} |
| Discard unstaged changes to a file | git restore <file> |
| Unstage a file | git restore --staged <file> |
| Bring one file back from a previous commit | git restore --source <hash> <file> |
| Fix the most recent commit's message | git commit --amend |
Next steps
- Viewing the commit history — find the right hash before reverting
- Committing file changes — the commands behind every undo
- Publishing local changes — push your revert to the remote
git revert undoes a commit at the source. When the commit has already been deployed, you'll often want to undo it in production as well — DeployHQ's one-click rollback restores a previous build on your servers without re-running the whole pipeline, so a source-side revert and a production-side rollback take seconds rather than a deploy cycle. Try DeployHQ free.