aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/dot_config/git
diff options
context:
space:
mode:
authorLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-05-13 13:43:34 +0100
committerLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2026-05-13 13:43:34 +0100
commit0ee8f260727f3e88d26d06f59e5c2fa71211a06d (patch)
treea5beb8045e63298142eeabd5049a110e20ea2758 /dot_config/git
parent9b2756e4b8ffcce1a2d494cf32a99b971c5ae13f (diff)
downloaddotfiles-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/config1
-rwxr-xr-xdot_config/git/hooks/executable_pre-push59
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