diff options
| author | 2026-05-13 13:43:34 +0100 | |
|---|---|---|
| committer | 2026-05-13 13:43:34 +0100 | |
| commit | 566b3aa8a6cca6eeaac32e8ecc66561b739b3e7d (patch) | |
| tree | ceaea8e2ae66450923fa6ebca20e6d4655d3fe83 | |
| parent | 0ee8f260727f3e88d26d06f59e5c2fa71211a06d (diff) | |
| download | dotfiles-566b3aa8a6cca6eeaac32e8ecc66561b739b3e7d.tar.gz dotfiles-566b3aa8a6cca6eeaac32e8ecc66561b739b3e7d.tar.bz2 dotfiles-566b3aa8a6cca6eeaac32e8ecc66561b739b3e7d.zip | |
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.
| -rwxr-xr-x | dot_config/git/hooks/executable_pre-push | 42 |
1 files changed, 34 insertions, 8 deletions
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 </dev/null | tr '0-9a-f' '0') +expected_name=$(git config user.name || true) +expected_email=$(git config user.email || true) + +if [ -z "$expected_name" ] || [ -z "$expected_email" ]; then + printf 'pre-push: user.name or user.email is unset; refusing.\n' >&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 <TAB> %G? <TAB> committer-name + # <TAB> committer-email <TAB> 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" <base>\n' >&2 - printf 'bypass: git push --no-verify\n\n' >&2 + printf '\nfix: git rebase --exec "git commit --amend --no-edit -S" <base>\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 |
