In Git, which objects does git gc treat as reachable, and what happens to the rest?
answer
- no reference counts anywhere
- start from roots, follow the edges
- the index and the reflog are roots too
- deletion waits out an age-based grace
- fsck names the tips of the orphan graph
basics
~20 sGit walks out from every ref, HEAD, the index and reflog entries, following commits to their parents, trees and blobs. Anything not reached is unreachable and eventually pruned, but only after a grace period based on object age.
solid answer
~50 sReachability is a graph traversal from a set of **roots**: every ref under `refs/heads`, `refs/tags`, `refs/remotes` and `refs/stash`, the `HEAD` of every worktree, the entries currently in the index, and every object ID recorded in a reflog. From each root Git follows commit parents, commit-to-tree, tree-to-subtree and tree-to-blob edges. Everything reached is kept; everything else is **unreachable**. `git gc` does not delete unreachable objects immediately: it first expires reflog entries (`gc.reflogExpire`, default 90 days; `gc.reflogExpireUnreachable`, default 30 days), then prunes loose unreachable objects older than `gc.pruneExpire` (default two weeks), keeping newer ones so that concurrent work is not destroyed. `git gc --prune=now` skips that grace. `git fsck --unreachable` lists what would go; **dangling** is the narrower label fsck uses for an unreachable object that no other unreachable object points at — the tips of the orphaned subgraph.
code
bash · 3 linesgit fsck --unreachable --no-reflogs
git count-objects -v
git gc --prune=nowgo deeper
Know that Git keeps an object as long as something points to it, and that branches, tags and HEAD are the obvious pointers. Recall that gc cleans up the rest.
Explain the traversal and name the less obvious roots — the index, remote-tracking refs and the reflog — plus the fact that pruning waits out an age-based grace period rather than deleting immediately.
Demonstrate diagnosis: use fsck with --unreachable and --no-reflogs to separate what is genuinely garbage from what only the reflog is holding, and reason about packed versus loose unreachable objects.
Own the policy: retention windows for reflog and prune are a safety-versus-footprint decision, and shortening them across a fleet changes what any incident response can still recover.
## Reachability is the only garbage-collection rule Git has no reference counts and no explicit delete. An object is worth keeping if and only if you can get to it by starting at a root and following edges. That is the entire model, and every surprising garbage-collection behaviour follows from it. The edges are: a ref points at a commit (or, for annotated tags, at a tag object that points at a commit); a commit points at its tree and at each of its parents; a tree points at blobs and subtrees; a tag object points at its target. Traversing all of that from the roots yields the live set. ## What counts as a root - **Refs** — everything under `refs/`, whether stored as a loose file in `.git/refs/` or as a line in `.git/packed-refs`. That includes `refs/heads/*` (branches), `refs/tags/*`, `refs/remotes/*` (remote-tracking branches) and `refs/stash`. - **HEAD** — including the `HEAD` of every linked worktree, which is why work done in a detached HEAD in a second worktree is not garbage. - **The index** — a blob you have `git add`ed but never committed is reachable, so staged work is not collected. - **Reflogs** — `.git/logs/HEAD` and `.git/logs/refs/**` record the old and new object ID of every ref movement. Each of those IDs is a root for as long as the entry lives. This is what makes a commit you reset away survivable: the branch no longer points at it, but the reflog entry still does. That last point is the one interviewers probe. A commit orphaned by `git reset --hard`, an amend, or a rebase is *not* unreachable — it is reachable through the reflog, and stays that way until the reflog entry expires. ## Unreachable versus dangling `git fsck --unreachable` reports every object the traversal did not reach. Among those, fsck labels an object **dangling** when no other unreachable object refers to it: the roots of the orphaned subgraph. An orphaned commit typically shows as dangling, while its tree and blobs are merely unreachable because the dangling commit points at them. Both are equally eligible for pruning; the distinction is a reporting convenience for finding the useful entry points. Adding `--no-reflogs` makes fsck ignore reflogs, which is how you see what would be lost once reflogs expire. ## What gc actually deletes, and when `git gc` is a wrapper. Roughly, it expires reflogs, repacks, and prunes: 1. `git reflog expire` drops entries older than `gc.reflogExpire` (default 90 days) and entries pointing at objects that are already unreachable older than `gc.reflogExpireUnreachable` (default 30 days). Removing reflog entries is what turns reachable-only-via-reflog objects into genuine garbage. 2. Repacking writes a fresh pack of the reachable objects. Unreachable objects that were sitting inside an old pack are not silently dropped — `git repack -A` writes them back out as loose objects so that the age-based grace period below applies to them, rather than deleting them the instant a pack is rewritten. 3. `git prune --expire <gc.pruneExpire>` deletes loose unreachable objects whose file modification time is older than the cutoff — two weeks by default. The grace period exists because the object store has no locking against a concurrent process that has written an object but not yet pointed a ref at it. Keeping recent unreachable objects makes that window safe in practice. `git gc --prune=now` and `git prune` with no `--expire` remove the grace and delete everything unreachable at once — which is exactly why they are the dangerous form. Note also that `git prune` operates on loose objects. Unreachable objects that remain packed are removed by rewriting the pack, not by prune, which is why an object can persist long after you expected it to vanish. ## Why `gc --auto` runs behind your back Many commands invoke `git gc --auto`, which does nothing unless loose objects exceed `gc.auto` (default 6700) or packs exceed `gc.autoPackLimit` (default 50). With `gc.autoDetach` on it runs in the background. So repositories get packed and pruned without anyone ever typing `git gc`, and the practical question is never "did gc run" but "was the object still reachable, or still inside its grace window, when it did". ## Practical reading of the model To answer "will this object survive", ask three questions in order: is any ref, HEAD, index entry or reflog entry a path to it; if not, is it loose and younger than `gc.pruneExpire`; and if it is packed, has anything repacked since it became unreachable. `git fsck --unreachable --no-reflogs` plus `git count-objects -v` answers all three from the command line.
- Why does removing reflog entries matter more than running prune?Because reflog entries are roots. While `.git/logs/refs/heads/main` still records the pre-reset object ID, the orphaned commit is reachable and no amount of pruning touches it. Expiring the reflog is the step that reclassifies it as garbage; prune then only enforces the age cutoff on the now-unreachable object.
- An object is unreachable but still inside a packfile. Does git prune delete it?No. `git prune` removes loose unreachable objects. A packed unreachable object only disappears when the pack is rewritten — for example by `git gc` or `git repack -a -d`. With `git repack -A` such objects are exploded back to loose form instead, so the mtime-based grace period applies to them.
- What is the difference between an unreachable object and a dangling one in git fsck output?Unreachable means the traversal from refs, HEAD, the index and reflogs never arrived at it. Dangling is the subset of unreachable objects that nothing else unreachable points at — the tips of the orphaned subgraph. Both are equally eligible for pruning; dangling is just the useful list to read first.
saying these in an interview costs you the question
- Thinks Git reference-counts objects like a runtime GC
- Believes reset --hard immediately deletes the old commit
- Says only branches count as reachability roots
- Assumes gc deletes every unreachable object instantly
- Confuses dangling with corrupted or broken objects