Skip to content

The FETCH_HEAD Throttle

 2 min date -r FETCH_HEAD as the entire last-fetch database: no state file, a 12-hour window, and an epoch backdate to force retries.
Published

Apr 9, 2018

My dotfiles update themselves at login. To avoid hitting the network on every shell, this updater uses a database git already maintains by accident. Its ordinary git fetch --verbose --prune rewrites .git/FETCH_HEAD, so the file’s mtime is the time of this updater’s last fetch:

now=$(date +%s)
last=$(($(date -r ${_ELLIPSIS}FETCH_HEAD +%s)))
next=$((last + (60*60*12)))

# Fetch latest from upstream.
if ((now > next)); then
  say 'Syncing upstream.'
  git fetch --verbose --prune 2>&1
fi

No state file, no cron, no lock. date -r reads a file’s modification time on both BSD and GNU date, so the same three lines run unmodified on macOS and Linux. The extra arithmetic wrapper around the command substitution is doing quiet work too: if FETCH_HEAD doesn’t exist yet, date fails, the substitution yields nothing, and $(( )) of an empty string is 0—the last fetch reads as 1970 and the first shell on a fresh clone fetches immediately. The missing-file case is the bootstrap case, handled by arithmetic coercion rather than an if.

The same degenerate value is also the retry rule. When the fast-forward merge after a fetch fails—local commits, diverged upstream—the updater backdates the file to the epoch:

# If merge failed, let's retry in an hour.
TZ=UTC touch -m -t 197001010000.00 ${_ELLIPSIS}FETCH_HEAD

Setting the mtime to 1970-01-01 guarantees now > next next time, so the throttle springs open. TZ=UTC matters because touch -t parses the timestamp in local time; pinning the zone makes it the actual epoch instead of the epoch plus or minus your offset.

The gotcha is right there in the comment: “retry in an hour” is not what the code does. Backdating to the epoch retries on the next shell, whenever that is—thirty seconds later if you open a new tab, tomorrow if you log out. An hour appears nowhere. The comment describes a delay the code never implements. The code wins: a failed merge is retried at the next shell, not on any clock.

This lives in boot/zsh (with a bash twin) in my dotfiles. The FETCH_HEAD throttle was added to the existing login-time updater, and six months later the older scheme of pushing into machines directly was deleted as “No longer used.” Pull-on-login with a 12-hour mtime window turned out to need nothing more than a file git was already touching.