Add git sync, and stop trusting the cached default branch

BuildEmpire/Totara renamed its default branch from main to totara-20.
Nothing local noticed, because refs/remotes/origin/HEAD is written once at
clone time and remote.<name>.followRemoteHEAD defaults to `create`, which
only fills the ref in when it is missing. So `git default-branch` still
said main, as did nvim's diff-against-branch prompt.

bin/git-sync does the start-of-work sequence for a fork - fast-forward the
default branch from upstream, push it to origin - and works out which
branch that is by asking the server, not the cache. It sets the cached
HEADs from the answer, so everything reading that ref agrees afterwards.
It lives in bin/ rather than as an alias because git picks up git-<name>
on the $PATH as a subcommand, and this is more shell than a gitconfig
alias should hold. followRemoteHEAD = always is set for origin as well, so
an ordinary fetch keeps the ref current without running the script.

The nvim prompt now prefers origin, then upstream, then the remaining
remotes. It took the first remote alphabetically before, which in
be-edition means kdog - a colleague's fork - rather than anything
authoritative.

Verified against a fixture of bare repos whose default branch is
totara-20 and whose cached HEAD is stale: the branch is created tracking
origin when absent, fast-forwarded when behind, pushed to the fork,
refuses to merge when the local copy has diverged, and is a no-op on a
second run. The nvim prompt was checked in three real repos.
This commit is contained in:
Jonny Barnes 2026-08-27 16:05:49 +01:00
commit 80683b82c1
No known key found for this signature in database
4 changed files with 137 additions and 11 deletions

67
bin/git-sync Executable file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env zsh
# Bring the default branch up to date in a fork-model repo, and keep my fork's
# copy of it aligned: fast-forward it from whichever remote owns it, then push
# it to origin. Named git-sync and installed on the $PATH, so git finds it as
# `git sync`.
set -e
git rev-parse --git-dir > /dev/null
# The branch belongs to upstream in a fork, and to origin when there is no
# fork in play.
if git remote get-url upstream > /dev/null 2>&1; then
source_remote=upstream
else
source_remote=origin
fi
if ! git remote get-url origin > /dev/null 2>&1; then
echo "git sync: no 'origin' remote to sync" >&2
exit 1
fi
# Ask the server which branch is the default rather than reading
# refs/remotes/<remote>/HEAD. That ref is a cache written at clone time, and
# git does not refresh it when the default branch changes upstream - which is
# the whole reason this script exists.
branch=$(git ls-remote --symref $source_remote HEAD |
awk '/^ref:/ { sub("refs/heads/", "", $2); print $2; exit }')
if [[ -z $branch ]]; then
echo "git sync: could not work out the default branch of $source_remote" >&2
exit 1
fi
echo "Syncing $branch from $source_remote"
git fetch --prune $source_remote
# Re-point the cached HEADs at what the server just told us, so `git
# default-branch` and nvim's diff-against-branch prompt agree with this script.
git remote set-head origin --auto > /dev/null
if [[ $source_remote != origin ]]; then
git remote set-head $source_remote --auto > /dev/null
fi
# Be explicit about what the local branch tracks. A bare `git switch` has to
# guess, and refuses to when several remotes carry the same branch name - which
# they do as soon as a colleague's fork is added as a remote.
if git show-ref --verify --quiet refs/heads/$branch; then
git switch $branch
elif git show-ref --verify --quiet refs/remotes/origin/$branch; then
# Track origin, not upstream: this script keeps the fork's copy current, and
# it means a stray `git push` here goes somewhere I can actually write to.
git switch --create $branch --track origin/$branch
else
git switch --create $branch --track $source_remote/$branch
fi
# Never a merge commit: if the default branch has diverged locally, that is
# something to look at, not something to paper over.
git merge --ff-only $source_remote/$branch
if [[ $source_remote != origin ]]; then
git push origin $branch
fi