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.
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
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
- You need to test whether one exact full ref exists without listing every ref. Which command form?
- You need a stable custom listing of ref names and object IDs. Which command?
- You observed a ref at A and want to move it to B only if nobody has changed it. Which command shape?
- You need to know whether HEAD is attached to a branch. Which command?
- You need the old local values of a branch tip after a mistaken update. Which local evidence source?
16. Cleanup
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.