git reflog expire: Recover and Prune the Reflog
git reflog lists every position HEAD has held, so a commit dropped by reset or rebase is recoverable; git reflog expire removes old entries and can force gc to drop them.
The reflog is the undo buffer for history surgery. Knowing how to read it recovers work after a bad reset; knowing how to expire it matters on persistent runners where stale reflogs keep dead objects alive and bloat .git.
What it does
Each branch and HEAD has a reflog of its recent tip positions. git reflog (or git reflog show HEAD) lists them as HEAD@{n}. You recover by resetting to one. git reflog expire deletes entries older than a cutoff; with --expire-unreachable it targets entries whose commits are no longer reachable, freeing them for gc.
Common usage
git reflog
git reset --hard HEAD@{2} # undo the last two HEAD moves
# expire entries to let gc reclaim dropped commits now
git reflog expire --expire=now --all
git reflog expire --expire-unreachable=now --all
git gc --prune=nowOptions
| Flag | What it does |
|---|---|
| show | Print the reflog (default subcommand) |
| expire --expire=<time> | Remove entries older than <time> (e.g. now, 2.weeks) |
| --expire-unreachable=<time> | Target only entries pointing at unreachable commits |
| --all | Apply to every ref, not just one |
| delete <ref>@{n} | Remove a single reflog entry |
| --rewrite / --updateref | Adjust surrounding entries / move the ref |
In CI
Fresh clones in CI have an empty reflog, so do not rely on HEAD@{n} surviving across jobs. On a persistent or cached runner, "lost" commits stay on disk because the reflog references them; git reflog expire --expire=now --all followed by git gc --prune=now actually frees the space.
Common errors in CI
"fatal: Log for 'HEAD' only has N entries" means you asked for HEAD@{n} beyond what exists, common on shallow or fresh clones. "fatal: ambiguous argument 'HEAD@{2}'" with no reflog means core.logAllRefUpdates is off (bare repos disable reflogs by default).