Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
git post-receive hook for gitolite to publish updates to NATS
#!/usr/bin/env bash
set -euo pipefail
progname="$(basename "$0" .sh)"
stderr() { printf >&2 '%s: %s\n' "$progname" "$*"; }
die() { stderr "$@"; exit 1; }
# Tunable
: "${NATS_SERVER:=nats.example.org}"
# Contractually has to have been provided, but this helps with command-line
# testing, and maybe very old git servers?
: "${GIT_PUSH_OPTION_COUNT:=0}"
# See <https://gitolite.com/gitolite/dev-notes.html>:
: "${HOSTNAME:?}"
: "${GL_REPO_BASE:?}"
: "${GL_USER:?}"
: "${GL_REPO:?}"
jq --version >/dev/null
: "${BASE_NATS_TOPIC:=git-update.${HOSTNAME%%.*}}"
NATS_ARGS=(-s "$NATS_SERVER")
if [[ $GIT_PUSH_OPTION_COUNT -gt 0 ]]; then
for (( i=0; i < GIT_PUSH_OPTION_COUNT; i++ )); do
declare -n po="GIT_PUSH_OPTION_$i"
case "${po^^}" in
NONATS|NO-NATS|NO_NATS)
stderr 'skipping per request'
exit 0
;;
esac
done
fi
dotted_repo="${GL_REPO//\//.}"
base_msg="$(
jq -ncer --arg R "$GL_REPO" --arg U "$GL_USER" --arg H "$HOSTNAME" \
'.host=$H|.user=$U|.repo=$R'
)"
main_msg="$(
jq <<<"$base_msg" -cer '.updates=[]'
)"
stderr publishing NATS notifications to "${BASE_NATS_TOPIC}.(PERREF,UPDATE).${dotted_repo}"
start_time=$EPOCHREALTIME
main_old=''
main_new=''
main_ref=''
while read -r oldval newval refname; do
jargs=(--arg R "$refname" --arg O "$oldval" --arg N "$newval")
per_msg="$(jq <<<"$base_msg" -cer "${jargs[@]}" '.update = {ref: $R, old: $O, new: $N} | .ref = $R')"
main_msg="$(jq <<<"$main_msg" -cer "${jargs[@]}" '.updates += [{ref: $R, old: $O, new: $N}]')"
# nats CLI at v0.0.19-24-g781e96f will not read stdin if stdout is not a tty.
# So we pass JSON in an argv element. This will only be a problem if a _lot_ of refs are updated,
# for the .UPDATE. post below.
nats "${NATS_ARGS[@]}" pub "${BASE_NATS_TOPIC}.PERREF.${dotted_repo}" "$per_msg" 2>/dev/null
case "$refname" in
refs/heads/master)
if [[ "$main_ref" == "" ]]; then
main_old="$oldval" main_new="$newval" main_ref="$refname"
fi
;;
refs/heads/main)
main_old="$oldval" main_new="$newval" main_ref="$refname"
;;
esac
done
nats "${NATS_ARGS[@]}" pub "${BASE_NATS_TOPIC}.UPDATE.${dotted_repo}" "$main_msg" 2>/dev/null
also_text=''
if [[ "$main_ref" != "" ]]; then
commits="$(git log --date=short --reverse --pretty=tformat:'%h %ad %s' "${main_old}..${main_new}")"
jargs=(--arg R "$main_ref" --arg O "$main_old" --arg N "$main_new" --arg C "$commits")
log_payload="$(
jq <<<"$base_msg" -cer "${jargs[@]}" '.ref=$R|.old=$O|.new=$N|
.logs=[$C|split("\n")|foreach .[] as $l ([];true; $l|capture("(?<abbrev>\\S+)\\s+(?<date>\\S+)\\s+(?<msg>.*)"))]'
)"
nats "${NATS_ARGS[@]}" pub "${BASE_NATS_TOPIC}.MAINLOG.${dotted_repo}" "$log_payload" 2>/dev/null
also_text=' (also published to MAINLOG)'
fi
# 2020-11-10: we have the stupid second line of output mostly so I can see if
# delays are being caused by publishing to NATS or elsewhere later.
#
# zsh spoils me, bash lacks floating-point
stderr "done${also_text} [$(printf '%s - %s\n' "$EPOCHREALTIME" "$start_time" | bc)s]"
@philpennock

This comment has been minimized.

Copy link
Owner Author

@philpennock philpennock commented Nov 10, 2020

The second version adds the publishing to MAINLOG variant, with the JSON object also holding an array containing objects, one per commit, ordered first to last (latest). My first time using either foreach or capture in jq(1) invocation.

@philpennock

This comment has been minimized.

Copy link
Owner Author

@philpennock philpennock commented Nov 28, 2020

Beware: the latencies on this proved higher than I am happy with, so I rewrote into Go and rigged up a shell wrapper for gitolite to auto-compile the Go hooks as needed, so that normally the hook is a shell check that the binary is up-to-date and then invoking the binary.

Latencies in shell with jq and nats(1): around 0.8s normally, sometimes 5.7s.

Latencies in Go version: around 10 or 11 milliseconds, total.

@philpennock

This comment has been minimized.

Copy link
Owner Author

@philpennock philpennock commented Nov 28, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment