Skip to content

Lab: Undo a Bad Merge

Lesson 3 of 6Intermediate4 min readHands-On Git & GitHub Labs · Git Recovery Labs
Time15 minutes
LevelIntermediate
You needLocal Git only — no GitHub account needed

A feature branch was merged into main. Ten minutes later the feature turns out to be broken. The merge has to come out.

There are two correct answers, and which one applies depends on a single question: has anyone else fetched the merge? Get it right and it is one command. Get it wrong and you either rewrite history under your colleagues, or you leave the bug in place while believing you removed it.

Undo the same bad merge both ways, understand when each is appropriate, and see for yourself why git revert of a merge leaves a trap for the next person who tries to merge that branch.

Terminal window
mkdir -p /tmp/lab-badmerge && cd /tmp/lab-badmerge
git init -q -b main .
git config user.email "lab@example.com"
git config user.name "Lab User"
echo "# Project" > README.md
git add . && git commit -q -m "Initial commit"
git switch -qc feature/broken
echo "bug" > broken.txt
git add . && git commit -q -m "Add feature (contains a bug)"
git switch -q main
echo "ok" > other.txt
git add . && git commit -q -m "Unrelated work on main"

Two branches that have diverged from the initial commit, so a merge will produce a real merge commit rather than a fast-forward.

  1. Merge the broken branch:

    Terminal window
    git merge feature/broken --no-edit
    git log --oneline --graph

    Note the merge commit at the top and its two parents.

  2. Undo it as if nobody has seen it. The merge exists only on your machine. Use the reference Git set aside for exactly this:

    Terminal window
    git reset --hard ORIG_HEAD
    git log --oneline --graph

    The merge commit is gone. What is ORIG_HEAD?

  3. Redo the merge, then undo it as if it has been pushed. Now imagine you pushed the merge an hour ago and three people have pulled. Resetting would rewrite history under them. Instead:

    Terminal window
    git merge feature/broken --no-edit -q
    git revert -m 1 HEAD --no-edit
    git log --oneline --graph
    ls

    broken.txt is gone from the working tree. The merge commit is still in history. Why -m 1?

  4. Hit the trap. The feature is fixed and you want to merge it again:

    Terminal window
    git merge feature/broken

    Read the output. Then run:

    Terminal window
    git log --oneline main..feature/broken

    What does the empty result tell you?

After step 2 the graph is linear with two commits. After step 3 it has a merge commit and a revert commit above it, and ls shows README.md and other.txt only. Step 4 prints Already up to date. — that is the trap, not a success.

Step 2. ORIG_HEAD is set by operations that move HEAD substantially — merge, rebase, reset — to where HEAD was before. It is the “undo” reference for the last such operation.

Step 3. A merge commit has two parents. Reverting it means “return to one of them”. -m 1 says mainline is parent 1 — the branch you were on when you merged. Without -m, Git refuses because it cannot guess.

Step 4. From Git’s point of view, feature/broken’s commits are already in main’s history — the merge commit is still there. The revert only undid the changes. Merging again brings in nothing, because there is nothing new to bring.

Step 1:

Merge made by the 'ort' strategy.
broken.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 broken.txt
* 2e42fdf Merge branch 'feature/broken'
|\
| * a707dde Add feature (contains a bug)
* | addee73 Unrelated work on main
|/
* f5813f4 Initial commit

Step 2 — reset, because the merge was never shared:

HEAD is now at addee73 Unrelated work on main
* addee73 Unrelated work on main
* f5813f4 Initial commit

Step 3 — revert, because it was:

[main bbc4060] Revert "Merge branch 'feature/broken'"
1 file changed, 1 deletion(-)
delete mode 100644 broken.txt
* bbc4060 Revert "Merge branch 'feature/broken'"
* 2e42fdf Merge branch 'feature/broken'
|\
| * a707dde Add feature (contains a bug)
* | addee73 Unrelated work on main
|/
* f5813f4 Initial commit

Step 4 — the trap:

Already up to date.

git log main..feature/broken prints nothing: every commit on the feature branch is already an ancestor of main.

Getting out of the trap. Once the feature is genuinely fixed, either revert the revert (git revert bbc4060), which reinstates the original changes, and then merge the fix commits; or rebase the feature branch so its commits are new and Git sees them as unmerged. The first is simpler to reason about.

Reset moves the branch pointer. git reset --hard ORIG_HEAD sets main back to where it was, and the merge commit becomes unreachable. Correct when the merge has not left your machine; wrong the moment someone else has it, because their main now contains a commit yours does not.

Revert adds a commit that undoes the changes. History is untouched and grows by one commit. Anyone who pulls gets the revert cleanly. This is the only safe option for shared history.

A reverted merge is still a merge. The graph still says feature/broken was merged, so Git will not merge it again. This surprises everyone the first time — the branch’s changes are gone but its commits are not. The lesson on reverting covers the recovery options.

git revert HEAD says “is a merge but no -m option was given”. Expected. Add -m 1.

Step 1 fast-forwarded instead of creating a merge commit. main had no commits of its own since the branch point. Check the starting state committed other.txt on main.

ORIG_HEAD points somewhere unexpected. It reflects the most recent merge, rebase or reset. If you ran another such command after the merge, it moved.

Terminal window
cd /tmp && rm -rf lab-badmerge

Reset, revert and restore — five situations, three commands, and the one that destroys work.

Choose a learning pathA sequenced route through the curriculum for wherever you are now.