From 35f024c159836d000960f38fc1a3ce86f901d1ff Mon Sep 17 00:00:00 2001 From: Rootiest Date: Sat, 12 Sep 2026 20:49:33 -0400 Subject: [PATCH 1/2] feat(git): add pre-push hook to reject unsigned commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gittyup commits via libgit2 directly and never invokes gpg, silently ignoring commit.gpgsign — root cause of an unsigned commit reaching main. Adds a tracked .githooks/pre-push that rejects any push carrying a commit with no signature or a bad signature, bypassable with --no-verify. Wiring core.hooksPath to it is a per-machine concern, done separately in user-dots, not shipped here. --- .githooks/pre-push | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..35fdd4e --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Rejects a push that would put an unsigned or bad-signature commit onto a +# branch. Root cause this guards against: GUI git clients (e.g. Gittyup) +# commit via libgit2 directly and never invoke gpg, silently ignoring +# commit.gpgsign. Bypass intentionally with `git push --no-verify`. +zero=0000000000000000000000000000000000000000 + +while read -r local_ref local_sha remote_ref _remote_sha; do + [ "$local_sha" = "$zero" ] && continue # branch deletion + case "$remote_ref" in refs/heads/*) ;; *) continue ;; esac + + if [ "$_remote_sha" = "$zero" ]; then + range="$local_sha --not --remotes" + else + range="$_remote_sha..$local_sha" + fi + + bad="" + for sha in $(git rev-list $range --); do + sig="$(git log -1 --pretty=%G? "$sha")" + case "$sig" in + N | B) bad="$bad $sha" ;; + esac + done + + if [ -n "$bad" ]; then + echo "pre-push: unsigned or bad-signature commit(s) pushing to $remote_ref, refusing:" >&2 + for sha in $bad; do + git log -1 --pretty=' %h %G? %s' "$sha" >&2 + done + echo "Fix: git commit --amend -S (or rebase -i + amend), then push again." >&2 + echo "Bypass: git push --no-verify" >&2 + exit 1 + fi +done + +# Chain to the global/system hook this local override is shadowing. +global_hooks="$(git config --global core.hooksPath 2>/dev/null)" +[ -z "$global_hooks" ] && global_hooks="$(git config --system core.hooksPath 2>/dev/null)" +if [ -n "$global_hooks" ]; then + global_hooks="${global_hooks/#\~/$HOME}" # git stores ~ verbatim + [ -x "$global_hooks/pre-push" ] && exec "$global_hooks/pre-push" "$@" +fi +exit 0 From 6c35dc43375d91710e381fedebaeb2f73fbbd5ce Mon Sep 17 00:00:00 2001 From: Rootiest Date: Sat, 12 Sep 2026 20:56:48 -0400 Subject: [PATCH 2/2] docs(contributing): document the pre-push signature hook opt-in Contributors need to know .githooks/pre-push exists and how to point their clone at it, since it isn't wired up automatically. --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45b6d41..5f9f0c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,21 @@ If you're touching anything under `docs/manual/`, you'll also want `pandoc`, (see [Documentation Pipeline](#documentation-pipeline)) — otherwise CI will catch problems on push. +**Point your clone at the tracked git hooks.** `.githooks/pre-push` rejects a +push carrying an unsigned or bad-signature commit — GUI git clients (Gittyup +included) commonly commit via libgit2 and skip `commit.gpgsign` silently. +This isn't wired up automatically (most users of this config never push to +this repo), so opt in once per clone: + +```fish +git config core.hooksPath .githooks +``` + +Bypass a specific push with `git push --no-verify` if you have a genuine +reason to. See [Secrets & Machine-Specific +Config](#secrets--machine-specific-config) if you'd rather set this from +your own `local.fish` than type it by hand. + ## Issues Issues live on the Gitea repo. Three templates cover the common cases, each