Git, CI/CD & GitOpsXVII · ReflogMechanics
What the reflog records — every ref update, every time, with a reason
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
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
- Read the reflog as a log of ref updates, not commands. The reason field is a hint; the OIDs are the truth.
- 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.
- For audit clones, set
core.logAllRefUpdates trueandgc.reflogExpire never. The reflog becomes the most complete per-ref movement log available. - 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
--hardreset 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 trueandgc.reflogExpire neverso every commit it has ever synced remains reachable by reflog entry.
Quiz
Knowledge check · 4 questions
Q1. Which statement precisely describes what the reflog records?
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).
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.
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.