git reset and git revert both undo work, but they do it in opposite ways. git reset moves your branch pointer backward and rewrites history, as if the later commits never happened. git revert leaves history intact and adds a brand new commit that cancels out an earlier one. The short rule: use reset on commits you have not shared yet, and use revert on anything you have already pushed to a branch other people pull from.
The one-line decision
If you are still deciding which to reach for, here is the whole thing in two lines:
# Local, not pushed yet: rewrite history, the bad commit disappears
git reset --hard HEAD~1
# Already pushed / shared: record an undo commit, history stays
git revert HEADBoth leave you in a sane state. The difference is what they do to the commit graph, and whether anyone else has to deal with the fallout.
How they actually differ
reset is a history-rewriting operation. It says "pretend my branch was always here," and the commits you reset past are no longer reachable from the branch tip. They still exist for a while (see recovering lost commits with the reflog), but as far as the branch is concerned they are gone.
revert is an additive operation. It computes the inverse of a commit's changes and applies that as a new commit on top of your current history. The original commit stays in the log forever; you just have a follow-up commit that says "and then I took that back."
git reset | git revert | |
|---|---|---|
| What it does | Moves the branch pointer to an older commit | Adds a new commit that reverses an old one |
| History | Rewritten (commits dropped from the branch) | Preserved (everything stays in the log) |
| Safe on shared branches | No | Yes |
| Creates a new commit | No | Yes (one per reverted commit) |
| Touches working tree | Depends on mode (--soft, --mixed, --hard) | Yes, applies the inverse diff |
| Typical use | Clean up local commits before pushing | Undo a commit that is already public |
| Recoverable if wrong | Via the reflog, time-limited | Trivially, just revert the revert |
The three reset modes
git reset always moves the branch pointer. What it does to the staging area (the index) and your working files depends on the mode. This is the part that trips people up, so go slowly.
--soft: move the pointer, keep everything staged
git reset --soft HEAD~1The branch pointer moves back one commit. Your staging area and working files are untouched. The changes from the commit you just "undid" are sitting right there, staged and ready. This is the move when you want to redo the last commit, maybe to reword it or combine it with new work. It is the simplest way to undo the last commit without losing a single line.
--mixed: move the pointer, unstage the changes (the default)
git reset --mixed HEAD~1
# same as:
git reset HEAD~1--mixed is what you get when you do not pass a mode. The pointer moves back, and the changes are unstaged, but they stay in your working tree. So your files still have all the edits; they are just no longer staged. Reach for this when you want to re-stage things differently before committing again. It is also how you pull a file back out of the staging area without losing edits, which overlaps with discarding local changes when you go further.
--hard: move the pointer and throw the changes away
git reset --hard HEAD~1This is the destructive one. The pointer moves back, the staging area is reset, and your working files are overwritten to match. Any uncommitted work in those files is gone. Use --hard only when you are certain you want the changes erased. If you run it by accident, do not panic and do not keep committing on top: go straight to the reflog to recover the lost commits before the dangling commits get garbage-collected.
Here is the same set of modes summarized:
| Mode | Branch pointer | Staging area | Working tree |
|---|---|---|---|
--soft | Moved back | Kept (changes stay staged) | Kept |
--mixed (default) | Moved back | Reset (changes unstaged) | Kept |
--hard | Moved back | Reset | Reset (changes discarded) |
Why revert is the safe choice on shared branches
The reason reset is dangerous on a shared branch comes down to one fact: it rewrites history. Once you have pushed a commit, that commit's hash is part of the history everyone else has pulled. If you reset past it and force-push the rewritten branch, your branch no longer contains commits that other people's branches still do. The next time a teammate runs git pull, their history and yours have diverged, and you get the painful "histories have diverged" mess that ends in conflicts or, worse, someone re-pushing the commits you tried to delete. At that point everyone has to reconcile the diverged branches before any of them can push cleanly again.
git revert sidesteps all of that. It never removes a commit; it only adds one. So the shared history grows forward the way Git expects, no force-push, no rewritten hashes, nothing for teammates to reconcile. They pull a normal new commit that happens to undo an old one.
# Undo a specific public commit by hash
git revert 9f1c0a2
# Undo a range (each commit gets its own revert, newest first)
git revert HEAD~3..HEAD
# Stage the inverse changes without committing yet, so you can group them
git revert --no-commit 9f1c0a2If a revert turns out to be wrong, the fix is almost funny in how simple it is: you revert the revert. The change comes right back, history still intact. That reversibility is exactly why this is the tool you want anywhere other people are pulling from. For a deeper picture of why divergent histories cause so much grief, see git pull vs git fetch.
A worked example
Say you committed an API key by accident and have not pushed yet. You want it gone from the last commit:
# Not pushed: rewrite history, the commit vanishes
git reset --hard HEAD~1Now say the same bad commit is already on main and three teammates have pulled it. Resetting is off the table, and if you have set up undoing a commit on a branch protected by rules, the force-push that a reset would need is blocked outright. That is exactly why revert is the right tool on a shared or protected branch. Instead:
# Pushed: add a commit that undoes the bad one
git revert <commit-hash>
git pushOne important caveat on secrets specifically: revert undoes the change, but the secret still sits in the repository's history and can be checked out from the old commit. Reverting is not enough to scrub a leaked credential. Rotate the key, and if you truly need it gone from history, follow the steps in removing a secret from Git history. The exposed .git directory attack is a good reminder of why a leaked history is a real risk and not a theoretical one.
reset and revert are not the only undo tools
These two cover most undo situations, but Git has neighbors worth knowing:
- git stash shelves uncommitted work without committing or discarding it, handy when you reset by mistake and want to park changes first.
- interactive rebase rewrites a series of local commits (squash, reorder, reword) when a single
resetis too blunt. - Removing a tracked file but keeping it on disk is its own move; see removing a file from Git without deleting it.
- When the question is how to fold two branches together rather than undo a commit, that comes down to choosing between merge and rebase to integrate work, which shapes history the same way reset and rebase do.
If you are still building the mental model for how commits, branches, and the staging area fit together, start at Git for beginners and the branching explainer. The undo commands make a lot more sense once the graph clicks.
Sources
Authoritative references this article was fact-checked against.
- git-reset: official Git documentationgit-scm.com
- git-revert: official Git documentationgit-scm.com
- Pro Git: Reset Demystifiedgit-scm.com





