Files
session-delete/scripts/check-commit-log.mjs
T
pyh 83eae74f63 📝 docs: 补上提交信息规范的自查脚本
scripts/check-commit-log.mjs 按 CONTRIBUTING.md 的类型表检查提交首行,
只报告不拦提交。它从规范文件里读 emoji 对照,所以改表不用改脚本。

同时收紧规范:emoji 由「可选」改为「必需且必须与类型一致」。原先的写法
下 ✨ docs: 这类不匹配不会被发现。
2026-09-30 16:50:29 +08:00

75 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Checks real commit subjects (newest first) against the CONTRIBUTING.md spec:
// the documented regex must parse them, the leading emoji must be present and
// match the type table exactly, and the subject must stay within 72 width.
//
// This is a reporting tool, not a hook: it never blocks a commit.
//
// Usage:
// node scripts/check-commit-log.mjs # newest 10 commits
// node scripts/check-commit-log.mjs 50 # newest 50 commits
// node scripts/check-commit-log.mjs all # every commit
// DSD_SPEC=path/to/CONTRIBUTING.md node scripts/check-commit-log.mjs
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const specPath = process.env.DSD_SPEC ?? resolve(repoRoot, 'CONTRIBUTING.md')
const doc = readFileSync(specPath, 'utf8')
const count = process.argv[2] ?? '10'
/** The regex the spec documents for stripping a leading emoji. */
const SUBJECT = new RegExp(
'^(?:\\p{Extended_Pictographic}\\uFE0F?\\s+)?' +
'(?<type>[a-z]+)' +
'(?:\\((?<scope>[^)]*)\\))?' +
'(?<breaking>!)?' +
':\\s(?<desc>.+)$',
'u',
)
// The spec's table is the single source of truth for the type → emoji mapping,
// so a table edit cannot drift from what this script checks.
const emojiOfType = new Map()
for (const row of doc.split('\n').filter((line) => /^\| `[a-z]+` \| /.test(line))) {
const cells = row.split('|').map((cell) => cell.trim())
emojiOfType.set(cells[1].replaceAll('`', ''), cells[2])
}
if (emojiOfType.size === 0) throw new Error(`${specPath} has no type table to read`)
/** Strip the variation selector, so '♻' and '♻️' compare equal. */
const base = (text) => text.replace(/\uFE0F/g, '')
/** Display width, counting anything above U+2000 as double. */
const widthOf = (text) => [...text].reduce((n, c) => n + (c.codePointAt(0) > 0x2000 ? 2 : 1), 0)
const args = count === 'all' ? ['log', '--format=%h%x09%s'] : ['log', `-${Number(count)}`, '--format=%h%x09%s']
const rows = execFileSync('git', args, { encoding: 'utf8', cwd: repoRoot })
.trim().split('\n').filter((line) => line !== '')
let problems = 0
for (const row of rows) {
const [hash, subject] = row.split('\t')
const notes = []
const match = SUBJECT.exec(subject)
if (match === null) {
notes.push('缺少类型前缀,或整体格式不是 <emoji> <type>[(scope)][!]: <描述>')
} else {
const expected = emojiOfType.get(match.groups.type)
const lead = [...subject][0]
if (expected === undefined) notes.push(`类型不在规范表里:${match.groups.type}`)
else if (base(lead) !== base(expected)) notes.push(`类型 ${match.groups.type} 的 emoji 应为 ${expected},实际是 ${lead}`)
const width = widthOf(subject)
if (width > 72) notes.push(`首行宽度 ${width},超过 72`)
if (/[。.]$/.test(subject)) notes.push('首行结尾不应有句号')
}
if (notes.length > 0) problems += 1
const mark = notes.length === 0 ? 'ok ' : 'FAIL'
console.log(` ${mark} ${hash} ${notes.length === 0 ? '' : notes.join(';') + ' — '}${subject}`)
}
console.log(problems === 0
? `\n检查了 ${rows.length} 条提交,全部符合规范`
: `\n检查了 ${rows.length} 条提交,其中 ${problems} 条不符合规范(规范文件:${specPath})`)
process.exit(problems === 0 ? 0 : 1)