Force Push Overwrote Teammates' Commits

A git push --force wiped teammates' work from a shared branch. Recover the lost commits and prevent it from happening again.

Your teammate ran git push --force origin main after a local rebase and the branch pointer jumped back two days of merged work. GitHub shows the branch history now ends at your colleague’s commit from Tuesday, and every PR that was merged since then is gone from the branch tip — though they still exist as objects in the repo. The situation looks catastrophic but is almost always fully recoverable: force-push only moves a pointer, it does not delete objects. This article walks through finding the overwritten commits, merging them back, and setting up safeguards so no single --force can do this again.

Common causes

Ordered by hit rate, highest first.

1. Developer rebased local branch and force-pushed to a shared branch

The most common cause. Someone ran git rebase main on develop locally then git push --force origin develop without realizing teammates had pushed commits to origin/develop since their last fetch.

How to spot it: Compare the reflog on the remote (GitHub/GitLab “force push” event in the branch activity log) with local git log --oneline origin/develop — the divergence point is the overwrite boundary.

2. git push --force used instead of --force-with-lease

A script or CI pipeline configured with --force (not --force-with-lease) re-ran after a delay, overwriting commits pushed in the interim.

How to spot it: Check CI pipeline logs for git push --force without --force-with-lease. The timestamp of the pipeline run matches the timestamp of the lost commits.

3. Amend + force-push to fix a typo in a commit message

Developer amended the last commit to fix a typo, then force-pushed. If the remote had one more commit on top (merged seconds earlier), that commit is now orphaned.

How to spot it: The number of commits on the remote branch dropped by exactly one after the force-push event.

4. Force-push during an automated release script

Release tooling bumps a version, commits, tags, and force-pushes main. If the window between the script’s git fetch and git push --force was long enough for a hotfix to land, the hotfix is orphaned.

How to spot it: Cross-reference the release script’s run time with the “last commit before loss” timestamp in the repo’s event log.

5. Mistyped branch name in the force-push command

Developer meant git push --force origin feature/my-fix but typed feature/main or just main due to tab-completion mismatch.

How to spot it: The force-pushed branch is unrelated to the developer’s task. The branch activity log shows one lone force-push event from an unexpected author.

Shortest path to fix

Step 1: Find the last good SHA from a teammate’s local clone

The remote reflog is available on GitHub/GitLab under the branch’s “activity” or “push events” API. But faster: ask a teammate who pulled recently to run:

git log --oneline origin/main | head -10

Their local origin/main ref still points to the pre-overwrite history if they fetched before the force-push. Note the SHA immediately before the force-push — call it GOOD_SHA.

Step 2: Fetch from the teammate’s clone as a remote

# On your machine, add their local repo as a temporary remote
git remote add teammate /path/to/their/local/clone
git fetch teammate

Or, if they push to a personal fork:

git fetch origin refs/heads/main:refs/remotes/origin/main-backup

Step 3: Create a recovery branch at the last-good SHA

git branch recovery/main-preforce GOOD_SHA
git log --oneline recovery/main-preforce | head -10

Confirm the recovery branch contains the missing commits.

Step 4: Merge the recovery branch back into main

git checkout main
git merge recovery/main-preforce --no-ff -m "recover: restore commits lost in force-push on $(date -u +%Y-%m-%d)"

Step 5: Push the restored branch

# Force-with-lease checks that the remote tip hasn't changed again
git push --force-with-lease origin main

Step 6: Verify the restored history

git log --oneline -20
git shortlog -s -n HEAD~20..HEAD   # confirm all authors' commits are back

Prevention

  • Replace every git push --force in scripts and documentation with git push --force-with-lease. The lease flag refuses the push if the remote has commits you have not fetched.
  • Enable branch protection on main/develop on GitHub, GitLab, or Bitbucket: disallow force pushes, or restrict force-push permission to repository admins only.
  • Add a pre-push hook that blocks force-pushes to protected branch names:
# .git/hooks/pre-push  (chmod +x)
while read local_ref local_sha remote_ref remote_sha; do
  if [[ "$remote_ref" == "refs/heads/main" ]]; then
    echo "ERROR: Direct push to main is not allowed. Open a PR."
    exit 1
  fi
done
exit 0
  • Set receive.denyNonFastForwards = true on self-hosted Git servers to block all non-fast-forward pushes at the server level.
  • Run git fetch --all before any rebase that will be followed by a push, and check git log HEAD..origin/branch is empty before pushing.
  • Require at least one reviewer on PRs to main — force-push overwrites are often attempted to “clean up” commits that should have gone through review.
  • Keep automated release scripts on a bot account with push access only to tags, not to main directly.

FAQ

Q: The GitHub web UI shows “force-pushed” in the PR timeline. Can I restore directly from GitHub? A: Yes, in some cases. GitHub retains the pre-force-push SHA in the PR timeline. Click the “force-pushed” event line — it often shows the old tip SHA. Use git fetch origin <old-sha> to retrieve it if the object still exists on the remote (GitHub keeps unreachable objects for 90 days).

Q: What is the difference between --force and --force-with-lease? A: --force pushes unconditionally, overwriting whatever is on the remote. --force-with-lease first checks that the remote tip matches your locally cached origin/<branch> — if someone pushed in the meantime, your push is rejected and you must fetch first.

Q: Can I configure Git globally to always use --force-with-lease when I type --force? A: Not via a flag alias, but you can add a shell alias: alias gpf='git push --force-with-lease'. Add it to your .zshrc or .bashrc.

Q: Branch protection is enabled on GitHub but someone still force-pushed. How? A: Repository admins can bypass branch protection by default. On GitHub you can enable “Include administrators” in the protection rule, which removes the admin bypass.

Tags: #git #version-control #Troubleshooting