#!/bin/sh # Reject pushes that include commits without a good signature, or whose # committer does not match this repo's user.name / user.email. # (Author is left free so rebased / amended foreign commits are fine # as long as you are the one re-committing them.) # # Activated via core.hooksPath in ~/.config/git/config so it applies to # every repo unless that repo overrides hooksPath itself (this dotfiles # repo does, pointing at .githooks/ which delegates here). # # Bypass for one push: git push --no-verify set -eu zero=$(git hash-object --stdin &2 exit 1 fi # %G? signature status codes from git-log: # G good signature # U good, unknown validity (e.g. trust level not set) # X good but expired signature # Y good but key is expiring # R good but key has been revoked # B bad signature # E signature cannot be checked (e.g. missing key) # N no signature # We accept G/U/X/Y and reject anything else. ok='G U X Y' fail=0 while read -r _local_ref local_sha remote_ref remote_sha; do # Branch deletion: nothing to verify on our side. [ "$local_sha" = "$zero" ] && continue if [ "$remote_sha" = "$zero" ]; then # New branch: verify the commits unique to this push, excluding # anything already reachable from any remote-tracking ref. set -- "$local_sha" --not --remotes else set -- "${remote_sha}..${local_sha}" fi # One pass: per-commit emit SHA %G? committer-name # committer-email subject. Tabs aren't valid in identity # fields, so awk -F'\t' parses unambiguously. bad=$(git rev-list --format='%H%x09%G?%x09%cn%x09%ce%x09%s' \ --no-commit-header "$@" | awk -F'\t' -v ok="$ok" -v en="$expected_name" -v ee="$expected_email" ' BEGIN { split(ok, a, " "); for (i in a) good[a[i]] = 1 } { reasons = "" if (!($2 in good)) reasons = reasons " [sig=" $2 "]" if ($3 != en || $4 != ee) { reasons = reasons " [committer=" $3 " <" $4 ">]" } if (reasons != "") print $1 reasons " " $5 } ') if [ -n "$bad" ]; then if [ "$fail" -eq 0 ]; then printf '\nrefusing to push: bad commits found\n' >&2 printf '(expected committer: %s <%s>)\n' \ "$expected_name" "$expected_email" >&2 fi printf '\non %s:\n%s\n' "$remote_ref" "$bad" >&2 fail=1 fi done if [ "$fail" -ne 0 ]; then printf '\nfix: git rebase --exec "git commit --amend --no-edit -S" \n' >&2 printf ' (re-stamps committer to your current identity and re-signs)\n' >&2 printf 'bypass: git push --no-verify\n\n' >&2 exit 1 fi exit 0