Recovering a Broken Repository
Three things went wrong in one week. Someone force-pushed over main and erased a day of merged work. A teammate botched a rebase and lost three commits off their feature branch. And a .env file with live API keys is now sitting in the repository history, already pushed to a public remote. None of these is the end of the world, but each demands a different response, and reaching for the wrong tool — or panicking and running git gc — can turn a recoverable problem into a permanent one.
The organizing question for all repository recovery is the same: is the data still reachable? Git almost never deletes anything immediately; it stops referencing it. The recovery playbook walks from "the data is right there in the reflog" through "it is dangling but findable" to "it must be rewritten out and the credential rotated" — in that order, because each step is more disruptive than the last.
The Reflog Is the First Stop
The reflog records every position HEAD and each branch has held locally, for about 90 days by default. A commit that looks lost after a reset, a rebase, or a checkout is almost always sitting in the reflog under its old SHA. This is the first command after any "I lost it" moment, before anything else and especially before any cleanup:
$ git reflog
a1b2c3d HEAD@{0}: rebase finished
9f8e7d6 HEAD@{1}: rebase: checkout main
4c5d6e7 HEAD@{2}: commit: the work I thought I lost
$ git reset --hard HEAD@{2} # recover the pre-rebase tip
Undoing a Bad Force-Push
A force-push over main overwrites the remote ref but does not destroy the old commits while something still references them. The overwritten tip is recoverable from the reflog of whoever last had it — often the person who pushed, or anyone whose local clone still points at the old SHA. Recover that SHA and force-push it back. Server-side reflog access is limited, which is why a local clone holding the old tip is the practical lifeline; GitHub's event API or branch-protection logs can surface the SHA when no local copy survives.
Dangling and Detached Commits
When no reflog entry covers a lost commit — a deleted branch whose reflog is gone, or a detached-HEAD commit that was never anchored — the commits survive as dangling objects until garbage collection reaps them. git fsck surfaces them so you can re-anchor a branch to the recovered SHA:
$ git fsck --lost-found --no-reflogs dangling commit 7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b $ git branch rescue 7a8b9c0d
Purging a Committed Secret
The leaked .env is a different category of problem, because the damage is already done. The instant a secret is pushed to a public remote it is compromised — scrapers index public pushes within seconds, so it has very likely already been read. The order of operations is non-negotiable: rotate the credential first, treating it as burned, and only then rewrite history to remove the blob. Deleting the file in a new commit does nothing; the old blob stays in history and remains clonable. Use git filter-repo (the deprecated filter-branch is slow and error-prone), then force-push the rewritten history.
$ git filter-repo --invert-paths --path .env $ git reflog expire --expire=now --all $ git gc --prune=now # only now, after recovery is done
The Force-Push Aftermath
Rewriting history changes every downstream SHA, so every collaborator's clone diverges from the rewritten remote. If you force-push silently, the next person to git pull re-merges the old history right back in, undoing the purge. The rewrite has to be coordinated: announce it, have everyone re-clone or hard-reset to the new history, and protect main so the accident class — force-push to a shared branch — cannot recur.
A Corrupted Object Store
Disk corruption that damages an object in .git/objects shows up as git fsck errors and operations that suddenly fail to read a SHA. The fix is to copy the intact object from another clone or remote that still has it — which is only possible if a second copy exists. This is why a second remote or a routine bare-clone backup is cheap insurance: with content-addressed storage, any clone holding the same object holds an exact, verifiable replacement.
git filter-repo — general-purpose history surgery: remove paths, replace text, rewrite authors, in one streaming pass. It is the official replacement for the unsafe filter-branch and the right tool when the job is anything beyond deleting a few files.
BFG Repo-Cleaner — a faster, narrower tool focused on two jobs: stripping large blobs and scrubbing secrets by text replacement. Reach for it when those two tasks are all you need; otherwise use filter-repo. Both replace filter-branch.
- Assuming a secret is safe once you delete the file in a new commit and never rotating the key — the blob is still in history, already scraped, and the credential is compromised.
- Running
git gcor a prune while trying to recover lost commits, destroying the reflog entries and dangling objects the recovery depended on. - Force-pushing a history rewrite without telling collaborators, so everyone's local
maindiverges and re-merges the bad history back on the next pull. - Using
git push --forceinstead of--force-with-lease, clobbering a teammate's commits you had not fetched. - Treating the reflog as permanent insurance — it is local-only and expires, so a fresh clone carries none of your recovery breadcrumbs.
- Run
git reflogbefore any other recovery step; the lost SHA is usually right there. - Rotate any committed credential immediately, then purge it from history with
git filter-repoor BFG. - Use
--force-with-leasefor every force-push so you never overwrite unfetched work. - Enable branch protection on
mainto block force-pushes and prevent the whole accident class. - Keep a second remote or a routine bare-clone backup as insurance against object-store corruption.
- Surface dangling commits with
git fsck --lost-foundand re-anchor them withgit branch <name> <sha>before any prune.
Knowledge Check
A live key is committed and pushed to a public repo. What must happen first?
- Rotate the credential, treating it as compromised, before rewriting history
- Rewrite history to remove the blob, after which the key is safe to keep
- Delete the file in a new commit, which removes it from history
- Make the repo private, which retroactively secures the key
Why can running git gc during recovery make things worse?
- It can prune the reflog entries and dangling objects the recovery depends on, deleting them for good
- It corrupts the working tree, forcing you to discard it and re-clone the repository fresh from the remote to keep going
- It pushes the pruned local state to the remote and overwrites it, discarding the upstream copies you were counting on as a backup
- It has no effect on recovery at all; the concern is entirely unfounded
Why prefer git push --force-with-lease over --force?
- It aborts the push if the remote moved since your last fetch, so you cannot clobber a teammate's commits
- It is faster because the lease lets it skip the diff computation and stream the rewritten objects straight to the remote without comparing refs
- It pushes the new commits without rewriting any existing history, appending them on top of the remote branch as an ordinary fast-forward
- It automatically notifies every collaborator that the history was rewritten
A commit is lost and no reflog entry covers it. What finds it?
git fsck --lost-found, which surfaces dangling commits surviving until garbage collectiongit pull, which re-downloads the lost commit from the remote and replays it onto your current branch tipgit status, which lists the unreachable objects in the store- Nothing; with no reflog entry left, that commit is already gone for good and no command in Git can bring its objects back
You got correct