My dotfiles update themselves when I log in. The repo is my home directory—GIT_DIR points at ~/..., GIT_WORK_TREE at $HOME—and a startup script fetches, fast-forwards, and, when the config changed, replaces the running shell with a fresh login shell of the new world. The handoff between those two steps is an exit code:
# Subshell to avoid side effects. Return code determines next step.
(if [[ -x $(type -p git) && -f ${_ELLIPSIS}config && -z ${UID/#$EUID} ]]; then
...
if git merge --quiet --ff-only $them; then
us=$(git rev-parse HEAD)
if [[ $us == $them ]]; then
at=INFO say 'Upstream merged (reloading).'
exit 99
fi
fi
...
fi)
case $? in
(99)
# Code updated. Re-exec!
exec -l $ZSH_ARGZERO "${@}"
;;
esacThe entire updater runs in one parenthesized subshell. It cannot set the parent’s variables, but its exit status carries the next step upward. Inside, only one path exits 99—a fast-forward that moved HEAD to match upstream. Everything else, including “already current” and “merge refused,” exits normally. The parent reads 99 as a one-byte message: your configuration is stale; replace yourself.
exec -l $ZSH_ARGZERO "${@}" is the replacement. exec swaps the process image, and -l prepends a dash to argv[0]—the old Unix convention for a login shell—so the fresh zsh runs the full login startup against the just-merged files. The bash twin of this file does the same with $BASH. There is no “restart your shell to apply changes”; logging in is applying changes.
Two guards keep it from misbehaving. The merge is only attempted when the two heads differ and git merge-base --is-ancestor $them $us says upstream isn’t already contained in HEAD—which also breaks any re-exec loop, because after the re-exec the heads are equal and the whole block short-circuits. And it is --ff-only: a diverged worktree gets a warning naming the exact command to run (cd ~; git merge ...) rather than an automatic merge into a live home directory.
The fetch throttle is its own small trick: “when did I last fetch” is read from the mtime of FETCH_HEAD—a file git already maintains—with the next fetch twelve hours later. A failed merge backdates FETCH_HEAD to the epoch so the next shell retries immediately.