Overwrote Commits with git push --force — Recovery and Prevention
Right after running git push --force, you discover that a teammate’s commits have disappeared from the remote branch. The remote history has been replaced wholesale by your local state, and the work your teammate pushed in the meantime is no longer visible on the branch. To lead with the conclusion: the overwritten commits are most likely still out there somewhere. Git does not delete commits immediately, and copies of the lost commits are usually scattered across several places. The copies do disappear one by one as time passes, though, so the faster you move, the easier the recovery.
This post walks through the three recovery paths in order of reliability, and ends with the settings that block this accident structurally.
What actually happened #
A force push moves the remote branch pointer to whatever commit your local branch points at, by force. Where a normal push refuses to proceed if the remote has commits you don’t have, a force push skips that check.
[Before the force push — remote]
A --- B --- C --- D ← main (D is your teammate's commit)
[Your local — history rewritten from B]
A --- B --- C' ← main
[After the force push — remote]
A --- B --- C' ← main (C and D vanish from the branch)One fact matters here. C and D were not deleted — they merely became unreachable, pointed to by no branch. Until the server’s garbage collection runs, the commit data itself remains, and knowing the hash is enough to attach a branch and bring them back. Complete copies of those commits also exist in your teammate’s local repository, and in some cases in yours.
Path 1 — a teammate’s local clone that has the commits #
This is the most reliable recovery path. The teammate who created the lost commit D (or anyone who pulled it) still has D intact in their local repository. Git repositories are peer copies of each other, so overwriting the remote with that copy settles everything.
This time the wrong state is the one on the remote, so the person doing the recovery has to force push. The one who runs it is the teammate who has the commits.
git fetch # inspect the current (wrong) remote state
git push --force-with-lease origin main # restore the remote from their local state--force-with-lease only overwrites when the remote still matches the state seen at the last fetch, so it prevents a second accident where the recovery collides with yet another push.
Path 2 — the remote-tracking reflog in your repository #
Even if the teammate is away or nobody has the lost commits, your own repository may hold the clue. If you fetched or pulled at any point before the force push, your reflog records where origin/main used to point.
$ git reflog show origin/main
f3d9a21 refs/remotes/origin/main@{0}: update by push: forced-update
8c4d2e7 refs/remotes/origin/main@{1}: fetch: fast-forward
1a2b3c4 refs/remotes/origin/main@{2}: fetch: fast-forwardThe entry labeled forced-update is the moment of the accident. The entry just before it, 8c4d2e7, is the commit the remote main pointed at before the force push — the lost commit D. The objects came down to your repository through that earlier fetch, so pushing this hash straight back to the remote branch restores it.
git push --force-with-lease origin 8c4d2e7:mainThe <hash>:main form is the syntax for setting the remote’s main to a specific commit without going through a local branch. After the recovery, run git fetch and confirm the remote state is back to normal.
Path 3 — records kept by the hosting service #
If no local clue exists anywhere, dig through the hosting service’s records. On GitHub, traces remain in three places.
- The Activity view — filtering the repository’s Activity page by force pushes shows the incident along with a comparison link to the commit before the overwrite. You can pick up the previous hash right there.
- The Events API — push events keep the previous state in a
beforefield.
curl -s https://api.github.com/repos/<owner>/<repo>/events \
| grep -B2 -A2 '"before"'- PR pages — if the lost commits were ever part of a pull request, their hashes remain in the PR’s commit list.
Once you have the hash, the objects are very likely still on the server, so fetch them into your repository and restore the remote branch.
git fetch origin 8c4d2e7
git push --force-with-lease origin 8c4d2e7:mainIf even this path is closed, the last resort is contacting GitHub Support. As long as the server-side garbage collection has not run yet, there is still room for them to help with the recovery.
Cleaning up after the recovery #
Even with the remote restored, one team-level step remains. Teammates who fetched or pulled between the accident and the recovery may still carry the wrong state locally. Announce the time of the accident and the completed recovery to the team, and ask anyone who synced during that window to run git fetch and then check with git status that their branches have not diverged from origin/main. Anyone who diverged should preserve their local work (splitting it onto a branch if needed) and realign with the remote.
Finally, confirm the cause. In most cases someone ran a rebase, had their push rejected, and reflexively added --force. The settings in the next section close off that path structurally.
Prevention — block it with settings #
- Make
--force-with-leasethe default habit instead of--force. It aborts the push when the remote holds commits you don’t know about, which filters out most accidents of this kind before they happen. The difference between the two options is covered in Practical Git Workflows #3: Interactive Rebase — Cleaning Up Commits with squash and fixup. - Block force pushes on protected branches. In GitHub’s branch protection, leaving Allow force pushes off (the default) for shared branches like main means the server rejects forced pushes regardless of anyone’s habits. Settings that stand in for human attention are the most reliable layer against this accident.
- Never rebase shared branches. This principle removes the very situations that make a force push feel necessary. The reasoning is laid out in Practical Git Workflows #2: rebase vs merge — Decision Criteria and the Golden Rule.
Wrapping up — the response sequence #
Here is the sequence for handling a force push accident.
- Stop any further pushes and tell the team about the accident. Moving before the copies disappear matters most.
- Find a teammate who still has the lost commits locally. If one exists, that teammate restores the remote with
--force-with-lease. - If not, find the previous hash in your own
git reflog show origin/mainand push it up with<hash>:main. - Failing that, recover the previous hash from GitHub’s Activity view, Events API, or PR pages, then fetch and restore.
- After the recovery, notify the team, and prevent a repeat with branch protection and the
--force-with-leasehabit.
Commits do not vanish as easily as it seems. Recovery comes down to calmly tracing the copies in the first few hours after the accident.