Safely Move Git Refs with git-update-ref
You will update a Git ref without editing files under .git directly. The examples cover creating or moving a ref, requiring an expected old value, deleting with a guard, and committing several ref changes as one transaction. Allow about fifteen minutes, and work in a disposable test repository before touching a valuable repository.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide describes Git 2.43.0 from Ubuntu package git 1:2.43.0-1ubuntu7.3 and its matching git-man package. Check your version first: later Git releases may add options that are not present in this installed manual.
1. Prepare a ref and record its current value
git-update-ref is a plumbing command. It changes the name stored in a ref, such as refs/heads/review, but it does not create a commit or change the index. Use it only when you have a specific ref-level operation in mind.
Run these read-only checks inside the repository. They need no elevated privileges:
$ git --version
git version 2.43.0
$ git rev-parse --git-dir
.git
$ OID=$(git rev-parse HEAD)
$ printf '%s\n' "$OID"
0123456789abcdef0123456789abcdef01234567
The displayed object ID will differ. The variable is used below so the examples follow the repository you are actually testing. If HEAD is unborn, use an existing object ID from git rev-parse <known-commit> instead.
Checkpoint: inspect the ref before changing it:
$ git show-ref --verify --quiet refs/heads/review
$ printf 'review: %s\n' "$(git rev-parse refs/heads/review 2>/dev/null || printf 'absent')"
review: absent
The first command returns success only if the ref exists. The second reports its object ID or the word absent. Do not confuse a missing ref with a ref whose value is all zeroes; the zero value is an input convention used to assert that a ref does not exist.
2. Create a ref only if it is absent
Pass the ref name, the new object ID, and an old object ID. Forty zeroes as the old value mean that creation is allowed only when the ref does not already exist:
$ git update-ref refs/heads/review "$OID" 0000000000000000000000000000000000000000
$ git rev-parse refs/heads/review
0123456789abcdef0123456789abcdef01234567
A successful command prints nothing. The following rev-parse check should print the same ID as $OID. If another process created the ref first, the compare-and-swap check fails rather than overwriting it.
For a normal branch, prefer Git's higher-level commands when they express the operation you want. This plumbing command is useful for scripts and tools that must update an exact ref with an explicit expected value.
3. Move a ref with a compare-and-swap guard
To move an existing ref safely, read its current ID, calculate or choose a new object ID, then supply the old ID as the final argument. The update succeeds only if the ref still has that value when Git locks it:
$ OLD=$(git rev-parse refs/heads/review)
$ NEW=$(git rev-parse HEAD~1)
$ git update-ref -m 'move review for test' refs/heads/review "$NEW" "$OLD"
$ test "$(git rev-parse refs/heads/review)" = "$NEW" && echo 'review moved'
review moved
The -m text is used in a reflog entry when logging is enabled. It is not a commit message and does not create a commit. If a concurrent update changes the ref after you read $OLD, Git reports that it cannot lock the ref and leaves the ref unchanged.
Warning: moving a branch ref changes what commands such as git log review and other users of that ref see. It does not rewrite the commits themselves, but it can make commits temporarily hard to find. Record the old ID before the change.
4. Delete a ref with an undo value
Deletion is also conditional. The -d form removes the ref only when its current value matches the old value you provide:
$ OLD=$(git rev-parse refs/heads/review)
$ git update-ref -d refs/heads/review "$OLD"
$ git show-ref --verify --quiet refs/heads/review; printf 'status: %s\n' "$?"
status: 1
This is a destructive ref change, so stop before running it if you have not saved the old object ID. Recovery is straightforward while that ID remains available:
$ git update-ref refs/heads/review "$OLD" 0000000000000000000000000000000000000000
$ git rev-parse refs/heads/review
0123456789abcdef0123456789abcdef01234567
That restore command recreates the ref only if it is still absent. If somebody else has recreated it, the zero old value protects their ref and the command fails.
5. Update several refs as one transaction
For related updates, feed commands to --stdin. The transaction below creates two refs, prepares their locks, and commits both:
$ printf 'start\nupdate refs/heads/review %s\nupdate refs/heads/test-copy %s\nprepare\ncommit\n' "$OID" "$OID" \
| git update-ref --stdin
start: ok
prepare: ok
commit: ok
$ git show-ref refs/heads/review refs/heads/test-copy
0123456789abcdef0123456789abcdef01234567 refs/heads/review
0123456789abcdef0123456789abcdef01234567 refs/heads/test-copy
In this input format, each instruction is a line. An update can include an old value, while create, delete and verify express stricter intentions. A repeated ref in one input is an error. If Git cannot lock one of the refs or an expected value does not match, the transaction is not committed.
Each individual ref update is atomic, but a concurrent reader may still observe a subset of a multi-ref change. Use start, prepare and commit when you need the transaction lifecycle and its lock failure point to be explicit. If a script exits before an explicit commit, the transaction is aborted.
6. Avoid the common traps
- Do not write
.git/HEADor files under.git/refswith shell redirection.update-reffollows Git's symbolic-ref rules, locks the ref, checks object names and reports failures. - Do not omit the old value in a script that must not overwrite a concurrent change. Read it, pass it, and handle a non-zero exit status.
- By default, a symbolic ref such as
HEADis dereferenced and its target is updated. Use--no-derefonly when you deliberately want to overwrite the named ref itself. - Reflogs are normally controlled by
core.logAllRefUpdatesand the type of ref. Use--create-reflogwhen this operation specifically needs a reflog, and make sure Git has committer identity information. - Do not use
sudofor repository ref updates. Elevated ownership can leave the repository harder to maintain and does not make a mismatched ref safe to overwrite.
Done means
- You identified the exact ref and recorded its old object ID.
- Creation, movement or deletion used an expected old value where concurrency matters.
- Every changed ref was checked with
git rev-parseorgit show-ref. - You kept the old ID so a mistaken ref change can be restored without guessing.
- Related changes used a reviewed
--stdintransaction rather than unrelated edits to.gitfiles.