149 lines
5.3 KiB
Bash
Executable File
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
|