skip to content

What closes the window for recovering unreachable commits in Git, and how do you keep it open?

level: seniorimportance: should knowfreq 36%

answer

  1. Unreachable is not the same as gone
  2. Something has to actually collect the garbage
  3. Two clocks: reflog expiry and prune grace
  4. gc.auto fires from ordinary commands
  5. Stop housekeeping before you investigate

basics

~20 s

Garbage collection closes it. Unreachable commits survive until gc expires the reflog entries holding them and prunes objects past the grace period, so recovery work starts by stopping housekeeping — set gc.auto to 0 and never run a prune with an immediate expiry.

solid answer

~40 s

Losing a commit costs nothing by itself; what actually destroys it is `git gc`. Housekeeping runs automatically when the `gc.auto` threshold of loose objects is crossed by ordinary commands, and it does two things that matter: it expires reflog entries (`gc.reflogExpire`, and `gc.reflogExpireUnreachable` for entries pointing at commits no longer reachable), and it prunes unreachable objects older than `gc.pruneExpire`. Defaults give you weeks, not minutes, which is why reflog recovery normally works. The commands that slam the window shut immediately are `git gc --prune=now`, `git prune --expire=now`, and `git reflog expire --expire-unreachable=now --all`. So my first move in a repository with missing work is to stop making it worse: `git config gc.auto 0`, take a copy of `.git` if the work is valuable, and only then investigate.

code

bash · 7 lines
bash
# First response in a repo with missing commits
git config gc.auto 0
cp -r .git /tmp/git-backup-$(date +%s)

# Check the effective policy before trusting the window
git config --get gc.pruneExpire
git config --get gc.reflogExpireUnreachable

go deeper

for a junior

Know that lost commits stick around for a while but not forever, and that running cleanup commands while work is missing is the one thing that can make it unrecoverable.

for a middle

Explain the two mechanisms by name: reflog entries expire on their own schedule, and unreachable objects are pruned only after a grace period, with gc invoked automatically once loose objects pile up.

for a senior

Lead the incident: suppress automatic gc, copy .git before experimenting, know exactly which commands close the window instantly, and anchor recovered hashes with a ref immediately rather than leaving them unreachable.

for a principal

Own the policy. Decide whether developer clones should run aggressive maintenance at all, and argue that recoverability should come from pushed branches and mirrors rather than from a prune grace period nobody has verified.

## Unreachable is a state, not a deletion When a reset, a rebase, a branch deletion, or a dropped stash removes the last ref pointing at a commit, the commit becomes *unreachable*. It is still a file (or a pack entry) in `.git/objects` and can be resurrected by pointing any ref at it. Recovery is therefore a race between you and garbage collection. ## What garbage collection does `git gc` performs housekeeping: it packs loose objects, packs refs, expires reflog entries, and prunes unreachable objects. Two of those steps are what close the recovery window. **Reflog expiry.** Reflog entries are themselves roots for reachability — a commit named by a reflog entry is not pruned. gc expires those entries on a schedule governed by two settings: `gc.reflogExpire` for entries whose commits are still reachable, and `gc.reflogExpireUnreachable` for entries pointing at commits that are no longer reachable from any ref. The stock defaults are generous — on the order of ninety days and thirty days respectively — which is exactly why "just check the reflog" works so reliably in practice. `git reflog expire` is the command that does this on demand. **Pruning.** Once nothing points at an object, gc will remove it, but only after a grace period controlled by `gc.pruneExpire`, whose default is a couple of weeks. The grace period is not politeness; it exists so that a concurrent process which has just written an object but not yet referenced it does not have that object deleted underneath it. ## Automatic runs You rarely type `git gc`. Many commands — commit, merge, rebase, fetch, am — invoke `git gc --auto`, which does nothing unless the repository has accumulated more loose objects or packs than `gc.auto` and `gc.autoPackLimit` allow. This matters because it means the window can close while you are still poking around: continuing to work in a repository where you have lost commits is itself a risk. ## Commands that close the window immediately - `git gc --prune=now` — prunes unreachable objects with no grace period. - `git prune --expire=now` — the plumbing version, same effect. - `git reflog expire --expire=now --all` and `--expire-unreachable=now --all` — throws away the reflog entries that were keeping commits alive, after which the next prune removes the objects. - `git stash clear` — drops all stash reflog entries at once, making every stashed commit unreachable. These appear in "speed up your repo" blog recipes and in cleanup scripts, which is precisely why an engineer who has just lost work should not be running someone's optimisation snippet. ## The first-response routine 1. Stop. Do not keep committing, fetching, or rebasing in that repository — each command can trigger automatic housekeeping. 2. `git config gc.auto 0` in that repository to suppress automatic runs while you investigate. 3. If the work is genuinely valuable, copy the whole `.git` directory (or the working copy) somewhere safe first, so that a mistaken recovery command cannot make the situation worse. A copy is cheap; a second failed recovery attempt is not. 4. Now do the forensics — reflog first, then fsck — and anchor whatever you find with a branch or tag straight away. 5. Undo the `gc.auto` change when you are done. ## Why anchoring immediately matters Finding a hash is not recovery. Until a ref points at the commit, it remains unreachable and still on the clock. `git branch rescue <hash>` takes a second and converts a race into a settled outcome. ## The strategic view The window exists because Git's data model separates object storage from reachability, and it is deliberately generous. But a window measured in weeks is still a window, and repositories that run aggressive maintenance, or clones that are recreated frequently, have effectively no window at all. The durable answer is not to depend on it: push topic branches, make WIP commits, and treat the reflog and prune grace period as a safety net for accidents rather than as a backup strategy.

  • Why does prune have a grace period at all instead of removing unreachable objects immediately?
    To avoid a race. A concurrent operation may have already written an object but not yet created the ref that makes it reachable; pruning with no grace period could delete that object mid-operation and corrupt the result. The delay also happens to give humans a recovery window, but safety against concurrency is the design reason.
  • How do reflog entries interact with pruning?
    Reflog entries act as reachability roots, so an object named by one is not pruned. Garbage collection therefore expires reflog entries first and prunes afterwards, with a shorter expiry for entries whose commits are already unreachable. Clearing the reflog is what actually exposes those objects to the next prune.
  • Your repository is on a machine where a nightly maintenance script runs aggressive cleanup. What changes?
    Effectively you have no recovery window, so the reflog stops being a safety net. The mitigations are organisational rather than technical: push topic branches early, commit work in progress rather than leaving it staged, and exclude developer clones from scripted pruning or at least keep the default grace period.

saying these in an interview costs you the question

  • Says unreachable commits are deleted the moment nothing points at them
  • Runs git gc --prune=now while trying to recover work
  • Treats the reflog as a backup rather than a short-lived safety net
  • Keeps working in the repository while investigating missing commits
  • Assumes default expiry windows apply without checking the config

context