hooks: add change-merged hook

Async hook, replicate changed merged in Gerrit onto an external
mirror, provided one. Ignore it otherwise.
diff --git a/change-merged.inactive b/change-merged.inactive
new file mode 100755
index 0000000..32669dd
--- /dev/null
+++ b/change-merged.inactive
@@ -0,0 +1,209 @@
+#!/bin/bash
+#
+# change-merged 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.
+
+# change-merged
+#
+#  --change <change id>
+#  --change-url <change url>
+#  --change-owner <change owner>
+#  --change-owner-username <username>
+#  --project <project name>
+#  --branch <branch>
+#  --topic <topic>
+#  --submitter <submitter>
+#  --submitter-username <username>
+#  --commit <sha1>
+#  --newrev <sha1>
+#
+# RATIONALE: change-merged can propagate one or more changes landing into
+# Gerrit to an external mirror. Be noted, this is an async hook that runs
+# in the background.
+#
+#                                    ` remote-update hook   ` TODO
+#                                    `                      `
+#                                    `                      `
+#               (1) merge            ` (2) push             ` (3) push
+#    +--------+             +--------+             +--------+
+#    | Gerrit | ··········> | Gerrit | ··········> | Mirror | ··········>
+#    +--------+             +--------+             +--------+
+#                                    `                      `
+#                                    `                      `
+#                                    `                      `
+#                                    `                      `
+
+LOG_FILE=${HOOKS_LOG_DIR:-/tmp}
+if [ ! -w "$LOG_FILE" ]; then
+	LOG_FILE="/dev/null" # avoid permission denied
+else
+	LOG_FILE="$LOG_FILE/hooks.change-merged-$(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" &> ${LOG_FILE}
+	exit 1
+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" &> ${LOG_FILE}
+	exit 1
+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" &> ${LOG_FILE}
+	exit 1
+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" &> ${LOG_FILE}
+	exit 1
+fi
+
+SSH_RSA_ID=${HOOKS_REMOTE_RSAID:-}
+if [ ! -n "$SSH_RSA_ID" ]; then
+	echo " ****** error: no rsa id file found" &> ${LOG_FILE}
+	exit 1
+fi
+
+SSH_PORT=${HOOKS_REMOTE_PORT:-22}
+if [ ! -n "$SSH_PORT" ]; then
+	echo " ****** warning: no ssh port, default to 22" &> ${LOG_FILE}
+fi
+
+GERRIT_PORT=${HOOKS_GERRIT_PORT:-29418}
+if [ ! -n "$GERRIT_PORT" ]; then
+	echo " ****** warning: no Gerrit port, default to 29418" &> ${LOG_FILE}
+fi
+
+GERRIT_HOST=${HOOKS_GERRIT_HOST:-}
+if [ ! -n "$GERRIT_HOST" ]; then
+	echo " ****** warning: no Gerrit host, skip notify" &> ${LOG_FILE}
+fi
+
+DEPLOY_EXEC=${HOOKS_DEPLOY_EXEC:-}
+if [ ! -n "${DEPLOY_EXEC}" ]; then
+	echo " ****** warning: no deploy exec, skip deploy" &> ${LOG_FILE}
+fi
+
+/bin/bash <(/bin/cat <<EOF
+set -x
+args='${*}'
+
+declare -A CHANGE=(
+		[change]=""
+		[change-url]=""
+		[change-owner]=""
+		[change-owner-username]=""
+		[project]=""
+		[branch]=""
+		[topic]=""
+		[submitter]=""
+		[submitter-username]=""
+		[commit]=""
+		[newrev]=""
+)
+
+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--}")"
+
+SSH_COMMAND="ssh"
+SSH_COMMAND+=" -o StrictHostKeyChecking=no"
+SSH_COMMAND+=" -o UserKnownHostsFile=/dev/null"
+SSH_COMMAND+=" -i $SSH_RSA_ID"
+
+function gerrit_cli
+{
+	\$SSH_COMMAND -p $GERRIT_PORT \${CHANGE[submitter-username]}@$GERRIT_HOST gerrit \${@}
+}
+
+remote_url="ssh://$HOOK_USER@$HOOK_HOST:$HOOK_PATH/\${CHANGE[project]}.git"
+
+# Does Gerrit have \\\$git_dir remote set up?
+git remote get-url "$GERRIT_R" | grep -q -- "\$remote_url"
+if [ "\$?" -ne 0 ]; then
+	echo >&2 " ****** error: \${CHANGE[project]} remote not found: $GERRIT_R"
+	echo >&2 " ****** error: \${CHANGE[branch]} not pushed, skip hook"
+	exit 1
+fi
+
+# Get patchset info
+NUMBERS=\$(gerrit_cli query \${CHANGE[commit]} --patch-sets | grep -- number)
+
+# Get patch number and patchset number
+patchnum=\$(printf "%s\n" "\$NUMBERS" | head -n1 | cut -f 2 -d ':')
+patchset=\$(printf "%s\n" "\$NUMBERS" | tail -n1 | cut -f 2 -d ':')
+
+# Trim whitespaces
+patchnum=\${patchnum//\ /}
+patchset=\${patchset//\ /}
+
+GIT_SSH_COMMAND="\$SSH_COMMAND"
+GIT_SSH_COMMAND+=" -p $SSH_PORT"
+GIT_SSH_COMMAND="\$GIT_SSH_COMMAND" git push --follow-tags $GERRIT_R refs/heads/*:refs/heads/*
+if [ "\$?" -ne 0 ]; then
+	echo >&2 " ****** error: failed to push \${CHANGE[branch]}"
+	gerrit_cli review -m '"Gerrit failed to replicate this change ¯\\\\_(ツ)_/¯"' \$patchnum,\$patchset
+	exit 1
+fi
+
+echo " ****** success: \${CHANGE[branch]} pushed to $GERRIT_R"
+gerrit_cli review -m '"Gerrit has successfully replicated this change \\\\o/"' \$patchnum,\$patchset
+
+if [ -n "$DEPLOY_EXEC" ]; then
+
+	# If going to deploy, breathe
+	sleep 1
+
+	# Invoke deploy script
+	\$SSH_COMMAND -p $SSH_PORT $HOOK_USER@$HOOK_HOST "${DEPLOY_EXEC} \${args[@]}"
+	if [ \$? -ne 0 ]; then
+		gerrit_cli review -m '"Gerrit failed to deploy this change ¯\\\\_(ツ)_/¯"' \$patchnum,\$patchset
+		exit 1
+	else
+		gerrit_cli review -m '"Gerrit has successfully deployed this change \\\\o/"' \$patchnum,\$patchset
+		exit 0
+	fi
+else
+	gerrit_cli review -m '"Gerrit has skipped deploying this change ¯\\\\_(ツ)_/¯"' \$patchnum,\$patchset
+	exit 0
+fi
+set +x
+EOF
+		) &> ${LOG_FILE}; RET=$?
+
+Keep the log if the hook has failed only
+[ "$RET" -ne 0 ] || rm -f ${LOG_FILE}