Hi,
The report below was investigated and drafted by Claude in the process of building a branch-protection tool; I've reviewed it and verified the reproduction before sending. Happy to answer questions or test patches.
---
githooks(5) says the reference-transaction hook "is invoked by any Git command that performs reference updates". Some branch copy and rename operations update a ref without invoking it.
Observed on git version 2.55.0.windows.5, with the hook registered both in .git/hooks and as a config-defined hook (hook.<name>.event).
1. `git branch -C <src> <dst>` where <dst> exists and is not checked out anywhere: <dst> is overwritten with <src>'s value and the hook is not invoked in any phase. This happens on both the files and the reftable backend. With every documented hook event registered to a logging script, none of them fired, and a GIT_TRACE2_EVENT log showed no child process.
2. `git branch -c <src> <dst>` where <dst> does not exist: <dst> is created and the hook sees no line for it.
3. `git branch -m/-M <src> <dst>`:
- files backend: the hook sees the deletion of <src> and a
"0000... 0000... refs/heads/<dst>" line, but no line carrying
<dst>'s new value;
- reftable backend: no line names <dst> at all, and renaming a branch
away deletes it without a line for it.4. `git reflog delete --updateref --rewrite <branch>@{0}` rewinds the
branch without invoking the hook, on both backends.Reproduction (any POSIX shell):
git init -q -b main t && cd t git commit -q --allow-empty -m one git branch other git commit -q --allow-empty -m two git switch -q other printf '#!/bin/sh\necho "hook $1" >>"$PWD/hook.log"\ncat
>/dev/null\n' >../h.sh
git config hook.log.event reference-transaction git config hook.log.command "sh $(cd ..; pwd)/h.sh" : >hook.log git branch -C other main # main now points at "one" cat hook.log # empty
Expected: the hook runs with a line updating refs/heads/main, as it does for `git branch -f main other`. Tools that enforce policy through this hook cannot otherwise see these updates.
Note that git already refuses -C/-M onto a branch checked out in any worktree, so the gap is limited to branches that are not checked out.