How to safely undo a git push --force
Table of Contents
Somebody force-pushed. Your main (or worse: someone else’s feature branch) now has commits missing that everyone had already fetched. Panic is optional; recovery is fast if you move before Git’s garbage collector does.
Recovery Assumes Someone Still Has the Old Commits #
Force-push doesn’t delete objects from GitHub immediately - they linger until garbage collection, which runs on GitHub’s schedule (usually weeks). And more importantly, any teammate who fetched before the force-push still has the old commits in their local reflog.
Step 1: Find Someone Whose Local Clone Was Recent #
On any machine that fetched before the disaster:
git reflog show origin/main --date=iso
That prints every state origin/main was ever in on this clone, most recent first. Find the SHA from just before the force-push - it’s the one right before the entry that shows the divergence.
Step 2: Push the Old Tip Back Under a Recovery Branch #
git branch recovery <sha-of-the-old-tip>
git push origin recovery
Now the old commits are safe on the remote under recovery.
Step 3: Fast-Forward main Back (if that’s what you want) #
If the force-push overwrote main with something you don’t want at all:
git push origin +recovery:main
If the force-push overwrote main with something you do want on top of the recovered commits, merge or rebase recovery in instead.
If Nobody Has a Local Reflog #
You can still recover from GitHub if the force-push was recent enough. GitHub’s activity feed exposes the SHAs:
gh api repos/OWNER/REPO/events --paginate | jq '.[] | select(.type=="PushEvent") | .payload'
The before value on the relevant PushEvent is the old tip.
You can also visit https://github.com/OWNER/REPO/activity in the UI - the force-push shows up with the pre-and-post SHAs and a “Restore” button on branch protection events.
The Real Fix: Never --force Again #
Use --force-with-lease instead:
git push --force-with-lease
It only force-pushes if your local view of the remote is up to date. If someone else pushed in the meantime, it refuses. Same power, guardrail included.
Set it as your muscle-memory alias:
git config --global alias.pushf 'push --force-with-lease'
Now git pushf is what your fingers reach for, and you can’t accidentally overwrite what you didn’t know was there.