PureYAML/.githooks/commit-msg

149 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# commit-msg hook: reject style tells in commit messages.
#
# Enforces github-discipline.md Rule 5.1 (no AI attribution) and
# Rule 5.2 (no em dashes U+2014) at the commit boundary.
#
# Install (per repo): git config core.hooksPath .githooks
#
# Portable: uses only BSD-compatible grep (no -P, no \x escapes in patterns).
set -e
MSG_FILE="$1"
[ -z "$MSG_FILE" ] && exit 0
[ ! -f "$MSG_FILE" ] && exit 0
# Strip comment lines (lines starting with #) and the diff scissors block.
CONTENT=$(awk '
/^# ------------------------ >8 ------------------------$/ { exit }
/^[[:space:]]*#/ { next }
{ print }
' "$MSG_FILE")
FAIL=0
# Em dash U+2014 = UTF-8 bytes E2 80 94. Build the literal as a variable so the
# script source itself contains no em-dash character.
EMDASH=$(printf '\xe2\x80\x94')
if LC_ALL=C printf '%s' "$CONTENT" | grep -qF -- "$EMDASH"; then
printf 'commit-msg: forbidden em dash (U+2014) in commit message.\n' >&2
printf ' Replace with a comma, period, colon, semicolon, or restructure.\n' >&2
printf ' Rule: github-discipline.md Rule 5.2\n' >&2
printf ' Locations (line numbers in the cleaned message):\n' >&2
LC_ALL=C printf '%s\n' "$CONTENT" | grep -nF -- "$EMDASH" | sed 's/^/ /' >&2
FAIL=1
fi
# Rule 5.1: literal AI-attribution phrases.
LITERAL_PATTERNS=(
'Co-Authored-By: Claude'
'Co-authored-by: Claude'
'Generated with Claude Code'
'Generated with Claude'
'Generated by Claude'
'with help from Claude'
'Powered by Claude'
'powered by Claude'
'as an AI'
'As an AI'
'this commit was generated'
'This commit was generated'
'this change was generated'
'This change was generated'
)
for p in "${LITERAL_PATTERNS[@]}"; do
if printf '%s' "$CONTENT" | grep -qF -- "$p"; then
printf 'commit-msg: forbidden AI-attribution phrase: %s\n' "$p" >&2
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
FAIL=1
fi
done
# Rule 5.1: AI-tell signature emojis used as flourish at end of line.
# Build emoji byte sequences in a variable so the script source stays clean.
EMOJI_TELLS=$(printf '\xf0\x9f\xa4\x96\n\xe2\x9c\xa8\n\xf0\x9f\xaa\x84\n\xf0\x9f\xa7\xa0\n\xf0\x9f\xa6\xbe\n\xf0\x9f\xa4\x9d')
while IFS= read -r emoji; do
[ -z "$emoji" ] && continue
if LC_ALL=C printf '%s' "$CONTENT" | grep -qF -- "$emoji"; then
printf 'commit-msg: forbidden AI-signature emoji in commit message.\n' >&2
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
FAIL=1
break
fi
done <<<"$EMOJI_TELLS"
# Rule 5.1: AI-tell preamble phrasings at start of any line.
PREAMBLE_PATTERNS=(
"I'll now"
"Let me "
"Here is a quick summary"
"Here's a quick summary"
)
for p in "${PREAMBLE_PATTERNS[@]}"; do
if printf '%s' "$CONTENT" | grep -qE -- "^${p}"; then
printf 'commit-msg: forbidden AI-tell preamble at line start: %s\n' "$p" >&2
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
FAIL=1
fi
done
# Rule 5.1: Cursor attribution. Reject Cursor co-author trailers, the agent
# email, any @cursor.com address, and "generated with Cursor" style tells.
# Case-insensitive on purpose: cursoragent@cursor.com, Co-authored-by: Cursor.
CURSOR_LITERALS=(
'Co-Authored-By: Cursor'
'Co-authored-by: Cursor'
'Generated with Cursor'
'Generated by Cursor'
'with help from Cursor'
'Powered by Cursor'
)
for p in "${CURSOR_LITERALS[@]}"; do
if printf '%s' "$CONTENT" | grep -qiF -- "$p"; then
printf 'commit-msg: forbidden Cursor attribution: %s\n' "$p" >&2
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
FAIL=1
fi
done
if printf '%s' "$CONTENT" | grep -qiE -- '@cursor\.com'; then
printf 'commit-msg: forbidden Cursor email (@cursor.com) in commit message.\n' >&2
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
FAIL=1
fi
# Author / committer identity must not be Cursor. git var resolves the exact
# identity (including --author / GIT_*_IDENT overrides) this commit will record.
for ident in "$(git var GIT_AUTHOR_IDENT 2>/dev/null)" "$(git var GIT_COMMITTER_IDENT 2>/dev/null)"; do
[ -z "$ident" ] && continue
if printf '%s' "$ident" | grep -qiE -- '@cursor\.com|(^|[^[:alpha:]])cursor(agent)?([^[:alpha:]]|$)'; then
printf 'commit-msg: forbidden Cursor identity as author/committer: %s\n' "$ident" >&2
printf ' Fix: set git config user.name / user.email to your own identity.\n' >&2
FAIL=1
fi
done
# commits.md Rule 1/2: subject must follow Conventional Commits (lowercase type
# from the allowed set, optional scope, optional ! for breaking) and be at most
# 72 characters. Merge/revert/fixup/squash subjects are tooling-generated and
# exempt.
SUBJECT=$(printf '%s\n' "$CONTENT" | sed '/^[[:space:]]*$/d' | head -1)
case "$SUBJECT" in
"Merge "*|"Revert "*|"fixup! "*|"squash! "*|"") ;;
*)
if [ "${#SUBJECT}" -gt 72 ]; then
printf 'commit-msg: subject is %s chars (max 72).\n' "${#SUBJECT}" >&2
printf ' Rule: commits.md Rule 1\n' >&2
FAIL=1
fi
if ! printf '%s' "$SUBJECT" | grep -qE '^(feat|fix|critic-fix|docs|style|refactor|perf|test|build|ci|chore)(\([a-z0-9._-]+\))?!?: .+'; then
printf 'commit-msg: subject must be Conventional Commits: "type(scope): summary".\n' >&2
printf ' Allowed types: feat fix critic-fix docs style refactor perf test build ci chore\n' >&2
printf ' Rule: commits.md Rule 1, Rule 2\n' >&2
FAIL=1
fi
;;
esac
exit $FAIL