git revert
git revert is the command you reach for when you need to undo a commit that has already been shared with other people. Instead of deleting or rewriting history, it works out the inverse of a commit’s changes and applies that inverse as a brand-new commit on top of your branch. The bad change disappears from your working tree, but the record of it — and the record of undoing it — both stay visible forever in the log, which is exactly what makes revert safe to use on branches other people are also working on.
How git revert Works
Every commit in Git is an object that points to a tree (a snapshot of every file at that point in time) and to one or more parent commits. A branch such as main is nothing more than a movable pointer — a hash stored in a small file under .git/refs/heads — that points at the tip commit. HEAD normally points at the branch, which in turn points at the commit, so moving the branch pointer is what “advancing” a branch actually means.
When you run git revert <commit>, Git computes the diff between that commit and its parent — the patch the commit introduced — and then tries to apply the opposite of that patch to your current index and working tree, using the same three-way merge machinery that powers git cherry-pick and git merge. If the reversal applies cleanly, Git writes new blob and tree objects for the resulting snapshot, wraps them in a brand-new commit object (default subject line Revert "<original subject>", with a body noting This reverts commit <sha>.), sets that new commit’s parent to whatever HEAD currently points to, and moves the branch pointer forward to the new commit. Nothing about the original commit object changes — its content, hash, author, and position in history are untouched. It simply stops being the last word on those lines.
Contrast this with git reset. reset (especially git reset --hard) moves the branch pointer backward or sideways and can drop commits out of a branch’s history entirely; that rewrites the branch, which is dangerous the moment anyone else has already fetched or based work on the old tip. revert never rewrites anything that already exists — it only ever adds a new commit — so it is the correct undo tool for anything already pushed to a shared branch like main, or already merged through a pull request.
revert can operate on any commit, not just the most recent one, on a merge commit (with an extra flag explained below), and on a whole range of commits at once. Because Git is really just replaying an inverse patch, a revert can hit a merge conflict exactly like a normal merge would if the lines it’s trying to undo have since been changed by later commits — Git pauses mid-revert, leaves conflict markers in the affected files, and waits for you to resolve them before finishing.
Syntax
git revert [--no-commit] [--no-edit] [-m parent-number] <commit>...
| Option | Description |
|---|---|
<commit> |
The commit (or commits) to undo. Accepts a hash, a branch name, HEAD, HEAD~2, and so on. |
-n, --no-commit |
Apply the reversal to the index and working tree but don’t create the commit yet — useful for folding several reverts into one combined commit. |
--no-edit |
Accept the default Revert "..." commit message without opening an editor. |
-e, --edit |
Force the editor to open (this is the default behavior when a commit is created). |
-m parent-number, --mainline parent-number |
Required when reverting a merge commit; tells Git which parent to treat as the “mainline” to diff against (usually 1). |
--abort |
Cancel a revert that stopped due to conflicts and restore the state from before it started. |
--continue |
Resume a revert after you’ve resolved conflicts and staged the fix. |
--skip |
Skip the commit currently being reverted (in a multi-commit revert) and move to the next one. |
Examples
Example 1: Reverting the most recent commit
Suppose the last commit on main introduced a pricing bug. We can undo just that commit while leaving everything else as it is.
git log --oneline -3
git revert --no-edit HEAD
Output:
9d2f4a0 (HEAD -> main) fix: correct discount calculation
7b1e3c2 feat: add checkout summary page
3c4d5e6 chore: initial commit
[main 5f3a9c1] Revert "fix: correct discount calculation"
1 file changed, 3 insertions(+), 1 deletion(-)
Git diffed 9d2f4a0 against its parent, applied the opposite of that diff, and committed the result as 5f3a9c1. Running git log again would now show three commits below plus this new revert commit on top — the buggy commit itself is still there, just no longer in effect.
Example 2: Reverting a commit that causes a conflict
Now imagine an older commit, a1b2c3d, added a seasonal discount to pricing.js, but later commits touched those same lines again. Reverting the old commit collides with the newer edits.
git revert a1b2c3d
Output:
Auto-merging pricing.js
CONFLICT (content): Merge conflict in pricing.js
error: could not revert a1b2c3d... feat: apply seasonal discount
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git revert --continue".
hint: You can instead skip this commit with "git revert --skip".
hint: To abort and get back to the state before "git revert",
hint: run "git revert --abort".
Git leaves conflict markers inside pricing.js. After opening the file, deciding which version of the discount logic is correct, and removing the markers, we stage the file and tell Git the revert is finished:
git add pricing.js
git revert --continue
Only once you run --continue does Git actually create the revert commit and move the branch pointer — before that, main still points at whatever commit it pointed to before you started.
Example 3: Reverting a merge commit
Merging feature/seasonal-pricing into main created a merge commit, 9e8d7c6, with two parents. To revert the whole feature we have to tell Git which parent is the mainline.
git log --oneline --graph -6
git revert -m 1 9e8d7c6
Output:
* 9e8d7c6 (HEAD -> main) Merge branch 'feature/seasonal-pricing'
|\
| * c2d3e4f feat: seasonal pricing rules
| * b1c2d3e feat: scaffold seasonal pricing module
|/
* 9d2f4a0 fix: correct discount calculation
* 7b1e3c2 feat: add checkout summary page
[main 4a5b6c7] Revert "Merge branch 'feature/seasonal-pricing'"
2 files changed, 1 insertion(+), 14 deletions(-)
-m 1 means “keep parent 1 (main’s line) as the mainline and undo everything parent 2 (the feature branch) introduced.” This only removes the merge’s changes from main — it does not delete feature/seasonal-pricing or its commits. One subtlety worth knowing: if you later merge that same unchanged branch again, Git will see its changes as already applied-and-reverted and may bring nothing back; you’d need a fresh commit (or a revert of the revert) to reintroduce the feature.
How It Works Step by Step
- Git identifies the target commit and its parent, then computes the diff between them — the patch that commit introduced.
- Git applies the inverse of that patch to the current index and working tree, using the same three-way merge logic as
cherry-pickandmerge, so it can cope with the file having drifted slightly since. - If the inverse patch applies cleanly, Git immediately writes the new blob and tree objects for the resulting snapshot.
- Git creates a new commit object whose parent is the current tip of
HEAD, with the default messageRevert "<subject>"and a body noting which SHA it reverts (unless--no-editis passed, an editor opens so you can change this). - The current branch pointer moves to the new commit, and
HEADfollows along with it. If a conflict happens instead, none of steps 3–5 occur until you resolve it and rungit revert --continue— the branch pointer only advances once a real commit exists.
Common Mistakes
Mistake 1: Reverting a merge commit without -m
git revert 9e8d7c6
error: commit 9e8d7c6 is a merge but no -m option was given.
fatal: revert failed
A merge commit has two parents, so Git can’t guess which side represents “the rest of history” to diff against. Tell it explicitly:
git revert -m 1 9e8d7c6
Mistake 2: Expecting revert to erase the commit from history
Some beginners run git revert expecting the bad commit to vanish from git log, the way git reset --hard would make it vanish. It doesn’t — both the original commit and the new revert commit remain visible:
9f0a1b2 (HEAD -> main) Revert "fix: correct discount calculation"
9d2f4a0 fix: correct discount calculation
7b1e3c2 feat: add checkout summary page
That’s by design — it’s what keeps revert safe on shared history. If you genuinely need a commit to disappear from history entirely, that’s a job for git reset (or an interactive rebase), and only on a branch nobody else has already pulled.
Mistake 3: Committing a revert with leftover conflict markers
After a conflicting revert, it’s easy to run git add and git revert --continue without actually removing the markers Git inserted:
<<<<<<< HEAD
export const SEASONAL_DISCOUNT = 0;
=======
export const SEASONAL_DISCOUNT = 0.15;
>>>>>>> parent of a1b2c3d (feat: apply seasonal discount)
Staging and committing this literally bakes the marker text into the file, which usually breaks the build in a new and confusing way. Always open every conflicted file, pick or rewrite the correct code, delete the marker lines, and run your tests before staging and continuing.
Best Practices
- Prefer
revertoverreset --hardplus a force-push whenever the commit you’re undoing has already been pushed or merged through a pull request. - Let the default
Revert "..."message stand for simple cases, but add a short explanation in the body — why it’s being reverted, a link to the issue — when it isn’t obvious. - When undoing several related commits, revert each with
--no-commitand then make one clean commit with a message likerevert: roll back seasonal pricing experiment, instead of a chain of separate revert commits. - Always run your test suite (and read the diff) after a revert completes, especially one that needed conflict resolution — the inverse patch might not fully restore the old behavior if surrounding code has moved on.
- Push a revert the same way you’d push any other commit; there’s no need to force-push, since revert never rewrites history.
- Open a pull request for a revert on protected branches, just like any other change, so teammates can see and approve what’s being undone.
- If a revert goes wrong mid-conflict, run
git revert --abortto cleanly back out rather than hand-editing the index.
Practice Exercises
- In a scratch repository, commit a change that introduces an obvious bug (for example, set a constant to the wrong value) with message
fix: adjust shipping threshold, then make one more unrelated commit on top. Undo only the buggy commit without touching the newer one. Hint: don’t reach forreset— history has already moved on past the bad commit. Expected end state:git log --onelineshows the original bug commit, the newer commit, and a new revert commit on top, all three present. - Reproduce a revert conflict on purpose: commit a change to one line of a file, then commit a second change to that same line, then try to revert the first commit. Resolve the conflict so the file matches the second commit’s intent. Expected end state:
git statusis clean,git logshows the revert commit, and no conflict markers remain in the file. - Create a feature branch, add a commit or two, merge it into
mainwith a real merge commit (no fast-forward), then revert the merge. Expected end state:main‘s files no longer contain the feature branch’s changes, butgit log --graphstill shows the original merge, and the feature branch itself is untouched.
Summary
git revertundoes a commit’s changes by creating a brand-new commit containing the inverse patch — it never rewrites or deletes existing commits.- Because it’s history-additive, it’s the safe way to undo something already pushed or merged;
git resetis only safe on branches nobody else has based work on. - Reverting a merge commit requires
-m <parent-number>to tell Git which parent line is the mainline. - A revert can hit a merge conflict just like a normal merge if the target lines were touched by later commits; resolve,
git add, thengit revert --continue(orgit revert --abortto bail out). --no-commitlets you fold several reverts into a single commit instead of one revert per undone commit.- Always test after a revert completes — the inverse patch restores old text, not necessarily old behavior, if the surrounding code has changed.
