290 words, 2 min read

Sometimes you push a merge commit you didn't mean to, and notice it only after the fact. Here's how we handled it cleanly when that happened on a feature branch.

My branch history looked like this:

abc1234 Fix for the feature we were working on ← latest commit, must keep
def5678 Merge branch 'feature/my-branch' of ... ← mistake
ghi9012 Previous commit
jkl3456 Older commit

The merge commit def5678 was already pushed to GitHub. The latest fix (abc1234) sat on top of it, so a plain git reset --hard HEAD~1 would have taken the fix with it.

I needed to drop exactly one commit from the middle of the history while keeping everything above it. git rebase --onto is the right tool for this:

git rebase --onto ghi9012 def5678 HEAD

This tells Git: take all commits after def5678 up to HEAD, and replay them directly onto ghi9012 — the commit before the bad merge. The merge commit is skipped entirely.

The rebase left us in a detached HEAD state, so we updated the branch pointer and switched back:

git branch -f feature/my-branch HEAD
git checkout feature/my-branch

The history was now clean:

xyz7890 Fix for the feature we were working on
ghi9012 Previous commit
jkl3456 Older commit

Then a force-push to update the remote:

git push --force

Why not git revert?

git revert -m 1 <hash> is the safer choice when multiple people have already pulled the branch, because it adds a new commit rather than rewriting history. In our case the branch was a personal feature branch with no other active collaborators, so rewriting was fine and kept the history cleaner.

When in doubt on a shared branch, revert. On a solo feature branch, rebase is cleaner.