git revert adds a new commit that undoes an earlier one. History is preserved; everyone who has the branch can pull normally. git reset moves the branch pointer backwards, dropping commits from the branch. History is rewritten; anyone who already has those commits will be out of sync.
git revert
- Creates a commit with the inverse change.
- Safe on shared branches (main, release).
- Can undo any commit, not just the latest.
- Leaves a record that the change happened and was undone.
git reset
- Moves the branch to an older commit.
- For local, unpushed work only.
- Can keep the changes (
--soft, mixed) or discard them (--hard). - Makes it look as if the commits never existed.
The rule
Pushed and shared? Revert. Local and unpushed? Reset. The moment a commit is on a branch other people fetch, rewriting it forces every collaborator to repair their clone, and a force-push can silently discard their work. A revert costs one extra commit and nothing else.
What each command does to the three areas
Git keeps changes in three places: the working tree (files), the index (staged changes) and the branch (commits). The reset modes differ in which of them they touch.
| Command | Branch pointer | Index (staged) | Working tree | Use when |
|---|---|---|---|---|
git reset --soft HEAD~1 | Moves back | Keeps the commit’s changes staged | Untouched | Redo the last commit with a different message or split it. |
git reset HEAD~1 (mixed) | Moves back | Unstaged | Untouched | Un-commit and re-stage selectively. |
git reset --hard HEAD~1 | Moves back | Discarded | Discarded | Throw the commit and all local changes away. Irreversible for uncommitted work. |
git revert HEAD | Moves forward (new commit) | Clean after commit | Clean after commit | Undo a commit that has been pushed. |
Reverting
git revert <commit> # one commit; opens an editor for the message
git revert --no-edit <commit> # keep the default message
git revert HEAD~3..HEAD # revert the last three commits, newest first, one revert each
git revert -m 1 <merge-commit> # revert a merge, keeping side 1 (the branch you merged into)
Reverting a commit that others have built on can produce conflicts, handled like any merge conflict: fix, git add, git revert --continue. Reverting a merge commit is special: it undoes the merged changes but Git remembers the merge, so merging the same branch again later brings nothing new. To re-apply, revert the revert.
Resetting
git reset --soft HEAD~1 # undo commit, keep changes staged
git reset HEAD~1 # undo commit, keep changes unstaged
git reset --hard HEAD~1 # undo commit, discard changes
git reset --hard origin/main # match the remote exactly, discarding local commits and edits
git reset <file> # unstage one file (same as git restore --staged <file>)
git reset --hard is the only command here that destroys uncommitted work with no undo. Before running it, git stash anything you might want, or at least run git status.
Recovering from a reset
Commits that were reset away are not deleted immediately. git reflog lists every position HEAD has had; find the entry from before the reset and go back:
$ git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~2
e4f5a6b HEAD@{1}: commit: Add search filters
c7d8e9f HEAD@{2}: commit: Fix pagination
$ git reset --hard HEAD@{1} # or: git branch rescue e4f5a6b
Reflog entries expire after 90 days by default. Uncommitted changes discarded by --hard are not in the reflog and cannot be recovered this way.
Scenarios
| Situation | Do this |
|---|---|
| Bad commit on main, already pushed | git revert <sha>, push. Open a fix in a new branch. |
| Last commit on my feature branch has a typo in the message | git commit --amend (a reset plus re-commit). Fine if not pushed, or if only you use the branch and you force-push with --force-with-lease. |
| I want to squash my last five local commits | git reset --soft HEAD~5 then one git commit. Or git rebase -i HEAD~5. |
| I committed to main instead of a branch, not pushed | git branch feature to save the commit, then git reset --hard origin/main. |
| A merged pull request broke production | git revert -m 1 <merge-sha> on main, deploy, then fix on the feature branch and revert the revert when merging again. |
| Local branch is a mess, I want the remote version | git fetch then git reset --hard origin/<branch>. |
Related commands
git restore <file>discards uncommitted changes to a file.git restore --staged <file>unstages it. These replace the oldgit checkout -- <file>andgit reset <file>forms.git checkout <sha>looks at an old commit without moving any branch (detached HEAD). It does not undo anything.git rebase -irewrites a range of commits (reorder, squash, drop). Same sharing rule as reset: local branches only.
The full command list is in the Git commands reference.