Checkpoint Lab — Refs, Reflogs, Packed Refs, Symbolic References, and Reference Transactions
Checkpoint safe reference automation by creating tool-owned refs, moving them with expected-old guards, coordinating a multi-ref transaction, inspecting reflogs, packing storage, and rejecting stale writers.
Learning objectives
- Predict and verify guarded ref transitions using captured old object IDs.
- Create explicit reflogs for custom automation refs.
- Commit two related guarded ref movements through one update-ref transaction.
- Prove pack-refs changes storage rather than logical ref values.
- Simulate stale single-ref and multi-ref writers and respond by re-reading/recomputing.
1. Checkpoint scenario — release automation needs safe temporary names
You will create three known commits and two tool-owned refs. You will predict/verify guarded movement, inspect reflogs, update both refs transactionally, pack refs without changing their meaning, then simulate a stale writer and prove that Git prevents it from overwriting concurrent state.
2. Predictions before running the lab
-
If
refs/automation/candidateis at A and you run a guarded A → B update, what must be true before it succeeds? - If two refs are queued in one transaction and one expected old value is wrong, should either requested new value be committed?
-
After
pack-refs --all, shouldgit rev-parse refs/automation/candidatechange? - If process 1 observed B, process 2 later moves the ref to C, should process 1's B-based guarded update succeed?
3. Create an isolated repository and three deterministic commit positions
Git Bash, Bash, or zsh
mkdir git-ref-checkpoint
cd git-ref-checkpoint
git init -b trunk repo
cd repo
git config user.name "Ref Checkpoint"
git config user.email "ref-checkpoint@example.invalid"
printf "state=A\n" > state.txt
git add state.txt
git commit -m "A: baseline"
A=$(git rev-parse HEAD)
printf "state=B\n" > state.txt
git commit -am "B: validated candidate"
B=$(git rev-parse HEAD)
printf "state=C\n" > state.txt
git commit -am "C: next candidate"
C=$(git rev-parse HEAD)
git log --oneline --decorate --graph
PowerShell
New-Item -ItemType Directory git-ref-checkpoint | Out-Null
Set-Location git-ref-checkpoint
git init -b trunk repo
Set-Location repo
git config user.name "Ref Checkpoint"
git config user.email "ref-checkpoint@example.invalid"
Set-Content state.txt 'state=A'
git add state.txt
git commit -m "A: baseline"
$A = git rev-parse HEAD
Set-Content state.txt 'state=B'
git commit -am "B: validated candidate"
$B = git rev-parse HEAD
Set-Content state.txt 'state=C'
git commit -am "C: next candidate"
$C = git rev-parse HEAD
4. Preflight repository identity, ref backend, and existing names
git status --short --branch
git rev-parse --absolute-git-dir
git rev-parse --show-ref-format
git show-ref --head
git reflog list
git show-ref --verify --quiet refs/automation/candidate
echo "candidate exists exit=$?"
git show-ref --verify --quiet refs/automation/verified
echo "verified exists exit=$?"
5. Create two tool-owned refs with explicit reflogs
git update-ref \
--create-reflog \
-m "checkpoint: create candidate at A" \
refs/automation/candidate "$A" ""
git update-ref \
--create-reflog \
-m "checkpoint: create verified at A" \
refs/automation/verified "$A" ""
git for-each-ref \
--format='%(refname) %(objectname)' \
refs/automation
git reflog show refs/automation/candidate
git reflog show refs/automation/verified
Both names should resolve to A and both should have a reflog because the creation requested one explicitly.
6. Prediction 1 — candidate A → B succeeds only if candidate is still A
OBSERVED=$(git rev-parse refs/automation/candidate)
test "$OBSERVED" = "$A"
git update-ref \
-m "checkpoint: candidate A to B" \
refs/automation/candidate "$B" "$OBSERVED"
git rev-parse refs/automation/candidate
git reflog show -2 refs/automation/candidate
Verify that the new value is B and the reflog includes the reason.
7. Prepare a two-ref state transition
The intended logical transition is: candidate B → C and verified A → B. Capture both actual old values first:
OLD_CANDIDATE=$(git rev-parse refs/automation/candidate)
OLD_VERIFIED=$(git rev-parse refs/automation/verified)
test "$OLD_CANDIDATE" = "$B"
test "$OLD_VERIFIED" = "$A"
8. Prediction 2 — commit both guarded ref movements in one transaction
Git Bash, Bash, or zsh
{
echo start
echo "update refs/automation/candidate $C $OLD_CANDIDATE"
echo "update refs/automation/verified $B $OLD_VERIFIED"
echo prepare
echo commit
} | git update-ref --stdin
git for-each-ref \
--format='%(refname) %(objectname)' \
refs/automation
PowerShell
@(
'start'
"update refs/automation/candidate $C $OLD_CANDIDATE"
"update refs/automation/verified $B $OLD_VERIFIED"
'prepare'
'commit'
) | git update-ref --stdin
git for-each-ref --format='%(refname) %(objectname)' refs/automation
Expected: candidate is C and verified is B. If either old-value check had failed, the transaction would not have committed the requested pair.
9. Verify reflog evidence after the transaction
git reflog show -4 refs/automation/candidate
git reflog show -4 refs/automation/verified
git reflog exists refs/automation/candidate
git reflog exists refs/automation/verified
The exact message generated for transaction-based updates can vary by command/version context; the important evidence is the old/new movement and continued reflog presence.
10. Prediction 3 — packing changes storage, not logical ref values
BEFORE_CANDIDATE=$(git rev-parse refs/automation/candidate)
BEFORE_VERIFIED=$(git rev-parse refs/automation/verified)
git rev-parse --show-ref-format
git pack-refs --all
AFTER_CANDIDATE=$(git rev-parse refs/automation/candidate)
AFTER_VERIFIED=$(git rev-parse refs/automation/verified)
test "$BEFORE_CANDIDATE" = "$AFTER_CANDIDATE"
test "$BEFORE_VERIFIED" = "$AFTER_VERIFIED"
git show-ref
git for-each-ref --format='%(refname) %(objectname)' refs/automation
The tests prove logical identity survives the storage optimization.
Do not judge success by counting files under .git/refs.
11. Prediction 4 — simulate process 1 becoming stale
Process 1 records candidate at C. Process 2 moves candidate C → A. Process 1 then tries to move the candidate to B while still expecting C.
PROCESS1_SEEN=$(git rev-parse refs/automation/candidate)
test "$PROCESS1_SEEN" = "$C"
git update-ref \
-m "process 2: C to A" \
refs/automation/candidate "$A" "$C"
git update-ref \
-m "process 1: stale C to B" \
refs/automation/candidate "$B" "$PROCESS1_SEEN"
echo "stale writer exit=$?"
git rev-parse refs/automation/candidate
git reflog show -4 refs/automation/candidate
Expected: process 1 fails and candidate remains A. That refusal prevents a lost update.
12. Correct stale-writer handling — re-read, re-evaluate, then guard again
Suppose the application policy determines that moving A → B is still valid after reviewing process 2's change:
NOW=$(git rev-parse refs/automation/candidate)
test "$NOW" = "$A"
git update-ref \
-m "checkpoint: recomputed transition A to B" \
refs/automation/candidate "$B" "$NOW"
git rev-parse refs/automation/candidate
The key is the re-evaluation. A blind retry would defeat the purpose of expected-old protection.
13. Bonus failure — one stale expected value prevents a two-ref transaction
Capture the correct values, then deliberately supply a wrong old
value for verified:
CAND_BEFORE=$(git rev-parse refs/automation/candidate)
VER_BEFORE=$(git rev-parse refs/automation/verified)
{
echo start
echo "update refs/automation/candidate $C $CAND_BEFORE"
echo "update refs/automation/verified $C $A"
echo prepare
echo commit
} | git update-ref --stdin
echo "transaction exit=$?"
git rev-parse refs/automation/candidate
git rev-parse refs/automation/verified
Because verified is B rather than the deliberately expected A, the transaction should fail; candidate must remain at its pre-transaction value as well.
14. Verify object reachability through ordinary porcelain
git log --graph --decorate --oneline --all
git show --stat refs/automation/candidate
git show --stat refs/automation/verified
git branch --contains "$A"
git branch --contains "$B"
Tool-owned refs are ordinary Git refs even though branch porcelain does not list them as local branches. They participate in object reachability and revision resolution.
15. Write the operating policy this lab demonstrates
| Concern | Checkpoint policy |
|---|---|
| Namespace |
Tool-owned state lives under refs/automation/*
|
| Single-ref concurrency | Always use observed expected-old OID |
| Multi-ref logical update | Use update-ref transaction with expected values |
| Recovery evidence | Create reflogs explicitly for tool-owned refs and use meaningful reasons where supported |
| Storage | Never parse loose/packed storage as application state |
| Stale writer | Fail, re-read, recompute, then attempt a new guarded update |
| Remote publication | Use server capability/policy such as atomic push where required; verify support |
16. Verification checklist
- Exactly three known commits A, B, C were created in a disposable repository.
-
candidateandverifiedwere created underrefs/automation/with explicit reflog requests. - Candidate A → B succeeded only after checking the observed A value.
- A two-ref transaction moved candidate B → C and verified A → B together.
- Reflogs remained inspectable after the transaction.
-
pack-refs --alldid not change either logical OID. - A stale writer expecting C was rejected after another process moved candidate to A.
- The correct response was re-read/recompute/re-guard, not blind overwrite.
- A deliberately stale multi-ref transaction left both requested changes uncommitted.
- No ref-storage file was manually edited in the checkpoint.
17. Cleanup
Git Bash / Bash / zsh
cd ../..
pwd
rm -rf git-ref-checkpoint
PowerShell
Set-Location ../..
Get-Location
Remove-Item -Recurse -Force git-ref-checkpoint
18. Knowledge check
Question 1. Why did the first guarded A → B candidate update succeed?
Question 2. What does packing refs prove if the OIDs before and after are identical?
Question 3. Process 1's stale update fails after process 2 moved the ref. Is the failure evidence of corruption?
Question 4. One old-value check in a multi-ref transaction is wrong. What should happen?
Question 5. Why use explicit reflogs for custom automation refs?
19. What Chapter 19 adds to a production Git operating model
You can now treat refs as a concurrent naming database rather than loose hash files. That means querying logical refs through Git, separating symbolic names from direct object IDs, retaining local movement evidence deliberately, using expected-old values to prevent lost updates, coordinating multi-ref changes transactionally, and keeping storage/backend details out of application logic.
20. Chapter checkpoint summary
Safe ref automation is optimistic concurrency control: observe the current value, compute the intended transition, update only if the observed value still holds, and fail closed when it does not. Reflogs help explain prior local values, while ref transactions coordinate several names. Neither mechanism replaces server authorization or permanent audit systems.
Authoritative references
git-update-ref
git-reflog
git-pack-refs
git-for-each-ref
git-push
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.