Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXVII · ReflogMechanics

What the reflog records — every ref update, every time, with a reason

Advanced⏱ ~20 mingit

What you'll learn

  • State precisely what the reflog records (ref updates) and what it does not (operations, commits, events)
  • Read a single reflog entry and identify its five positional fields
  • Distinguish the HEAD reflog from per-branch and per-remote reflogs
  • Recognise the reflog is a local-only artefact that is not part of the wire protocol

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The reflog is a per-clone log of ref updates. Each line records that “ref X moved from OID A to OID B at time T because of reason R”. The word update matters: the reflog does not record operations, commits, or events. It records the moment a named pointer in the repository moved from one OID to another. If an operation does not move any ref, the reflog has no entry for it. If an operation moves several refs at once, the reflog has one entry per moved ref.

Ref updates, not operations

A common mistake is to treat the reflog as a command history. It is not. git reset --soft HEAD~3 is an operation; the reflog does not contain the literal string. It contains one entry per ref the operation moved (HEAD and the current branch). git add <file> does not move any ref and produces no entry.

git reset --soft HEAD~3
git reflog -3
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# 8a3f9d2 HEAD@{1}: commit: bump terraform module

git checkout -- terraform/main.tf
git reflog -3
# (unchanged: no ref moved)
flowchart LR
    A["operation"] --> B["moves ref X?"]
    B -- yes --> C["reflog entry for X"]
    B -- no --> D["no entry written"]

The rule is mechanical: a reflog line exists iff a ref’s value changed. The reason string tells you why, but the existence of a line tells you only that something updated a ref.

Anatomy of a single entry

Each line of .git/logs/<refname> has five positional fields:

<old-oid> <new-oid> <refname> <unix-ts> <tz>\t<reason>

The all-zeros OID (40 hex zeroes) is a sentinel. In the old-oid position it marks a creation; in the new-oid position it marks a deletion. The reason is a tab-separated free-form string Git populates with the action and, when applicable, the commit subject.

tail -3 "$GIT_DIR/logs/HEAD"
# 4d2c8e0... 9a8b7c6... refs/heads/main 1730000000 +0000	commit: bump terraform module to v1.4.0
# 0000...   8a3f9d2... refs/heads/main 1729000000 +0000	commit (initial): initial commit
# 8a3f9d2... 0000...   refs/heads/feature/x 1728500000 +0000	branch: Deleted

The OIDs are content-addressed and therefore checkable against the object store; the reason is a hint, not authoritative. A corrupted reflog entry that names an OID absent from the object store is detectable; a corrupted reason string is not.

Which refs are logged by default

By default, the following refs are logged when they move: HEAD (always), branch refs under refs/heads/, and remote-tracking refs under refs/remotes/. Tags under refs/tags/ are not logged unless core.logAllRefUpdates is set to true. The full opt-in:

git config core.logAllRefUpdates true
# every ref movement now writes to .git/logs/refs/...

The default scope (HEAD + branches + remote-tracking) is what makes the reflog useful for the typical “recover from a mistaken operation” case: the ref the engineer cares about is almost always one of the three.

Why the reflog is local-only

The reflog is per-clone. It is not pushed by git push, not fetched by git fetch, and not served by any Git wire-protocol command. The reason is that the reflog records local intent (“this engineer made this checkout, this commit, this reset”), not shared state. A reflog entry that says “checkout: moving from main to feature/x” is meaningful only on the clone where it happened.

flowchart LR
    A["clone A\nreflog: local history"] -->|"git push\n(sends OID)"| R["remote"]
    R -->|"git fetch\n(updates remote-tracking ref)"| B["clone B\nreflog: local history"]

The wire protocol transmits OIDs, not reflogs. Two engineers who each fetched the same commit at the same time have two different reflog entries because they took two different paths to get there.

Production discipline

  1. Read the reflog as a log of ref updates, not commands. The reason field is a hint; the OIDs are the truth.
  2. For high-value operations, copy the new OID into a ticket or commit message immediately. The reflog retains the OID for 90 days by default; the comment retains it for the life of the record.
  3. For audit clones, set core.logAllRefUpdates true and gc.reflogExpire never. The reflog becomes the most complete per-ref movement log available.
  4. Never rely on the reflog as a shared record. The reflog is local. If the answer must be shared, copy it.

Cross-course references

  • Git, CI/CD & GitOps — Part V (Branches, Refs and HEAD) — Part V lesson 6 introduced the reflog at the foundations level. This lesson is the advanced follow-up on semantics.
  • Git, CI/CD & GitOps — Part XV (Reset) — the recovery path from a mistaken --hard reset relies on the reflog having recorded the pre-reset OID of HEAD.
  • GitOps with Argo CD — Part IV (SyncPhases) — the controller’s clone configures core.logAllRefUpdates true and gc.reflogExpire never so every commit it has ever synced remains reachable by reflog entry.

Quiz

Knowledge check · 4 questions

  1. Q1. Which statement precisely describes what the reflog records?

  2. Q2. The all-zeros OID (40 hex zeroes) appears in a reflog entry only when a ref is being created (as the old OID) or deleted (as the new OID).

  3. Q3. Name the five fields of a reflog entry in on-disk order, and identify which field position distinguishes a creation from a normal update.

  4. Q4. Diagnose what the reflog reveals about a series of operations on a feature branch and recommend how to preserve the diagnostic value.

    An engineer ran four operations on a feature branch over the course of a morning: (1) `git switch -c feature/iam`; (2) `git commit -m 'initial policy'`; (3) `git reset --soft HEAD~1` and a re-commit; (4) `git switch main`. The engineer later asks: 'what was the OID of the initial-policy commit before I reset it?'

Passing score: 75%. Answers are checked in this browser.