chore(husky): add git hook rejecting bare @mentions in commit messages (#12387)

* chore: add git hook rejecting bare `@mentions` in commit messages

* fix(husky): align bare-mention boundary with GitHub linkification rules

* fix(husky): keep mention boundaries intact when stripping code spans

* fix(husky): ignore the verbose-commit diff below the scissors line

* refactor(husky): split bare-mention hook into small single-purpose functions

* fix(husky): exempt tilde-fenced code blocks from mention scanning

* fix(husky): trim trailing punctuation from the reported scoped handle

* refactor(husky): detect bare mentions by scanning, not regex

Replace the single packed detection regex with a plain character scan and
small predicate functions: for each `@`, exempt it when it sits inside
backticks (odd backtick count before it), when it is not followed by an
ASCII letter/digit, or when it is preceded by one (email-like). This is
easier to read and reason about than the regex.

Side effect: the `~~~` tilde-fence special case is gone, so a mention inside
a `~~~` block is now flagged. Backtick code spans and fences are still
exempt.

* docs(husky): note readHandle precondition that the char after @ is alphanumeric

* refactor(husky): spell out abbreviated identifiers in the mention hook

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Khải
2026-06-14 03:24:37 +07:00
committed by GitHub
parent 5b402ea22b
commit 8dfe43857d
3 changed files with 146 additions and 0 deletions

View File

@@ -1,2 +1,3 @@
node .husky/reject-bare-issue-refs.mjs "$1"
node .husky/reject-bare-mentions.mjs "$1"
pnpm commitlint --edit --config=commitlint.config.cjs

View File

@@ -0,0 +1,134 @@
// Rejects commit messages that contain a bare `@name` mention — one that is not
// wrapped in backticks. GitHub turns such a token into a real notification.
//
// Rationale lives in the error message below and in AGENTS.md.
import { readFileSync } from 'node:fs'
const messagePath = process.argv[2]
if (!messagePath) {
console.error('reject-bare-mentions: missing commit message file path argument')
process.exit(1)
}
const offenders = findBareMentions(scannableText(readFileSync(messagePath, 'utf8')))
if (offenders.size === 0) {
process.exit(0)
}
reportAndExit(offenders)
// Reduce the raw commit-message file to the text git will actually keep: the
// diff that `git commit -v` appends below the scissors line is dropped, and
// `#` comment lines are dropped — git strips both, so a mention in either is
// never committed and must not be flagged.
function scannableText (raw) {
return stripCommentLines(stripScissorsSection(raw))
}
function stripScissorsSection (raw) {
const lines = raw.split('\n')
const cut = lines.findIndex(isScissorsLine)
return cut === -1 ? raw : lines.slice(0, cut).join('\n')
}
// git's `commit -v` cut line, e.g. "# ------------------------ >8 ------------------------".
function isScissorsLine (line) {
return line.startsWith('#') && line.includes('>8') && line.includes('--')
}
function stripCommentLines (text) {
return text
.split('\n')
.filter((line) => !line.trimStart().startsWith('#'))
.join('\n')
}
// Find every distinct `@handle` that GitHub would linkify as a mention. We only
// care about GitHub's rendering, not the exact username rules: an `@` is a
// mention when it is not inside code, is followed by an ASCII letter or digit,
// and is not preceded by one (which would make it part of an email address).
function findBareMentions (text) {
const offenders = new Set()
for (let i = 0; i < text.length; i++) {
if (text[i] !== '@') continue
if (isInsideBackticks(text, i)) continue
if (!isAsciiAlphaNumeric(text[i + 1])) continue
if (isAsciiAlphaNumeric(text[i - 1])) continue
offenders.add(readHandle(text, i))
}
return offenders
}
// An `@` sits inside a code span when an odd number of backticks precede it (one
// is still open). This covers inline `` `code` `` and triple-backtick fences
// alike, without parsing Markdown.
function isInsideBackticks (text, index) {
let backticks = 0
for (let i = 0; i < index; i++) {
if (text[i] === '`') backticks++
}
return backticks % 2 === 1
}
// Read the handle starting at the `@`, for display in the error message. The
// caller only invokes this once the char after `@` is an ASCII alphanumeric, so
// that first char is always kept; trailing punctuation is then trimmed so the
// reported token is the part GitHub would actually link (e.g. the sentence-ending
// dot in "@pnpm/core." is dropped).
function readHandle (text, atIndex) {
let end = atIndex + 1
while (isHandleCharacter(text[end])) end++
while (end > atIndex + 1 && !isAsciiAlphaNumeric(text[end - 1])) end--
return text.slice(atIndex, end)
}
function isHandleCharacter (character) {
return isAsciiAlphaNumeric(character) ||
character === '-' ||
character === '_' ||
character === '.' ||
character === '/'
}
function isAsciiAlphaNumeric (character) {
return character !== undefined && (
(character >= 'a' && character <= 'z') ||
(character >= 'A' && character <= 'Z') ||
(character >= '0' && character <= '9')
)
}
function reportAndExit (offenders) {
const list = [...offenders].join(', ')
console.error(`
✖ Commit message rejected: bare @mention(s) found: ${list}
A bare "@name" is ambiguous and frequently wrong. Wrap it in backticks
instead, or remove it.
WHY THIS IS BLOCKED
GitHub turns any "@name" into a mention of that user/org/team. That is
wrong in both of the ways "@name" is normally meant:
1. If it is code (a scoped package like @pnpm/core, a handle, a path),
GitHub should NOT treat it as a mention.
2. If it really is a person, every push, force-push, and rebase that
carries the commit re-notifies them — which is noise nobody asked for.
HOW TO FIX
Wrap the reference in backticks so GitHub renders it as code and sends no
notification:
@pnpm/core -> \`@pnpm/core\`
@foo -> \`@foo\`
If you do not need the reference at all, just remove it.
DO NOT bypass this check with --no-verify, by editing/deleting this hook, or
with any suppression file. Fix the mention in the commit message instead.
`)
process.exit(1)
}

View File

@@ -171,6 +171,17 @@ For references to issues/PRs in **this** repo, also use the qualified form `pnpm
**Address the root cause when the hook fires.** Rewrite the reference into the correct unambiguous form. Never bypass the check with `git commit --no-verify`, by editing or deleting the hook, or with any suppression file.
### Never use a bare `@mention`
**Do not write a bare `@name` (an `@` followed by a username-like token) anywhere in a commit message.** A `commit-msg` hook (`.husky/reject-bare-mentions.mjs`) rejects them.
GitHub turns any `@name` into a mention of that user/org/team, which is wrong either way it is meant:
- If it is code (a scoped package like `@pnpm/core`, a handle, a path), GitHub should not treat it as a mention.
- If it really is a person, every push, force-push, and rebase that carries the commit re-notifies them — noise nobody asked for.
**Fix:** wrap the reference in backticks so GitHub renders it as code and sends no notification — e.g. `` `@pnpm/core` `` or `` `@foo` `` — or remove it if it is not needed. Never bypass the check with `git commit --no-verify`, by editing or deleting the hook, or with any suppression file.
## Changesets (TypeScript only)
If your changes affect published packages, you MUST create a changeset file in the `.changeset` directory. The changeset file should describe the change and specify the packages that are affected with the pending version bump types: patch, minor, or major.