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.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.