A merge commit landed on a shared branch and it has to come out. git revert
merge is the command for exactly that: it removes the effect of a merge commit by
adding a new commit on top, without deleting or rewriting anything already pushed. That rules
out git merge --abort, which only works while
MERGE_HEAD still exists — long gone once the merge is committed and pushed — and
it rules out a hard reset, which would force everyone who already pulled the merge to reconcile
diverged history. Almost every guide to this command mentions the -m flag and
stops there. Almost none explain how to actually verify which parent number is correct before
you run it, or what happens if you merge the same branch again afterward. This guide covers
both, in depth, plus the GitHub Revert-button mechanics, the exact error messages you'll hit if
you get the flag wrong, and how to confirm the rollback landed correctly before you push. For
reverting an ordinary, single-parent commit, see the companion guide on
reverting a commit — merge commits are the one
case that guide only touches briefly, and this one owns.
The Fast Answer: Revert a Merge Commit in One Command
One command is all it takes to undo a merge that's already pushed — safely, with no history rewrite:
git revert -m 1 <merge-commit-hash>
That's the whole move in the overwhelming majority of cases: parent 1 is the branch you were
on when you ran the merge — main, typically — and -m 1 keeps that line
of development intact while undoing everything the merge brought in from the other side. That's
what a git revert merge actually is once you strip away the jargon — a
rollback git merge, in plainer terms, that leaves the merge commit itself alone
and just cancels out what it introduced. What that one command doesn't answer up front: whether
"1" is actually correct for your merge, and what happens if someone tries to merge the
same branch again later. Both get a full section below — the second one trips up teams who think a
revert is the end of the story.
Why a Merge Commit Needs -m to Revert
An ordinary commit has exactly one parent, so git revert <hash> has exactly
one possible diff to invert — no ambiguity, no extra flag, which is the whole case covered in the
guide to reverting a single commit. A merge
commit is different by construction: it has two parents (or more, for a rare octopus merge), one
for each branch that came together. "The diff of this commit" stops being a single well-defined
thing the moment there are two parents to diff against, and git refuses to guess which one you
mean. That refusal is the entire reason -m exists —
git's official
git revert documentation calls the value it takes the mainline parent
number. It's not an optional performance flag, it's the answer to a question git cannot answer on
its own: relative to which parent should the merge's changes be undone?
Try it without the flag and git stops you immediately, before touching anything:
git revert a1b2c3d
# error: commit a1b2c3d is a merge but no -m option was given. The full mechanics of that error, and its mirror-image opposite, get their own section further down. First, the part that actually decides whether your revert does the right thing: figuring out which parent number to pass.
Finding the Right Parent Before You Revert
This is the step most tutorials skip entirely, and it's the one that actually matters. Don't assume parent 1 is correct just because it usually is — confirm it. Git records a merge commit's parents in the exact order they were combined, and four commands expose that order directly.
# Print both parent SHAs, in order: parent 1 first, parent 2 second
git log --pretty=%P -1 <merge-hash>
# Same information via git show
git show --format=%P -s <merge-hash>
# The raw commit object — look for the "parent" lines, in the order they appear
git cat-file -p <merge-hash>
# tree 8f3d2c1...
# parent 9f8e7d6... <- parent 1
# parent 3d81e0f... <- parent 2
# author ...
# Visual confirmation of which branch is which
git log --graph --oneline --all
The order is not a convention you have to trust — it's a direct consequence of how
git merge builds the commit. Parent 1 is always whatever HEAD pointed
to when the merge started: the branch you were checked out on. Parent 2 is the tip of the branch
named in the merge command — the one being brought in. Run git merge feature while
on main and parent 1 is main's pre-merge tip, parent 2 is feature's tip, full stop.
If a teammate instead had feature checked out and ran
merge master into branch to sync it, the
order for that commit is flipped — which is exactly why checking with
%P beats assuming.
git log --graph --oneline --all is the fastest sanity check once you already have a
theory about which side is which: the branch whose line runs straight through the merge commit
without a kink is parent 1; the branch whose line visibly joins in from the side is parent 2. On
a busy repository with several concurrent branches, narrow the graph to just the two commits you
care about with git log --graph --oneline <merge-hash>~3..<merge-hash>
before trusting what you're looking at.
Choosing -m 1 vs -m 2: The Decision Rule
With the parent order confirmed, the choice is mechanical. -m 1 keeps parent 1 as
the baseline and undoes whatever parent 2 uniquely introduced — this is what you want in roughly
95% of real merges, because parent 1 is almost always the long-lived branch (main)
and parent 2 is the short-lived feature branch you actually want gone. -m 2 does the
opposite: it keeps parent 2 as the baseline and undoes what parent 1 contributed instead. That's
the inverted case, and it only applies when the parent order itself is inverted from the usual
pattern — for example a merge commit created while a feature branch was checked out and
main was merged into it, rather than the other way around. Guessing here instead of
checking with the commands from the previous section is the single most common way this goes
wrong.
The wrong parent doesn't fail loudly — it fails by producing a commit that looks plausible and
is completely wrong. Run -m 2 on a standard "feature merged into main" commit and
git tries to undo main's changes relative to the feature branch instead of the feature's
changes relative to main. On any branch with real history, that diff is enormous — it reverses
weeks or months of unrelated main-line development, not the small feature you meant to pull back
out. That size mismatch is the tell: if the revert's diff looks nothing like the size of the
feature you're trying to remove, you almost certainly passed the wrong parent number. Catch it
with a diff before you push — the verification
section below covers exactly how.
Running git revert -m 1 Against the Merge Commit
Once the parent number is confirmed, running it is the easy part:
git revert -m 1 a1b2c3d
# opens your editor with a default message:
# Revert "Merge branch 'checkout-flow-redesign' into main"
#
# save and close -> the revert commit is created
# a1b2c3d itself is untouched and stays in history
The same flags that apply to a plain commit revert work here too — --no-edit to
skip the editor and accept the default message, --no-commit to stage the inverse
changes without committing yet, --continue and --abort if the revert
hits a conflict mid-way. The full rundown of those is in the
revert commit options guide; nothing about them
changes for a merge beyond the required -m.
One syntax trap worth calling out directly: typing git revert m1 a1b2c3d — no
dash — does not work. Without the leading -, git treats m1 as a
revision name to revert, not as a flag, and fails with something like
fatal: bad revision 'm1' or an "unknown revision" error, because no branch or tag
named m1 exists. The flag is -m followed by the parent number as a
separate argument — -m 1, with a space, or the glued short form -m1 if
you prefer; either works, but the dash is not optional.
The Re-Merge Trap: Why the Branch Won't Come Back
Here's the part almost nobody explains, and the reason this article exists. Say the revert landed clean, shipped, and later someone — maybe you, maybe a teammate who doesn't know the history — decides the feature should come back after all, and does the obvious thing:
git checkout main
git merge feature-branch
# Already up to date.
Nothing happens. Not "conflicts," not "nothing to commit" — git genuinely believes there is
nothing new to bring in, and the feature's changes stay reverted. This isn't a bug and it isn't
rare; it's the mechanical, guaranteed result of how git merge decides what to do.
git merge does not compare file contents to figure out what's missing. It walks
commit ancestry to find a merge base, then merges in whatever commits exist on one side but not
the other. The first merge already made every commit on feature-branch an ancestor
of main — that's what a merge commit is. Reverting the merge commit removes
the file changes but does not remove that ancestry; feature-branch's tip is still,
provably, an ancestor of main's current tip. So when you run git merge
feature-branch again with no new commits on that branch, git finds zero commits that
aren't already ancestors, and correctly — by its own logic — reports nothing to do. The revert's
removal simply stays in effect, silently, because there's no new commit for a merge to carry
across.
This is documented behavior, not a guess — Linus Torvalds wrote the canonical explanation of it in Git's own howto directory, and it's worth reading in full if this is happening on a repo you maintain: "How to revert a faulty merge". The short version matches what's above: reverting a merge and then re-merging the same branch is not how you bring a reverted feature back, because git's merge logic only looks at ancestry, and the ancestry never changed.
Escaping the Trap: Revert the Revert or Rebuild the Branch
Two ways out, and they trade off differently.
Option 1 — revert the revert. Since the revert commit is itself just an ordinary, single-parent commit, undo it the normal way:
git revert <revert-commit-hash>
# no -m needed — the revert commit has one parent This creates a new commit that reapplies the feature's original content, byte for byte — no re-merge involved, so the ancestry trap never comes into play. History ends up with an honest, fully auditable three-step record: merged, reverted, restored. The full mechanics of reverting a revert — including the doubled commit message git generates — are covered in the dedicated revert-a-revert guide; this is the same operation, just applied to a merge's revert commit instead of a plain one. It's the safer default: nothing gets rewritten, it works on any shared branch, and it's a single command.
Option 2 — rebuild the branch. Rebase feature-branch onto the
current tip of main so every one of its commits gets a brand-new SHA. New SHAs are
not ancestors of anything yet, so a subsequent git merge feature-branch finds real,
genuinely new commits to bring in and applies them properly — the trap only applies to commits
git has already seen. This is a good moment to also
squash the branch's commits into a cleaner unit before the
second merge. The catch: rebasing rewrites the branch's history, which is only safe if nobody
else has based work on it, or requires a coordinated force-push if they have.
Pick option 1 when the feature branch is done and you just want the exact same content back with minimal ceremony. Pick option 2 when the branch is going to keep evolving and you'd rather future merges behave normally without another double-revert dance every time.
GitHub Undo Merge: What the Revert Button Actually Does
On a merged pull request's page, GitHub shows a Revert button. Clicking it does
not touch the original merge commit — it opens a brand-new pull request whose diff is the exact
equivalent of running git revert -m 1 against the merge commit locally. That's the
real mechanism behind a github rollback merge: a second PR, not an un-merge.
You still have to review and merge that new PR for the rollback to actually take effect —
clicking Revert alone doesn't change anything on the target branch by itself.
Branch protection doesn't create a side door here — on a protected main, that
revert PR has to pass the same required reviews and status checks as any other pull request
before it can merge. If you'd rather drive it from the terminal instead of the button, the CLI
path is the same command covered above followed by a normal PR:
git revert -m 1 <merge-hash>
git push origin revert-checkout-flow-redesign
gh pr create --title "Revert: checkout flow redesign" --body "Reverts #482"
This is also the fallback for the one case the button can't handle: the Revert button is greyed
out or missing entirely when the revert can't be applied cleanly — usually because commits landed
on the target branch after the merge that now conflict with undoing it, or because the same
branch was merged more than once. When that happens, run git revert -m 1 locally,
resolve the conflict by hand, then push a branch and open the PR yourself with
a normal push to a remote branch and gh pr
create as shown above. Either route ends in the same place: a
github rollback merge adds a commit to main, it never erases the
one that's already there.
Whole Merge vs Individual Commits: What to Revert
Reverting the merge commit with -m 1 undoes the entire feature atomically — every
file change the branch introduced disappears in one commit, and the log keeps a clean pointer:
"this shipped in commit X, got pulled back out in commit Y." That's the right default when the
branch was one cohesive unit of work.
The alternative is reverting individual commits from inside the merged branch instead —
git revert <commit-sha> for each one you want gone, no -m needed
since those are ordinary single-parent commits once you're targeting them directly rather than
the merge itself. This lets you keep some of the work and drop the rest, but it gets
error-prone fast: if a later commit renames a function or extends a type that an earlier commit
introduced, reverting only one of them leaves the tree in a state that never actually existed and
often won't even compile. The decision rule is simple — revert individual commits only when
you've confirmed they're genuinely independent of each other; when in doubt, revert the whole
merge commit and bring back the parts worth keeping as fresh commits with a
cherry-pick from the original
branch instead of trying to selectively undo pieces of an interdependent chain.
If what you actually regret is a single ordinary commit rather than a whole merged branch, the
undo last commit guide covers the lighter-weight
tools for that — amend, soft reset, or a plain revert with no -m involved at all.
Common Errors: No -m Option and Wrong Mainline
Three failure messages cover almost every mistake made trying to git revert merge commit history. All of them are refusals to run — none of them leave a half-finished revert behind.
git revert a1b2c3d
# error: commit a1b2c3d is a merge but no -m option was given.
# fatal: revert failed
You pointed revert at a commit with more than one parent and didn't say which one
is the baseline. A merge commit's diff is ambiguous on its own, and git refuses to guess rather
than silently pick one. The fix is adding -m with the parent number you confirmed
using the commands from the parent-finding section above — most often -m 1.
git revert -m 2 9f8e7d6
# error: commit 9f8e7d6 does not have parent 2
# fatal: revert failed
The mirror-image mistake: you asked for a parent the commit doesn't have. That happens two ways
— you passed -m 2 against an ordinary single-parent commit, or you passed
-m 3 against a merge that only has two parents. Git checks the parent list before
it computes anything, so nothing is staged and nothing is committed. Confirm the parent count
with git log --pretty=%P -1 <hash> and re-run with a number that exists.
One asymmetry catches people out here, and it's worth knowing because it fails
quietly rather than loudly. Running git revert -m 1 against an ordinary
non-merge commit does not error — it just succeeds. Every commit that has a
parent at all has a parent 1, so git honours the request and reverts the commit exactly as if
you'd omitted the flag. That means a copy-pasted -m 1 aimed at the wrong hash won't
warn you; it produces a perfectly valid revert of something you didn't mean to touch. The hash
is what you need to double-check, not the flag. (Older git releases rejected this case with
mainline was specified but commit ... is not a merge, which is why that message
still shows up in tutorials — current git, verified on 2.50.1, no longer emits it.)
git revert -m 0 a1b2c3d
# error: option `mainline' expects a number greater than zero
Parent numbering is 1-indexed, not 0-indexed. There is no parent 0, and git rejects the value
during option parsing before it looks at the commit at all. If you came to git from an
array-indexed mental model, this is the one-line correction: the first parent is
-m 1.
git revert -m vs git reset for a Merge
Same undo intent, opposite blast radius — the whole rollback git merge decision
in one paragraph. git revert -m 1 adds a commit — reversible, safe on any branch, and
it never touches history anyone else already has. git reset --hard back to before
the merge moves the branch pointer and discards the merge commit outright; that's only safe if
the merge commit never left your machine, and even then it wipes
any uncommitted work with no exceptions. Once the merge is pushed, reset plus a force-push is
the wrong tool, full stop — it rewrites shared history and forces everyone else to reconcile a
diverged branch.
Four ways to undo a merge, side by side — what each does to history, whether it's safe once the merge is pushed, when to reach for it, and what happens if you try to bring the branch back later.
| Command | What it does to history | Safe on a pushed/shared branch | When to use | Re-merge consequence |
|---|---|---|---|---|
git revert -m 1 <hash> | Adds a new commit that undoes the merge's changes; original merge commit stays in history | Yes — purely additive, no rewrite | Merge is already committed and pushed, or you want a fully auditable undo either way | Falls into the re-merge trap — commits are already ancestors, so merging the branch again reports "Already up to date" |
git reset --hard HEAD~1 | Moves the branch pointer back one commit and discards the merge commit outright | No — rewrites history, forces a force-push and reconciliation for anyone who has it | Merge commit is still local only, confirmed with git log origin/main..HEAD | None — the merge commit is gone, so a later merge of the same branch brings its commits in fresh |
git merge --abort | Discards the in-progress merge entirely; no commit is ever created | N/A — nothing has been committed or pushed yet | Merge is still unresolved, MERGE_HEAD still exists, conflict markers still in your files | None — there's no merge commit, so trying again later is a completely fresh merge |
| GitHub Revert button | Opens a new PR containing the equivalent of git revert -m 1; merging it adds a revert commit | Yes — same additive mechanism as the CLI command, plus normal PR review | Merged PR needs undoing and you want the review trail on the rollback itself | Same re-merge trap as the CLI revert — the button doesn't remove ancestry either |
The full breakdown of what each reset mode preserves and destroys is in the dedicated
git reset --hard guide — this isn't the place to
re-teach it. And if the merge hasn't even finished committing yet — conflict markers still in
your files, MERGE_HEAD still present — reverting is the wrong tool entirely;
git merge --abort is cheaper and cleaner, since it
cancels the merge before any commit, revert or otherwise, needs to exist at all.
Verifying the Rollback Before You Push
Confirm the revert actually did what you meant before it ships, especially after any
-m decision that wasn't completely obvious.
# Compare the merge commit's pre-merge state against your current tip —
# should be empty or minimal if the revert fully undid the feature
git diff <merge-hash>^1 HEAD
# Quick summary of exactly which files the revert commit touched
git show <revert-commit> --stat git diff <merge-hash>^1 HEAD is the sharpest check: it compares the branch's
state right before the merge against where you are now. If the revert was clean and nothing else
changed since, that diff should be empty or close to it — anything large or unexpected there is
the same size-mismatch signal covered earlier, and it's worth catching before you push, not
after. Both commands emit standard unified diffs; the
unified diff format guide walks through reading the
+/- hunks if the notation is unfamiliar.
For a revert that touches a lot of files, scrolling through terminal hunks is slow and easy to
misread. This is where a visual side-by-side check pays for itself: paste the "before the merge"
and "after the revert" versions of a file into Diff Checker, and every added,
removed, and unchanged line is color-coded and lined up automatically — no squinting at plus and
minus signs. It runs on Monaco Editor's diff engine entirely in your browser, with a "Show Diff
Only" mode that collapses everything unchanged so a long file doesn't bury the two or three
regions that actually matter, plus Alt+Down and Alt+Up
to step through every change one at a time. Nothing you paste leaves your machine — there's no
upload, no account, and no git integration to configure; it's a plain two-pane text compare that
works on whatever you feed it, which is exactly what git show already extracted for
you. For comparing arbitrary file versions beyond a single revert, the
git diff between two files guide covers the
full range of scoping flags.
Frequently Asked Questions
What's the difference between -m 1 and -m 2 when reverting a merge?
-m 1 tells git revert to treat parent 1 — the branch you were
checked out on when you ran git merge, usually main — as the
baseline, and undo everything the other parent brought in. That is correct in roughly 95% of
merges: you were on main, you merged a feature branch in, and now you want the feature's
changes gone while main's own history stays intact. -m 2 flips it: it keeps
parent 2 as the baseline and undoes what parent 1 uniquely contributed instead. Only use
-m 2 when the parent order is actually inverted from the usual pattern, and
confirm that with git log --pretty=%P -1 <hash> rather than guessing —
picking the wrong number reverts the opposite set of changes.
What happens when you merge the same branch again after reverting it?
Nothing — and that's the trap. Git decides what a merge needs to bring in by walking commit
ancestry, not by diffing file contents. Because the feature branch's commits already became
ancestors of main the first time it was merged, they are still ancestors after
you revert; the revert commit removes the file changes but not the ancestry. Running
git merge feature-branch again finds no new commits to bring across and reports
"Already up to date," even though the revert already undid every line the branch introduced.
The fix is to revert the revert commit, or rebuild the branch so its commits get new SHAs
before merging again.
Can you undo a revert of a merge commit?
Yes. A revert is an ordinary commit, so you revert it the same way you'd revert anything
else: git revert <revert-commit-hash>, no -m flag needed
since the revert commit itself has only one parent. This creates a second commit that
reapplies the original merge's changes, and it also sidesteps the re-merge trap — reverting
the revert restores the feature's content directly instead of relying on a fresh merge, which
would find nothing new to bring in.
How do I revert a merged pull request on GitHub using the UI?
Open the merged pull request and click the Revert button near the bottom of the page. GitHub
does not delete or touch the original merge commit — it opens a brand-new pull request whose
diff is the equivalent of running git revert -m 1 against the merge commit
locally. You still have to review and merge that second PR for the rollback to take effect,
and on a protected branch it goes through the same required checks as any other PR. If the
button is greyed out or missing, the revert cannot be applied cleanly; fall back to running
git revert -m 1 locally, resolving the conflict, and opening the PR yourself
with gh pr create.
What does "commit is a merge but no -m option was given" mean?
It means you ran git revert against a commit that has more than one parent
without telling git which parent to treat as the baseline. A merge commit's diff is ambiguous
on its own — undo relative to which side? — so git refuses to guess and stops with that error
instead of reverting the wrong thing. The fix is adding the -m flag with the
correct parent number, most often git revert -m 1 <hash>, after confirming
the parent order with git log --pretty=%P -1 <hash>.
Is it safe to revert a merge that's already been pushed?
Yes — that is exactly the case git revert -m 1 is built for. It adds a new
commit that undoes the merge's changes instead of deleting or rewriting the merge commit, so
a plain git push afterward works with no force-push and no history rewrite.
Every clone that already has the merge commit simply pulls the revert commit like any other
new commit. The unsafe move on a pushed merge is git reset --hard plus a
force-push, which rewrites shared history and breaks every other clone of the branch.