Chapter 19Lesson 04~145 minutes

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.

DiagnosticsConcurrencyPacked refsRecovery 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

  1. Preserve evidence: current ref OIDs, reflog presence/entries, HEAD attachment, ref backend, Git version, config origin, remote-tracking state.
  2. Inspect logical APIs: show-ref, for-each-ref, symbolic-ref, reflog, rev-parse.
  3. Identify the layer: ref value, symbolic ref, reflog availability, storage backend, remote-tracking freshness, or concurrency guard.
  4. Choose the least destructive correction.
  5. 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

Disposable files-backend repository only. The next demonstration intentionally edits Git metadata. First confirm the backend, save a byte-for-byte backup, and record logical refs. Never do this to repair a valuable repository.
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.

14. Ref integrity is not authorization

A successful local update-ref proves the local ref update met Git's expected-value/storage rules. It does not prove the actor was authorized by an organizational policy. Authorization belongs to repository filesystem controls, server receive policy, hosting permissions, CI gates, or other governance layers.

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

Do not manually rewrite packed-refs, force valuable refs to guessed OIDs, expire reflogs, prune objects, or delete unknown refs while evidence is incomplete. First capture logical refs, reflogs, config, object reachability, and remote state.

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.

Next

Checkpoint guarded refs, reflogs, packing, and stale-writer protection

Lesson 5 integrates the chapter into a reproducible local operating procedure and finishes with a small multi-ref transaction.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.