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).
Using this in CI
CI checkouts are shallow and detached by default, which changes the answer this command gives you. Commands that read history, branch names, or tags need the checkout configured for it.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # history, tags, and git describe all need this
- run: |
git rev-parse --is-shallow-repository # expect false
git rev-parse --abbrev-ref HEAD # prints HEAD when detached