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
|