From 566b3aa8a6cca6eeaac32e8ecc66561b739b3e7d Mon Sep 17 00:00:00 2001 From: sommerfeld Date: Wed, 13 May 2026 13:43:34 +0100 Subject: feat(git): pre-push also rejects commits with foreign committer Now flags any commit whose committer name+email doesn't match the local user.name / user.email (which respects the includeIf rules in ~/.config/git/config, so per-tree work/personal identities work). Author is left free: pulling someone else's commit and rebasing it locally re-stamps the committer to you, satisfies this gate, and the original author is preserved in the commit metadata. Both checks (signature + committer) run in one rev-list pass with tab-separated fields so awk parses unambiguously. --- dot_config/git/hooks/executable_pre-push | 42 ++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 8 deletions(-) (limited to 'dot_config/git/hooks') diff --git a/dot_config/git/hooks/executable_pre-push b/dot_config/git/hooks/executable_pre-push index f964305..5d94727 100755 --- a/dot_config/git/hooks/executable_pre-push +++ b/dot_config/git/hooks/executable_pre-push @@ -1,8 +1,12 @@ #!/bin/sh -# Reject pushes that include commits without a good signature. +# 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 has its own hooks). +# repo does, pointing at .githooks/ which delegates here). # # Bypass for one push: git push --no-verify @@ -10,6 +14,14 @@ 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) @@ -35,15 +47,28 @@ while read -r _local_ref local_sha remote_ref remote_sha; do set -- "${remote_sha}..${local_sha}" fi - bad=$(git rev-list --format='%H %G? %s' --no-commit-header "$@" | - awk -v ok="$ok" ' + # 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 } - !($2 in good) { print } + { + 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: unsigned or bad-signed commits found\n' >&2 + 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 @@ -51,8 +76,9 @@ while read -r _local_ref local_sha remote_ref remote_sha; do done if [ "$fail" -ne 0 ]; then - printf '\nfix with: git rebase --exec "git commit --amend --no-edit -S" \n' >&2 - printf 'bypass: git push --no-verify\n\n' >&2 + 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 -- cgit v1.3.1