---
title: Reflog
source: 'https://academia.sh/en/courses/advanced-git/reflog'
course: 'Advanced Git'
language: en
updated: '2026-08-17T18:10:43+00:00'
license: 'CC BY-SA 4.0'
---

# Reflog

Rewriting does not shorten history, it doubles it: in a thousand-commit repository, a transformation from the first commit leaves 1000 commits reachable and 1000 unreachable; in a six-entry log, when the retention horizon is 7, 30, 60, and 90 days, the recoverable commits are 3, 50, 500, and 1000.

Two of the previous lesson's recovery paths looked at the same place: the local record
holding the branch tip's old value. Bringing back an overwritten branch and recovering
stranded local work both started from that record. The lesson ended with a warning
there — that record has a lifespan.

This lesson measures that lifespan. Two questions are asked, and they are separate.
First: after a rewrite, what stays inside the repository, how many commits are
reachable and how many are not. Second: how long do the unreachable ones stay
recoverable, at what moment does the window close, and what is lost when it does.

## Being Unreachable Is Not Ceasing to Exist

This course's first lesson said that rewriting is not a deletion but a duplication. The
measurement turns that sentence into a number. When a thousand-commit history is
rewritten, the new chain is also a thousand commits; the old chain's commits go nowhere.
The repository now holds two chains: one reachable from the branch tip, the other pointed
to by no reference at all.

For an object to be **reachable** means it can be found by starting from a branch, a
tag, or `HEAD` and following parent links. An unreachable object cannot be found by this
walk; but it stays on disk, is read if its identity is given, and stays there until
**garbage collection** runs. When rewriting finishes, the repository does not shrink, it
grows.

## The Log Holds Movements, Not Commits

The way to recover the unreachable chain is to know the old tip's identity. The tool
records this on its own: every move of every reference is written to a local log. This
is the **reflog**.

The unit of the record is this lesson's most important distinction. The log holds not
**commits** but **movements**. A rewrite that leaves five hundred commits unreachable
does not write five hundred lines to the log; it writes one line. That line holds the
tip at that moment, and walking backward from the tip reaches all five hundred commits.
The number of entries and the number of recoverable commits are therefore not the same
thing.

The log has three limits. **It is local:** it does not travel to the other side, is not
cloned, and one copy's log does not recover another copy. **It is per reference:** every
branch has its own log, and `HEAD` additionally keeps a general one. **It is assumed to
be indefinite but is not:** entries are cleaned up according to a retention horizon, and
that horizon is a setting.

```text
# taught command and example dump — not executed

$ git reflog metrics
5b6c7d8 metrics@{0}: rebase (finish): refs/heads/metrics onto 9c4d7e1
2f8a1c3 metrics@{1}: commit: metrics: print threshold value
7e1b4d2 metrics@{2}: commit: metrics: split config file

$ git branch recovery/metrics metrics@{1}
```

The second line is the entire recovery: a branch name is given to the old tip, and that
chain becomes reachable again. The operation deletes nothing and moves no reference; this
is why it can be written here in full form. Recovery paths are not destructive — what is
destructive is the operation that made them necessary.

There is a second tool for a case the log cannot show: the subcommand that checks the
repository's integrity lists objects pointed to by no reference as **dangling**. If an
entry has been cleaned but garbage collection has not run yet, recovery is done from
there. That, too, is a read operation.

The third limit's most common practical cost is this: **when a branch is deleted, its
log is deleted with it.** Looking at a mistakenly deleted branch's own record is no
longer possible at that point. In this case the remedy is `HEAD`'s log — the tips the
working area stood on are additionally kept there, and the deleted branch's last tip
usually appears in that list. Keeping the two logs separate is not a detail, it can be
the only recovery path left.

When objects are actually cleaned up also has to be written down. **Garbage collection**
runs on its own; it is triggered during maintenance operations and can also be called
manually. But for an unreachable object to actually be cleaned up, two conditions have to
be met at once: the log entry pointing to it has to have fallen outside the horizon
**and** collection has to have run. Until both conditions are met, the object stays in
place. The horizon itself is not a single number either: the entries for reachable tips
and the entries for unreachable tips can be set separately, and the latter is usually
kept shorter.

The measurement's assumptions:

- **RH25** — The setup is the previous lessons' setup and is unchanged; the measurement
  is done on a **1000**-commit history.
- **RH26** — Rewriting does not shorten history: the new chain is the same length, and
  the old chain's commits stay in the repository, unreachable.
- **RH27** — The reflog records the branch tip's movements, not commits. An entry holds
  the tip at that moment; it represents the whole chain reachable from that tip.
- **RH28** — The old tips the entries point to are nested suffixes; the recoverable set
  is determined by the entry that goes **furthest back**, not by the sum of the entries.
- **RH29** — The retention horizon is a setting, not a fixed number. The measurement
  tries four values: **7, 30, 60, 90** days.
- **RH30** — An entry outside the horizon is deleted, and only the commits reachable
  from that entry become collectible. The measurement does not count when garbage
  collection runs, it counts which set is **collectible**.
- **RH31** — Two separate things are counted in this lesson and are not mixed:
  **reachability** is counted in commits, **cost** is counted in touched objects. The
  step unit is not used.
- **RH32** — The resolution is **1/1000** in commits, **1/6** in log entries.

## Measurement

```python
"""Reflog: reachability and the recovery window.

Part 1 - what stays in the repository after a rewrite.
Part 2 - log entries: entry count and recoverable commits are separate things.
Part 3 - when the retention horizon closes the window.
"""
SEED = 20260814
FILES = ("metrics.py", "report.py", "identity.py", "config.py", "document.md")
POSITIONS = (1, 300, 501, 800, 951, 998)
HORIZONS = (7, 30, 60, 90)


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def history(n, seed=SEED):
    """n-commit linear history; each commit touches one file."""
    draw, commits = rng(seed), []
    for i in range(n):
        file = FILES[draw(5)]
        binary = draw(11) == 0
        commits.append({"no": i + 1, "file": file, "binary": binary,
                         "objects": 2 + (40 if binary else 0)})
    return commits


def rewrite(t, position):
    after = [x for x in t if x["no"] >= position]
    return len(after), sum(x["objects"] for x in after)


def log(seed=SEED):
    """Every move of the branch tip writes an entry; an entry holds the day and the tip."""
    draw, entries, day = rng(seed), [], 0
    for i, position in enumerate(POSITIONS):
        day += 1 + draw(30)
        entries.append({"no": i + 1, "day": day, "position": position})
    return entries


def recoverable(t, entries):
    """The union of the tips the entries point to: the one going furthest back decides."""
    if not entries:
        return 0, 0
    return rewrite(t, min(e["position"] for e in entries))


T = history(1000)
G = log()
TODAY = G[-1]["day"]

print(f"{'rewritten at':>20s} {'reachable':>13s} {'unreachable':>11s}"
      f" {'total in repo':>16s} {'unreachable objects':>17s}")
for position in (998, 501, 300, 1):
    changed, touched = rewrite(T, position)
    print(f"{position:20d} {len(T):13d} {changed:11d} {len(T) + changed:16d} {touched:17d}")

print()
print(f"{'entry':>6s} {'day':>5s} {'old tip position':>17s}"
      f" {'reachable from entry':>20s}")
for e in G:
    changed, _ = rewrite(T, e["position"])
    print(f"{e['no']:6d} {e['day']:5d} {e['position']:17d} {changed:20d}")
print(f"entry count {len(G)}, today day {TODAY}")

print()
print(f"{'retention horizon':>18s} {'entries left':>13s} {'recoverable commits':>21s}"
      f" {'recoverable objects':>20s} {'permanent loss':>14s}")
total_changed, _ = recoverable(T, G)
for horizon in HORIZONS:
    remaining = [e for e in G if TODAY - e["day"] <= horizon]
    changed, touched = recoverable(T, remaining)
    print(f"{horizon:18d} {len(remaining):13d} {changed:21d} {touched:20d}"
          f" {total_changed - changed:14d}")
```

```
        rewritten at     reachable unreachable    total in repo unreachable objects
                 998          1000           3             1003                 6
                 501          1000         500             1500              3000
                 300          1000         701             1701              4162
                   1          1000        1000             2000              5640

 entry   day  old tip position reachable from entry
     1    11                 1                 1000
     2    20               300                  701
     3    40               501                  500
     4    49               800                  201
     5    58               951                   50
     6    81               998                    3
entry count 6, today day 81

 retention horizon  entries left   recoverable commits  recoverable objects permanent loss
                 7             1                     3                    6            997
                30             2                    50                  260            950
                60             4                   500                 3000            500
                90             6                  1000                 5640              0
```

## The Repository Does Not Shrink

The top table's second column is the same across all four rows: **1000**. No matter
where the rewrite starts, the history reachable from the branch tip is a thousand
commits. The third column varies: **3**, **500**, **701**, and **1000** stay
unreachable. The fourth column is the sum of the two and carries the lesson's headline —
after a transformation from the first commit, **2000** commits stand in the repository,
half reachable, half not.

This explains why the previous lesson's bulk transformation did not bring an immediate
gain. Removing a binary asset from history does not lower the repository's size; it only
moves that asset's objects into the unreachable set. The unreachable-objects column is
**5640** for a transformation from the first commit, and that number is only freed once
garbage collection runs. There is a delay between the gain and the operation, and that
delay is at the same time **the recovery window.** The two are the same thing: the
duration objects are kept is the duration a mistake can be undone.

## Entry Count vs. Recoverable Commits

The middle table shows the log's six entries. One entry recovers three commits, another
recovers a thousand. A log of **6** entries can recover **1000** commits; the ratio is
one hundred sixty-seven commits per entry. This is why the log is a cheap mechanism: it
keeps tips, not chains.

There is a second piece of information in reading the entries. Every line also records
the movement's **type**: it reports separately whether a commit was written, a rebase
finished, or a branch tip was reset. This is the field to look at when searching for the
tip to recover — what is being sought is usually the tip one entry before the one
reporting the rewrite. The log holding identity and type together turns finding the
right entry from a guessing game into something else.

The same structure creates a trap too. Because the sets the entries cover are nested
suffixes, the recoverable set is determined not by the **number** of entries but by
which one **goes furthest back** among them. Even if five entries remain, if the deepest
one is gone, the recoverable set drops to that deepest one among the remaining five.
Seeing many lines in the log says nothing about the scope of recovery.

## As the Window Closes

The bottom table shows how the window closes. When the retention horizon is **90** days,
all six entries stand, all **1000** commits are recoverable, and the permanent loss is
**0**. When the horizon drops to **60** days, four entries remain and recoverable commits
fall to **500**: half of history can no longer be found from the log. At **30** days,
two entries and **50** commits remain. At **7** days, a single entry is left,
recoverable commits are **3**, permanent loss is **997**.

The numbers' uneven jump is not the horizon's result, it is the result of the entries'
distribution: the moment the deep entry from day eleven falls outside horizon sixty, five
hundred commits become unreachable at once. The window does not close smoothly, it
closes **in steps**. This is why the sentence "there is still a week left on the log" is
not a guarantee — without knowing which entry drops on which day, how much stays
recoverable cannot be known. The steps' locations are random too: the entries' days come
from the work schedule, while the depth they cover comes from what work was done that
day. There is no link between the two, which is why the deepest entry being the oldest
entry is common but not guaranteed.

The horizon itself is a setting and varies from repository to repository. What the
measurement says is not to defend a particular number but that **the horizon directly
determines the scope of recovery.** A longer horizon means more recovery, more objects
kept; a shorter one means the opposite. It is a two-way trade-off, and both directions
show in this table: the recoverable-objects column climbing from **6** to **5640** is
matched by an equal drop in the objects that could be freed.

## Loss Paths and Recovery Paths

Every loss path counted in this topic has a recovery path, and all four can be written
together.

When **the old form of a rewritten chain** is lost, recovery is in the log: a branch name
is given to the old tip. If the window is closed, the dangling-object list is the second
path.

If **an asset removed in a bulk transformation** was removed by mistake, recovery is
again in the log — but only in the repository that did the transformation. The
separate-clone habit pays off here: if the actual repository stays untouched, the
question of recovery never comes up.

For **an overwritten remote branch**, recovery is not in the local log but **in the
copies**. The overwritten side's backup is the side that pays the cost, and that backup
has no retention horizon; recovery stays open as long as a copy still points at the old
tip.

For **an entry that has fallen outside the horizon**, there is no recovery path. The
only remedy is prevention, and prevention is the backup branch opened before rewriting:
a backup branch is a reference, references are not subject to the horizon, and it keeps
reachability standing until deleted.

The four paths share a common feature, and it is the lesson's conclusion:
**recovery always happens on the side that holds the objects.** There is no central
recovery location. Your own log recovers only your own repository; your log does not
show a copy's lost work, and your log does not bring back a branch you overwrote. This
is the mirror image of the previous lesson's cost distribution — the cost was distributed
to the copies, and so is the ability to recover.

From here a one-line habit follows: **before rewriting, where the tip is gets
recorded.** A backup branch is the cheapest way to do this, and its cost is as much as
the previous course measured — a single reference. Not needing the window at all, rather
than asking how long it will stay open, is the shortest of all the paths counted here.

## Summary

- Rewriting does not shorten history. At all four positions, reachable commits stay at
  **1000**; unreachable commits become **3 / 500 / 701 / 1000**, and the total in the
  repository climbs as high as **2000**.
- Unreachable objects stay on disk until garbage collection runs; a transformation from
  the first commit leaves **5640** objects waiting to be freed. The delay in the gain and
  the recovery window are the same duration.
- The reflog records **branch tip movements**, not commits: **6** entries can recover
  **1000** commits. Entry count does not indicate the scope of recovery.
- The recoverable set is determined by the entry that goes furthest back; at retention
  horizons of **7 / 30 / 60 / 90** days, recoverable commits are **3 / 50 / 500 / 1000**
  and permanent loss is **997 / 950 / 500 / 0**.
- The log is local, per reference, and subject to the horizon; a backup branch is outside
  all three and is the only permanent safeguard.
- Recovery operations are read operations: giving a branch name to an old tip deletes
  nothing.

## Next Step

Up to this point, history was **changed**, and every change's cost was counted in
touched objects: 3002 in interactive mode, 5640 in bulk transformation, 39480 through the
copies in force push, between 6 and 5640 in recovery depending on the window's width. The
cost of changing is now known. What is not known is the opposite's cost: how much it
costs to **ask a question** of the same history. The next topic measures that — in a
thousand-commit history, how many steps does finding which commit introduced a defect
take, and how many does that step count climb to when history quadruples in size. The
unit of cost changes there too: not the object, but the **step**.
