Download
140 lines 5.7 KiBBashModified 2026-10-28executable raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
#!/bin/sh
# post-receive hook for Repo Renderer. contrib/new-repo links each
# repository's hooks/post-receive to this one file.
#
# A push queues a job for each branch and tag pushed, and starts
# repo-render kick, which renders them. repo-render is the one on PATH, or
# /usr/local/bin/repo-render, and it reads /etc/repo-render/repo-render.yaml
# unless $REPO_RENDER_CONFIG names another config. Then, if the
# repository's git config has any, it pushes to its mirrors:
#
#   repo-render.mirror   a git URL to push to, one per mirror: the default
#                        branch and every tag. Never --mirror, which would
#                        delete what only the mirror has. The branch is
#                        forced, so a force push here reaches the mirror,
#                        but only over a commit this repository has had:
#                        commits made on the mirror are never thrown away
#                        (unless "repo-render mirror REPO --force"). Tags are
#                        never forced.
#                        ssh runs as core.sshCommand says, if set, and
#                        otherwise never prompts, gives up on a
#                        connection that hangs, and checks host keys
#                        against ssh's known_hosts files.
#
# It must return in milliseconds, so the render and the mirror pushes run
# in the background. Nothing from the client is trusted except ref names
# and SHAs, which enqueue and the renderer both validate. Mirror failures
# go to syslog (logger -t repo-render-mirror), and the next push retries.

# The repository's name comes from its directory on disk, which the
# client can't influence.
repo=$(basename "$(cd "${GIT_DIR:-.}" && pwd -P)" .git)
case "$repo" in
	""|.*|*[!A-Za-z0-9._-]*) exit 0 ;;
esac

# Run in the background below, as "post-receive mirror". Run by hand as
# "post-receive mirror --force", it pushes the branch over anything.
if [ "${1-}" = mirror ]; then
	force=""
	[ "${2-}" = --force ] && force=1
	branch=$(git symbolic-ref --short HEAD) || exit 1
	force_cmd="repo-render mirror $repo --force"
	# One mirror push at a time: two at once, from pushes close together
	# or git-selfhost's check, fail on each other's locks on the mirror.
	# A lock still there after two minutes was left by one that died.
	gitdir=$(git rev-parse --absolute-git-dir)
	lock=$gitdir/repo-render-mirror.lock
	waited=0
	until mkdir "$lock" 2>/dev/null; do
		if [ $waited -ge 120 ]; then
			rmdir "$lock" 2>/dev/null
			waited=0
		fi
		sleep 1
		waited=$((waited + 1))
	done
	trap 'rmdir "$lock"' EXIT
	trap 'exit 1' HUP INT TERM
	if [ -z "$(git config core.sshCommand)" ]; then
		GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=4"
		export GIT_SSH_COMMAND
	fi
	# Exits 1 if any mirror failed, for git-selfhost, which runs this to
	# check the mirrors. The failures are kept, for the next push to show,
	# and for repo-render list and check (internal/mirror's FailedFile).
	status=$gitdir/repo-render-mirror.failed
	git config --get-all repo-render.mirror | {
		failed=0
		: >"$status.new"
		fail() {
			echo "$repo: $1"
			echo "$1" >>"$status.new"
			failed=1
		}
		while read -r url; do
			# Where the mirror's branch is. A commit this repository has, or
			# had before a force push here, is ours to replace; one it never
			# had was made on the mirror.
			if ! theirs=$(git ls-remote -- "$url" "refs/heads/$branch" </dev/null 2>&1); then
				echo "$theirs"
				fail "mirror to $url failed: it can't be reached, or the key isn't added there (the next push retries)"
				continue
			fi
			theirs=${theirs%%[!0-9a-f]*}
			if [ -n "$force" ]; then
				spec=+refs/heads/$branch:refs/heads/$branch lease=""
			elif [ -z "$theirs" ] || git cat-file -e "$theirs^{commit}" 2>/dev/null; then
				# The lease is on what was just seen, so a commit made on the
				# mirror since is not thrown away either.
				spec=refs/heads/$branch:refs/heads/$branch lease=--force-with-lease=refs/heads/$branch:$theirs
			else
				fail "mirror to $url refused: its $branch is at $theirs, which this repository has never had, so it has commits made there. Pull them and push them here, or throw them away, as root: $force_cmd"
				continue
			fi
			if git push --no-verify --quiet ${lease:+"$lease"} -- "$url" "$spec" "refs/tags/*:refs/tags/*" </dev/null 2>&1; then
				echo "$repo: mirrored to $url"
			else
				fail "mirror to $url failed: is the key added there with write access, or is a tag different there? (the next push retries)"
			fi
		done
		if [ $failed -eq 1 ]; then
			mv -f "$status.new" "$status"
		else
			rm -f "$status.new" "$status"
		fi
		exit $failed
	}
	exit
fi

program=$(command -v repo-render) || program=/usr/local/bin/repo-render

pushed=""
while read -r old new ref; do
	case "$ref" in
		refs/heads/*|refs/tags/*)
			pushed=1
			# enqueue locks against the renderer claiming the queue, so
			# no job is lost to a push that races a running kick.
			"$program" enqueue "$repo" "$ref" "$old" "$new"
			;;
	esac
done
[ -n "$pushed" ] || exit 0

nohup "$program" kick </dev/null >/dev/null 2>&1 &
# The pusher sees what went wrong with the mirrors last time, which they
# can't see otherwise.
status=$(git rev-parse --absolute-git-dir)/repo-render-mirror.failed
if [ -s "$status" ]; then
	echo "repo-render: the last mirror push failed, and is tried again now:"
	sed 's/^/    /' "$status"
fi
if git config --get-all repo-render.mirror >/dev/null; then
	# nohup on the shell, so that both ends of the pipe ignore hangups.
	# shellcheck disable=SC2016 # $0 expands in that shell
	nohup sh -c 'if command -v logger >/dev/null 2>&1; then "$0" mirror 2>&1 | logger -t repo-render-mirror; else "$0" mirror; fi' "$0" \
		</dev/null >/dev/null 2>&1 &
fi
exit 0