Chapter 19Lesson 02~165 minutes

Refs, Reflogs, Packed Refs, Symbolic References, and Reference Transactions: Guided Hands-On Workflow and Core Operations

Create and inspect local branch, tag, remote-tracking, symbolic, and temporary refs; perform guarded create/update/delete operations; inspect reflogs; detach HEAD; and pack refs without changing logical behavior.

Hands-on refsGuarded updateDetached HEADpack-refs

Learning objectives

  • Inventory logical refs with show-ref and for-each-ref.
  • Prove attached versus detached HEAD behavior with symbolic-ref and rev-parse.
  • Create, move, reject, and delete a temporary ref using expected-old checks.
  • Inspect reflog presence instead of assuming every ref has one.
  • Pack refs in a disposable files-backend repository and verify porcelain/ref APIs remain correct.

1. Create a disposable repository with several known commits

Git Bash, Bash, or zsh

mkdir git-refs-lab
cd git-refs-lab
git init -b trunk repo
cd repo
git config user.name "Refs Lab"
git config user.email "refs-lab@example.invalid"

printf "version=1\n" > app.conf
git add app.conf
git commit -m "A: initial application"
A=$(git rev-parse HEAD)

printf "version=2\n" > app.conf
git commit -am "B: update application"
B=$(git rev-parse HEAD)

printf "version=3\n" > app.conf
git commit -am "C: update application again"
C=$(git rev-parse HEAD)

git log --oneline --decorate --graph

PowerShell setup alternative

New-Item -ItemType Directory git-refs-lab | Out-Null
Set-Location git-refs-lab
git init -b trunk repo
Set-Location repo
git config user.name "Refs Lab"
git config user.email "refs-lab@example.invalid"

Set-Content app.conf 'version=1'
git add app.conf
git commit -m "A: initial application"
$A = git rev-parse HEAD

Set-Content app.conf 'version=2'
git commit -am "B: update application"
$B = git rev-parse HEAD

Set-Content app.conf 'version=3'
git commit -am "C: update application again"
$C = git rev-parse HEAD

2. Inventory logical refs before adding more names

git show-ref --head
git for-each-ref \
  --sort=refname \
  --format='%(refname) %(objectname) %(objecttype)'

At this point you should have the local branch refs/heads/trunk. HEAD can be included explicitly with show-ref --head.

3. Add a tag and inspect the namespace rather than the filesystem

git tag -a demo-v1 "$B" -m "Demo release at B"

git show-ref --tags --dereference
git for-each-ref \
  --format='%(refname) %(objectname) %(objecttype)' \
  refs/tags

The annotated tag ref points to a tag object, and --dereference can also show the object that the tag ultimately names.

4. Create a no-account local remote and obtain a remote-tracking ref

cd ..
git init --bare central.git
git -C repo remote add origin ../central.git
git -C repo push -u origin trunk
git -C repo fetch origin

git -C repo show-ref --verify refs/remotes/origin/trunk
git -C repo for-each-ref \
  --format='%(refname) %(objectname)' \
  refs/remotes

The bare repository has its own server-side refs/heads/trunk. Your non-bare repository has a separate local refs/remotes/origin/trunk that records fetched/known remote state.

5. Inspect symbolic HEAD while attached

cd repo
git symbolic-ref HEAD
git symbolic-ref --short HEAD
git rev-parse HEAD
git rev-parse refs/heads/trunk

The two object-ID queries should agree because HEAD resolves through the symbolic branch ref.

6. Detach HEAD safely and prove it is no longer a symbolic ref

git switch --detach "$B"
git status --short --branch

git symbolic-ref -q HEAD
echo "symbolic-ref exit=$?"

git rev-parse HEAD
git rev-parse refs/heads/trunk

Expected: the quiet symbolic-ref query exits non-zero. HEAD resolves directly to B, while trunk still points to C. No branch was moved.

git switch trunk

7. Create a temporary ref using an explicit expected non-existence check

First verify that the name is absent:

git show-ref --verify --quiet refs/heads/release-candidate
echo "exists exit=$?"

Then create the ref. In this disposable lab, an empty expected-old value tells update-ref to require that the ref does not already exist:

git update-ref \
  --create-reflog \
  -m "lab: create release candidate at A" \
  refs/heads/release-candidate "$A" ""

git show-ref --verify refs/heads/release-candidate
git reflog show --date=iso refs/heads/release-candidate

8. Move the temporary ref with an expected-old guard

CURRENT=$(git rev-parse refs/heads/release-candidate)
test "$CURRENT" = "$A"

git update-ref \
  -m "lab: advance candidate A to B" \
  refs/heads/release-candidate "$B" "$A"

git rev-parse refs/heads/release-candidate
git reflog show refs/heads/release-candidate

Git moves the ref only because its actual old value matches A.

9. Intentionally submit a stale update and interpret the failure

git update-ref \
  refs/heads/release-candidate "$C" "$A"
echo "stale update exit=$?"

git rev-parse refs/heads/release-candidate

Expected: the update fails with an error explaining that the ref is at B rather than the expected A. The final rev-parse still prints B. This failure is the concurrency protection, not a problem to bypass.

10. Delete the temporary ref only if it still names the object you expect

git update-ref -d refs/heads/release-candidate "$B"

git show-ref --verify refs/heads/release-candidate
echo "verify after delete exit=$?"

The ref is removed. The commits A/B/C remain reachable through trunk, so deleting this extra name does not delete those objects.

11. Reflog availability is ref-specific

git reflog list
git reflog exists refs/heads/trunk
echo "trunk reflog exit=$?"
git reflog exists refs/tags/demo-v1
echo "tag reflog exit=$?"

In a normal non-bare repository, branch/HEAD reflogs are commonly created automatically. A tag reflog may not exist unless explicitly requested or configured. Never infer “there must be a reflog” from the existence of a ref.

12. Preflight before packing refs

Storage-only operation: pack-refs changes ref storage representation, not the logical object IDs each ref names. The lab is disposable and uses the default files ref backend.
git rev-parse --show-ref-format
git show-ref
git for-each-ref --format='%(refname) %(objectname)'

13. Pack refs, then prove logical behavior is unchanged

git pack-refs --all

git show-ref
git for-each-ref --format='%(refname) %(objectname)'
git rev-parse trunk
git log -1 --oneline trunk
git tag --list

With the files backend, the repository may now contain packed-refs and fewer loose ref files. Do not use that storage change as your verification. The logical Git queries above are the verification.

14. Advance a packed branch and observe that storage can change again

printf "version=4\n" > app.conf
git commit -am "D: advance trunk after packing"
D=$(git rev-parse HEAD)

git show-ref --verify refs/heads/trunk
git reflog show -3 refs/heads/trunk

In the traditional files backend, updating a packed branch normally creates a new loose ref that overrides the older packed value. This is another reason direct storage scanning is fragile.

15. Challenge — choose the ref command from the mental model

  1. You need to test whether one exact full ref exists without listing every ref. Which command form?
  2. You need a stable custom listing of ref names and object IDs. Which command?
  3. You observed a ref at A and want to move it to B only if nobody has changed it. Which command shape?
  4. You need to know whether HEAD is attached to a branch. Which command?
  5. You need the old local values of a branch tip after a mistaken update. Which local evidence source?

16. Cleanup

Confirm you are inside the disposable parent directory before recursive deletion.

Git Bash / Bash / zsh

cd ../..
pwd
rm -rf git-refs-lab

PowerShell

Set-Location ../..
Get-Location
Remove-Item -Recurse -Force git-refs-lab

17. Knowledge check

Question 1. Why did symbolic-ref -q HEAD fail in detached HEAD state?

Question 2. Why did the stale expected-old update fail?

Question 3. Did deleting release-candidate delete commit B?

Question 4. Why might a tag ref exist without a reflog?

Question 5. What should scripts query after pack-refs?

18. Summary

You created, guarded, advanced, rejected, and deleted a ref; distinguished attached from detached HEAD; inspected reflog presence; created local remote-tracking state; and packed refs without changing logical behavior. The key operational rule is to verify a ref's expected old value whenever concurrent writers are possible.

Next

Design ref policy across repositories, automation, hooks, and servers

Lesson 3 covers core.logAllRefUpdates, custom namespaces, files versus reftable storage, multi-ref transactions, reference-transaction hooks, and remote push --atomic capability.

Authoritative references

 git-show-ref
 git-for-each-ref
 git-symbolic-ref
 git-update-ref
 git-pack-refs

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.