aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/dot_config/git/hooks/executable_pre-push
blob: 5d947274871b0f2b05ea94c4c2922e5cc0112062 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/bin/sh
# 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 delegates here).
#
# Bypass for one push: git push --no-verify

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)
#   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

  # 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 }
      {
        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: 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
  fi
done

if [ "$fail" -ne 0 ]; then
  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

exit 0