Refs, Reflogs, Packed Refs, Symbolic References, and Reference Transactions: Diagnostics, Failure Modes, Security, and Performance
Diagnose stale-writer races, missing reflogs, stale remote-tracking refs, ref deletion/reachability misconceptions, packed-ref scanning failures, and controlled packed-ref corruption without destroying evidence.
Learning objectives
- Diagnose stale expected-old failures as concurrency protection rather than corruption.
- Verify reflog availability and remote-tracking freshness explicitly.
- Separate ref deletion from object deletion/reachability.
- Replace .git/refs scanning with backend-independent logical ref queries.
- Preserve/restore evidence in a tightly controlled manual-metadata corruption lab.
1. Ref diagnostic sequence
- Preserve evidence: current ref OIDs, reflog presence/entries, HEAD attachment, ref backend, Git version, config origin, remote-tracking state.
-
Inspect logical APIs:
show-ref,for-each-ref,symbolic-ref,reflog,rev-parse. - Identify the layer: ref value, symbolic ref, reflog availability, storage backend, remote-tracking freshness, or concurrency guard.
- Choose the least destructive correction.
- Verify the final logical refs and reachability.
2. Capture a ref evidence bundle before changing anything
git --version
git status --short --branch
git rev-parse --show-ref-format
git show-ref --head
git for-each-ref --format='%(refname) %(objectname)'
git symbolic-ref -q HEAD || echo "HEAD is detached"
git reflog list
git config --list --show-origin --show-scope
3. Failure mode — two automation processes try to move the same ref
Process 1 and process 2 both read A. Process 2 succeeds first and moves the ref A → B. Process 1 later tries A → C.
SEEN_BY_PROCESS_1="$A"
git update-ref \
-m "process 2: A to B" \
refs/automation/deploy "$B" "$A"
git update-ref \
-m "process 1: stale A to C" \
refs/automation/deploy "$C" "$SEEN_BY_PROCESS_1"
echo "process 1 exit=$?"
git rev-parse refs/automation/deploy
The second update must fail because the actual value is B, not A. Do not “fix” this by retrying blindly without re-reading and re-evaluating the desired state.
4. Why a blind update would be a lost-update bug
# Dangerous concurrency pattern:
git update-ref refs/automation/deploy "$C"
The two-argument form deliberately updates without an expected-old check. It has valid uses, but concurrent automation should normally retain the value it observed and use the guarded form. Otherwise it can overwrite a newer decision made by another process.
5. Failure mode — assuming every ref has a reflog
git update-ref refs/automation/no-log "$A"
git reflog exists refs/automation/no-log
echo "reflog exists exit=$?"
git reflog list
Depending on core.logAllRefUpdates policy, this custom
ref can exist without a reflog. If the tool requires local movement
evidence, create/update it with an explicit reflog policy:
git update-ref \
--create-reflog \
-m "enable tool-owned reflog" \
refs/automation/no-log "$B" "$A"
git reflog exists refs/automation/no-log
git reflog show refs/automation/no-log
6. Failure mode — confusing a local remote-tracking ref with the server's current ref
Create a second clone that advances the server, while the first clone does not fetch yet:
git clone ../central.git ../other
git -C ../other config user.name "Other Writer"
git -C ../other config user.email "other@example.invalid"
printf "server=advanced\n" > ../other/server.txt
git -C ../other add server.txt
git -C ../other commit -m "advance server"
git -C ../other push origin HEAD:trunk
git rev-parse refs/remotes/origin/trunk
git ls-remote origin refs/heads/trunk
If the object IDs differ, your local remote-tracking ref is stale. Fetch deliberately, then verify:
git fetch origin
git rev-parse refs/remotes/origin/trunk
git ls-remote origin refs/heads/trunk
7. Failure mode — treating ref deletion as immediate object deletion
git update-ref refs/automation/extra-name "$B"
git branch contains-b "$C"
git update-ref -d refs/automation/extra-name "$B"
git cat-file -e "$B^{commit}"
git branch --contains "$B"
Deleting one name does not delete B while another reachable branch/history still reaches it. Ref deletion changes naming/reachability edges; object retention depends on the entire ref/history/reflog graph and later maintenance policy.
8. Failure mode — a script scans .git/refs and misses
packed refs
On the files backend, create a tag, pack refs, then compare a filesystem scan with Git's logical query:
git tag packed-demo "$B"
git pack-refs --all
find .git/refs -type f -print
git show-ref --tags
git for-each-ref --format='%(refname) %(objectname)' refs/tags
A tag can remain logically present even if no corresponding loose file exists. The repair is architectural: replace directory scanning with a documented ref API.
9. Intentionally broken example — manual editing corrupts packed-ref storage
git rev-parse --show-ref-format
git pack-refs --all
git show-ref > ../refs-before.txt
cp .git/packed-refs ../packed-refs.backup
printf 'this is not a valid packed-ref record\n' >> .git/packed-refs
git show-ref
echo "show-ref exit=$?"
Git should reject or report invalid packed-ref content rather than silently trusting arbitrary text. The broken line is the original cause.
Repair from the deliberate backup
cp ../packed-refs.backup .git/packed-refs
git show-ref > ../refs-after.txt
diff -u ../refs-before.txt ../refs-after.txt
The clean repair is restoring known-good metadata in this controlled lab. In production, avoid manual editing and use Git's ref commands/backups/authoritative repository data according to the incident.
10. Packed-ref troubleshooting applies only to the files backend
If git rev-parse --show-ref-format reports another
backend such as current experimental reftable, a
packed-refs-specific runbook is the wrong diagnostic
path. Use backend-independent commands and current
git refs verify support where appropriate.
11. Failure mode — one stale value aborts a multi-ref transaction
{
echo start
echo "update refs/automation/one $C $B"
echo "update refs/automation/two $C $A"
echo prepare
echo commit
} | git update-ref --stdin
If refs/automation/two is no longer A, preparation
fails and the queued transaction is not committed. Verify both refs
after the error; do not assume the earlier textual order means the
first update happened.
12. Failure mode — assuming an old reflog entry will always be available
git reflog show is powerful recovery evidence, but
reflog expiration and object pruning are finite/local maintenance
concerns. If an incident depends on a reflog entry, preserve the
evidence promptly. Do not run reflog expiration or aggressive
pruning during diagnosis.
13. Security relevance — validate ref names and never shell-evaluate repository-controlled names
Automation that constructs refs from user/project input should
validate the full name with git check-ref-format and
pass arguments without shell eval. Ref names are data.
A valid Git ref name is not automatically a safe shell fragment,
path fragment outside Git, deployment environment name, or
authorization decision.
15. Performance relevance — large ref sets need logical APIs and backend-aware optimization
Repositories with very large numbers of tags/refs can incur filesystem/storage costs. Packing refs or using newer ref-storage approaches can improve performance, but application scripts should remain backend-neutral. Optimize storage independently from the business logic that queries/moves refs.
16. Red-zone actions during a ref incident
17. Symptom → layer → least-destructive correction
| Symptom | Likely layer | Correction |
|---|---|---|
| Expected-old update rejected | Concurrent ref movement | Re-read actual ref and recompute decision |
| Ref exists but reflog command fails | Reflog creation/retention | Inspect policy; do not invent missing history |
| Filesystem scan misses tag | Packed/backend storage | Use show-ref/for-each-ref |
| origin/trunk disagrees with server | Remote-tracking freshness | ls-remote/fetch then verify |
| show-ref rejects packed metadata | Files-backend storage corruption | Preserve evidence; restore known-good metadata/authoritative refs |
18. Knowledge check
Question 1. A guarded update fails because the ref is at B instead of expected A. What should automation do next?
Question 2. Why can a custom ref exist without a reflog?
Question 3. Why did a .git/refs directory scan miss a packed tag?
Question 4. Why can origin/trunk be stale even when the remote changed?
Question 5. Why is restoring a backed-up packed-refs file acceptable only in this lab?
19. Summary
Ref failures are usually concurrency, reflog-policy, remote-freshness, or storage-abstraction problems. Diagnose with logical APIs first. The most dangerous response is to bypass Git's ref mechanisms and edit metadata before understanding which ref value is authoritative.
Authoritative references
git-update-ref
git-reflog
git-show-ref
git-pack-refs
git-ls-remote
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.