Undoing a bad merge on a shared branch Jump to heading

A pull request merged to main breaks production, and it needs to come out now. On a private branch you would reset and force-push. On a shared branch that is the wrong move: everyone who fetched since the merge now has history the server no longer agrees with, and the next person to push reintroduces it. The safe tool is git revert -m 1, which adds a new commit undoing the merge’s changes while leaving history intact. The catch comes later, when the fixed version of the work is ready to merge again — Git believes those changes are already in main, and they silently fail to reappear. This page covers the revert and, just as importantly, how to bring the work back correctly, within history rewriting and recovery.

When to use this approach Jump to heading

  • A merge commit on a shared branch introduced a problem that must be undone quickly.
  • Other people have already fetched or built on top of the merge.
  • The work will be fixed and merged again later.
  • For a squash-merged pull request, an ordinary git revert <sha> is enough; the reintroduction concerns below still apply. Choosing between revert and reset in general is covered in when to use git revert vs git reset.

Step 1 — Revert the merge relative to its first parent Jump to heading

A merge commit has two parents; the revert must undo the changes relative to main’s side. That is parent 1 when the merge was made on main.

git switch main && git pull --ff-only
git log -1 --format='%h %s%nparents: %p' 5a9e3c1      # confirm it is the merge commit
git revert -m 1 5a9e3c1
git push origin main
Reverting a merge leaves history intactThe pull request's commits P1 and P2 were merged into main as M. The revert R is a new commit on main whose changes undo everything M added relative to its first parent. P1 and P2 remain in history, which is what makes re-merging tricky later.history keeps the merge; the revert cancels its effectmainAMRfeatureAP1P2effect+feature−featuremain's tree after R matches A again, but P1 and P2 are still ancestors of main Reverting a merge leaves history intactThe pull request's commits P1 and P2 were merged into main as M. The revert R is a new commit on main whose changes undo everything M added relative to its first parent. P1 and P2 remain in history, which is what makes re-merging tricky later.history keeps the merge; the revert cancels its effectmainAMRfeatureAP1P2effect+feature−featuremain's tree after R matches A again, but P1 and P2 are still ancestors of main

The revert commit’s message names the merge it undoes. Add one line saying why — the incident or bug — so the history explains itself.

# Verification: main's tree no longer contains the merged changes
git diff 5a9e3c1^1 HEAD --stat     # expect empty, or only commits made after the merge

Step 2 — Understand why re-merging the same branch does nothing Jump to heading

After the revert, the feature’s commits are still ancestors of main. When the feature branch gets a fix and is merged again, the merge base is the old tip of the feature branch, so Git only considers the new fix commit as the feature’s change. The original work — undone by the revert — stays undone.

Re-merging after a revert, two waysMerging the fixed branch directly brings in only the new fix commit, because the original commits are already ancestors of main, so the revert still wins and the feature stays missing. Reverting the revert first restores the original changes, and then merging the fix applies cleanly on top.Merge fix directlyRevert the revert, then mergeoriginal workstays undonerestorednew fixappliedappliedresulthalf a featurewhole featurethe revert is a change on main like any other — it must be undone explicitly Re-merging after a revert, two waysMerging the fixed branch directly brings in only the new fix commit, because the original commits are already ancestors of main, so the revert still wins and the feature stays missing. Reverting the revert first restores the original changes, and then merging the fix applies cleanly on top.Merge fix directlyRevert the revert, then mergeoriginal workstays undonerestorednew fixappliedappliedresulthalf a featurewhole featurethe revert is a change on main like any other — it must be undone explicitly
# Demonstrate: what would a direct re-merge bring in?
git diff main...feature/search --stat      # only the new fix, not the original work

Step 3 — Bring the work back by reverting the revert Jump to heading

When the fixed version is ready, first revert the revert on a branch, then merge the fix on top, and review both together.

git switch -c reland/search main
git revert <sha-of-revert-commit>          # brings the original changes back
git merge --no-ff feature/search           # adds the fix made since
git push -u origin reland/search
gh pr create --base main --title "Re-land search (reverts revert of #812, adds fix)" --fill

Reviewers should look at the combined diff against main, which is exactly what will land. It should be the original feature plus the fix.

# Verification: the re-land contains the original work and the fix
git diff main...reland/search --stat

Step 4 — Alternative: rebuild the branch with new commits Jump to heading

If the feature branch will be reworked substantially, a clean alternative is to recreate its commits so they are not already ancestors of main. Rebasing the branch onto main with --no-ff style replay — or simply rebasing it onto the revert — produces new commit hashes, and a normal merge then brings everything in.

git switch feature/search
git rebase --force-rebase main      # replay every commit as a new one, even if unchanged
git log --oneline main..HEAD        # now the full feature appears as new commits

⚠️ SAFETY WARNING: --force-rebase rewrites every commit on the feature branch, so the branch must be force-pushed and anyone else working on it must reset to the new version. Use git push --force-with-lease. If the rewrite goes wrong, git reset --hard ORIG_HEAD immediately afterwards restores the previous branch tip.

Step 5 — Make the sequence visible to the team Jump to heading

Reverts and re-lands are easy to misread in history. Use a consistent message convention, and link the incident, the revert and the re-land together.

Revert "Add search index rebuild (#812)"
This reverts commit 5a9e3c1, reversing changes made to 2d4b6f8.
Reason: INC-4471, index rebuild locked the orders table.

Re-land "Add search index rebuild (#812)" with batched rebuild
Reverts the revert in 8c71a02 and adds batching (#839).
The life of a reverted pull requestA pull request merges and causes an incident. It is reverted with -m 1 within minutes. A fix is developed on the original branch. The re-land reverts the revert and merges the fix in one reviewed pull request.Merged#812Mon 10:02Incidentorders table lockedMon 10:40Revertedrevert -m 1Mon 10:51Fix readybatched rebuildWedRe-landedrevert of revert + fixWedno history was rewritten at any point, so nobody had to re-clone The life of a reverted pull requestA pull request merges and causes an incident. It is reverted with -m 1 within minutes. A fix is developed on the original branch. The re-land reverts the revert and merges the fix in one reviewed pull request.Merged#812Mon 10:02Incidentorders table lockedMon 10:40Revertedrevert -m 1Mon 10:51Fix readybatched rebuildWedRe-landedrevert of revert + fixWedno history was rewritten at any point, so nobody had to re-clone

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why not just reset main and force-push? Jump to heading

Because everyone who fetched since the merge now has commits the server has dropped, and their next push or pull reintroduces them or causes confusing divergence. A revert is an ordinary commit that every clone can fast-forward to.

What if the merge was squash-merged? Jump to heading

There is no merge commit, so git revert <squash-sha> without -m undoes it. Re-landing is simpler: the squash commit is not an ancestor of the feature branch, so merging a new squash of the fixed branch works — but the revert must still be reverted, or its change will conflict with the new squash.

Can a revert itself conflict? Jump to heading

Yes, if later commits on main changed the same lines the merge introduced. Resolve it like any conflict, keeping the later commits’ intent while removing the merged work.