3.5 KiB · text History b868907
#!/bin/sh
# gitgud-prereceive — invoked by pre-receive in each bare repo.
#
# Reads `<old> <new> <ref>` lines from stdin and POSTs them to the
# Phoenix node for branch-protection evaluation. If Phoenix says DENY,
# we print the reason on stderr (visible to the pushing client) and
# exit non-zero so git aborts the push.
#
# **Fail-closed**: if the protection backend can't give an answer, the
# push is REJECTED. Better to refuse than to silently bypass branch
# protection.
#
# `GITGUD_PREHOOK_FAIL_OPEN=1` is an emergency override, and it covers
# exactly one case: the backend is *unreachable* — curl missing, the
# connection refused or timed out, or the node answering 5xx. It never
# covers an answer the backend gave on purpose: a 4xx (a missing or
# wrong hook secret is a 403, an unknown repository a 404) is a
# misconfiguration or someone probing, and letting the push through on
# it would turn a broken install into one without branch protection.
# Nor does it cover a hook with no GITGUD_HOOK_SECRET at all, which the
# backend could only ever refuse.

set -eu

GITGUD_PREHOOK_URL="${GITGUD_PREHOOK_URL:-http://127.0.0.1:4000/_hooks/pre-receive}"
REPO_PATH="${GIT_DIR:-$(pwd)}"
REPO_PATH="$(cd "$REPO_PATH" && pwd)"

STDIN_PAYLOAD="$(cat)"

# The backend is unreachable: refuse, unless the operator opted into
# fail-open, in which case say so as loudly as a hook can.
unreachable() {
  echo "gitgud: $1" >&2
  if [ "${GITGUD_PREHOOK_FAIL_OPEN:-}" = "1" ]; then
    echo "gitgud: WARNING: GITGUD_PREHOOK_FAIL_OPEN=1 — allowing push WITHOUT branch protection" >&2
    exit 0
  fi
  echo "gitgud: refusing push (branch protection cannot be evaluated)" >&2
  exit 1
}

if ! command -v curl >/dev/null 2>&1; then
  unreachable "curl not installed; cannot reach protection backend"
fi

if [ -z "${GITGUD_HOOK_SECRET:-}" ]; then
  echo "gitgud: GITGUD_HOOK_SECRET is not set in the hook's environment;" >&2
  echo "gitgud: the protection backend would refuse this hook, so the push is refused" >&2
  exit 1
fi

# The node's hook secret (GitGudWeb.Plugs.HookAuth), inherited from the
# git process that runs this hook. Fed to curl on stdin rather than as
# an argument, so it doesn't show up in `ps`.
#
# No `-f`: an HTTP error status has to stay distinguishable from a
# connection that never happened. curl exits non-zero only for the
# latter; the status code comes back as the last line of the output.
set +e
RESP=$(printf 'x-gitgud-hook-token: %s\n' "${GITGUD_HOOK_SECRET}" | curl -s -X POST \
  --connect-timeout 10 \
  -H @- \
  -H "content-type: text/plain" \
  -H "x-repo-path: ${REPO_PATH}" \
  --data-binary "${STDIN_PAYLOAD}" \
  -w '\n%{http_code}' \
  "${GITGUD_PREHOOK_URL}" 2>/dev/null)
CURL_EXIT=$?
set -e

if [ "$CURL_EXIT" -ne 0 ]; then
  unreachable "protection backend unreachable at ${GITGUD_PREHOOK_URL} (curl exit ${CURL_EXIT})"
fi

HTTP_CODE=$(printf '%s' "$RESP" | tail -n 1)
BODY=$(printf '%s' "$RESP" | sed '$d')

case "$HTTP_CODE" in
  2??) ;;
  5??)
    unreachable "protection backend at ${GITGUD_PREHOOK_URL} failed (HTTP ${HTTP_CODE})"
    ;;
  *)
    echo "gitgud: protection backend refused the hook (HTTP ${HTTP_CODE}); push refused" >&2
    if [ "$HTTP_CODE" = "403" ]; then
      echo "gitgud: check that GITGUD_HOOK_SECRET matches the node's" >&2
    fi
    exit 1
    ;;
esac

STATUS=$(printf '%s' "$BODY" | head -n1 | awk '{print $1}')
REASON=$(printf '%s' "$BODY" | tail -n +2)

if [ "$STATUS" = "OK" ]; then
  exit 0
fi

echo "gitgud: push denied" >&2
printf '%s\n' "$REASON" >&2
exit 1