git update-ref: Usage, Options & Common CI Errors
git update-ref is the low-level way to point a ref at a commit, with optional safety checks.
Automation that moves branches or custom refs should use update-ref rather than writing files under .git/refs by hand - it handles packed-refs, reflogs, and compare-and-swap correctly.
What it does
git update-ref sets a reference to a new object id, optionally only if its current value matches an expected old value (compare-and-swap), and can delete a ref or process a batch of updates from standard input.
Common usage
git update-ref refs/heads/main <new-sha>
git update-ref refs/heads/main <new-sha> <old-sha> # CAS guard
git update-ref -d refs/heads/stale
git update-ref --stdin < updates.txtOptions
| Flag | What it does |
|---|---|
| <ref> <newvalue> [<oldvalue>] | Set ref, optionally guarded by old value |
| -d <ref> [<oldvalue>] | Delete a ref |
| --stdin | Read a batch of commands |
| --create-reflog | Force a reflog entry |
| -z | NUL-terminate stdin records |
Common errors in CI
fatal: Cannot lock ref ‘refs/heads/main’: is at <X> but expected <Y> - the compare-and-swap old value did not match; another job moved the ref. Re-read the current value and retry. "unable to create ‘.git/refs/...’.lock: File exists" means a stale lock from a killed process; remove it only when sure no other git is running.
Using this in CI
CI checkouts are shallow and detached by default, which changes the answer this command gives you. Commands that read history, branch names, or tags need the checkout configured for it.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # history, tags, and git describe all need this
- run: |
git rev-parse --is-shallow-repository # expect false
git rev-parse --abbrev-ref HEAD # prints HEAD when detached