diff options
| author | 2026-05-13 13:43:34 +0100 | |
|---|---|---|
| committer | 2026-05-13 13:43:34 +0100 | |
| commit | 0ee8f260727f3e88d26d06f59e5c2fa71211a06d (patch) | |
| tree | a5beb8045e63298142eeabd5049a110e20ea2758 /dot_config/git | |
| parent | 9b2756e4b8ffcce1a2d494cf32a99b971c5ae13f (diff) | |
| download | dotfiles-0ee8f260727f3e88d26d06f59e5c2fa71211a06d.tar.gz dotfiles-0ee8f260727f3e88d26d06f59e5c2fa71211a06d.tar.bz2 dotfiles-0ee8f260727f3e88d26d06f59e5c2fa71211a06d.zip | |
feat(git): global pre-push hook rejecting unsigned commits
Activated via core.hooksPath = ~/.config/git/hooks in the global
git config. The hook walks each ref being pushed (range: remote..local
or, for new branches, local --not --remotes) and checks %G? on every
commit. Accepts G/U/X/Y (good signature variants), rejects N/B/E/R
(no signature, bad, missing key, revoked).
Bypass: git push --no-verify
This repo overrides hooksPath to .githooks/ for its just-check
pre-commit gate, so a thin .githooks/pre-push delegates to the global
hook to keep the policy enforced here too.
Diffstat (limited to 'dot_config/git')
| -rw-r--r-- | dot_config/git/config | 1 | ||||
| -rwxr-xr-x | dot_config/git/hooks/executable_pre-push | 59 |
2 files changed, 60 insertions, 0 deletions
diff --git a/dot_config/git/config b/dot_config/git/config index 33687e7..db562a6 100644 --- a/dot_config/git/config +++ b/dot_config/git/config @@ -9,6 +9,7 @@ [core] whitespace = trailing-space,cr-at-eol pager = delta + hooksPath = ~/.config/git/hooks [branch] sort=-committerdate [diff] diff --git a/dot_config/git/hooks/executable_pre-push b/dot_config/git/hooks/executable_pre-push new file mode 100755 index 0000000..f964305 --- /dev/null +++ b/dot_config/git/hooks/executable_pre-push @@ -0,0 +1,59 @@ +#!/bin/sh +# Reject pushes that include commits without a good signature. +# 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 has its own hooks). +# +# Bypass for one push: git push --no-verify + +set -eu + +zero=$(git hash-object --stdin </dev/null | tr '0-9a-f' '0') + +# %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 + + bad=$(git rev-list --format='%H %G? %s' --no-commit-header "$@" | + awk -v ok="$ok" ' + BEGIN { split(ok, a, " "); for (i in a) good[a[i]] = 1 } + !($2 in good) { print } + ') + + if [ -n "$bad" ]; then + if [ "$fail" -eq 0 ]; then + printf '\nrefusing to push: unsigned or bad-signed commits found\n' >&2 + fi + printf '\non %s:\n%s\n' "$remote_ref" "$bad" >&2 + fail=1 + fi +done + +if [ "$fail" -ne 0 ]; then + printf '\nfix with: git rebase --exec "git commit --amend --no-edit -S" <base>\n' >&2 + printf 'bypass: git push --no-verify\n\n' >&2 + exit 1 +fi + +exit 0 |
