hooks: add remote-update hook
Sync hook. Supports either `submit' and `commit-received'.
Reject a change if doesn't merge against an external mirror,
provided one, ignore it otherwise.
diff --git a/remote-update b/remote-update
new file mode 100755
index 0000000..11abe19
--- /dev/null
+++ b/remote-update
@@ -0,0 +1,204 @@
+#!/bin/bash
+#
+# remote-update hook
+#
+# Copyright 2020 Luigi Santivetti <luigi.santivetti@gmail.com>
+
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+# remote-update supports calling from two server side hooks with the following
+# list of command line arguments (we only care about --project).
+#
+# 1. commit-received:
+#
+# --project <project name>
+# --refname <refname>
+# --uploader <uploader>
+# --uploader-username <username>
+# --oldrev <sha1>
+# --newrev <sha1>
+# --cmdref <refname>
+#
+# 2. submit:
+#
+# --project <project name>
+# --branch <branch>
+# --submitter <submitter>
+# --patchset <patchset id>
+# --commit <sha1>
+#
+# RATIONALE: remote-update can reject one change either pushed for review or
+# submitted for a merge into Gerrit. It performs a remote update, importing
+# into Gerrit all refs/heads/* merged outside Gerrit from an external mirror.
+# This allows Gerrit to sit in the middle and act like a reviewing tool, but
+# having to follow the line of develpment potentially done also elsewhere.
+#
+# ` remote-update hook ` TODO
+# ` `
+# ` `
+# (1) push/submit ` (2) fetch ` (3) fetch
+# +--------+ ··········> +--------+ ··········> +--------+ ··········>
+# | Change | | Gerrit | | Mirror |
+# +--------+ <·········· +--------+ <·········· +--------+ <··········
+# reject/accept (6) ` update (5) ` update (4)
+# ` `
+# ` `
+# ` `
+#
+# The assumption is that `Mirror' does not have other remotes set, so there
+# no need to `git remote update' it. The only changes to mirror either come
+# from Gerrit itself (with change-merged hook) or via direct push for who's
+# access granted to it.
+
+echo "Gerrit Code Review: remote-update hook"
+
+# File to dump onto the file system storing this hook stdout and stderr
+LOG_FILE=${HOOKS_LOG_DIR:-/tmp}
+if [ ! -w "$LOG_FILE" ]; then
+ echo " ****** warning: skip log" >&2
+ LOG_FILE="/dev/null" # avoid permission denied
+else
+ LOG_FILE="$LOG_FILE/hooks.remote-update-$(date +'%d%m%Y%H%M%S').txt"
+fi
+
+# Host service where replication mirrors and deploy engine live
+HOOK_HOST=${HOOKS_REMOTE_HOST:-}
+if [ ! -n "$HOOK_HOST" ]; then
+ echo " ****** warning: no host, skip hook" >&2
+ exit 0
+fi
+
+# User allowed t othe target host to push and eventually deploy changes
+HOOK_USER=${HOOKS_REMOTE_USER:-}
+if [ ! -n "$HOOK_USER" ]; then
+ echo " ****** warning: no user, skip hook" >&2
+ exit 0
+fi
+
+# Directory where this hook expects the project mirror to be located
+HOOK_PATH=${HOOKS_REMOTE_PATH:-}
+if [ ! -n "$HOOK_PATH" ]; then
+ echo " ****** warning: no path, skip hook" >&2
+ exit 0
+fi
+
+# Alias for the remote tracking a given mirror
+GERRIT_R=${HOOKS_REMOTE_ALIAS:-}
+if [ ! -n "$GERRIT_R" ]; then
+ echo " ****** warning: no remote alias, skip hook" >&2
+ exit 0
+fi
+
+SSH_RSA_ID=${HOOKS_REMOTE_RSAID:-}
+if [ ! -n "$SSH_RSA_ID" ]; then
+ echo " ****** error: no rsa id file found" >&2
+ exit 1
+fi
+
+SSH_PORT=${HOOKS_REMOTE_PORT:-22}
+if [ ! -n "$SSH_PORT" ]; then
+ echo " ****** warning: no ssh port, default to 22" >&2
+fi
+
+/bin/bash <(/bin/cat <<EOF
+set -x
+args='${*}'
+
+declare -A CHANGE=(
+ [project]=""
+ [refname]=""
+ [uploader]=""
+ [uploader-username]=""
+ [oldrev]=""
+ [newrev]=""
+ [cmdref]=""
+ [branch]=""
+ [submitter]=""
+ [patchset]=""
+ [commit]=""
+)
+
+while read -a line; do
+ for K in \${!CHANGE[@]}; do
+ if [ "\${line[0]}" == "--\$K" ]; then
+ CHANGE[\$K]="\${line[@]:1}"
+ break
+ fi
+ done
+done <<< "\$(echo -e "\${args// --/\\\\n--}")"
+
+remote_url="ssh://$HOOK_USER@$HOOK_HOST:$HOOK_PATH/\${CHANGE[project]}.git"
+
+# Does Gerrit have \\\$remote_url remote set up?
+git remote get-url "$GERRIT_R" | grep -q -- "\$remote_url"
+if [ "\$?" -ne 0 ]; then
+ echo >&2 " ****** info: \${CHANGE[project]} clean url for $GERRIT_R"
+ git remote rm $GERRIT_R
+
+ echo >&2 " ****** info: \${CHANGE[project]} adding remote: $GERRIT_R"
+ git remote add $GERRIT_R \$remote_url
+ if [ "\$?" -ne 0 ]; then
+ echo >&2 " ****** error ¯\\_(ツ)_/¯ : add remote $GERRIT_R failed"
+ exit 1
+ fi
+fi
+
+git remote show | grep -q -- "$GERRIT_R"
+if [ "\$?" -ne 0 ]; then
+ echo >&2 " ****** info: \${CHANGE[project]} clean remote: $GERRIT_R"
+ git remote rm $GERRIT_R
+
+ echo >&2 " ****** info: \${CHANGE[project]} adding remote: $GERRIT_R"
+ git remote add $GERRIT_R \$remote_url
+ if [ "\$?" -ne 0 ]; then
+ echo >&2 " ****** error ¯\\_(ツ)_/¯ : add remote $GERRIT_R failed"
+ exit 1
+ fi
+fi
+
+GIT_SSH_COMMAND="ssh"
+GIT_SSH_COMMAND+=" -o StrictHostKeyChecking=no"
+GIT_SSH_COMMAND+=" -o UserKnownHostsFile=/dev/null"
+GIT_SSH_COMMAND+=" -p $SSH_PORT"
+GIT_SSH_COMMAND+=" -i $SSH_RSA_ID"
+
+# RATIONALE: \`+refs' in order to allow non-fast-forward fetch. This has two
+# consequences. Gerrit's refs/heads/* for \$CHANGE[project] is always hard
+# reset to match the refs/heads/* on the remote, which is distruptive from
+# a Gerrit point of view. Secondly, \$CHANGE[newref] may not merge against
+# non-fast-forwardable heads. In such a case, \$CHANGE[newref] needs to be
+# rebased on top of refs/heads/*.
+GIT_SSH_COMMAND="\$GIT_SSH_COMMAND" git fetch -v "$GERRIT_R" +refs/heads/*:refs/heads/*
+if [ "\$?" -ne 0 ]; then
+ echo >&2 " ****** error ¯\\_(ツ)_/¯ : failed to update remote: $GERRIT_R"
+ exit 1
+fi
+
+echo " ****** success: remote update done"
+set +x
+EOF
+ ) &> ${LOG_FILE}; RET=$?
+
+if [ -r "${LOG_FILE}" ]; then
+ grep -E "^ \*\*\*\*\*\* " ${LOG_FILE} 2>/dev/null
+fi
+
+[ "$RET" -ne 0 ] || rm -f ${LOG_FILE}
+
+exit "$RET"