#!/bin/sh # commit-msg hook — ai-code-style-kit # 规则源:skills/commit-style/SKILL.md(v1.0)§2/§3.5/§7.1 # 同规则的权威实现见 extension/rules.cjs(两侧口径必须一致,tests 负责对照) # 跨平台:Linux / macOS 原生 sh;Windows 经 Git for Windows 自带 sh。零依赖。 # # 安装:hooks/install.sh(或 install.ps1),或手动: # mkdir -p .githooks && cp hooks/commit-msg .githooks/ && git config core.hooksPath .githooks msg_file="$1" [ -f "$msg_file" ] || exit 0 # 首行(去首尾空白与 CR,与 rules.cjs 的 trim 对齐) first=$(sed -n '1p' "$msg_file" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') # 豁免:git 自动生成的 merge / revert 消息(SKILL.md §3.5) case "$first" in "Merge "*|'Revert "'*) exit 0 ;; esac errors=0 fail() { echo "commit-msg: $*" >&2 errors=$((errors + 1)) } TYPE_RE='(feat|fix|perf|refactor|test|docs|build|ci|chore|style|revert)' # header 格式:type(scope)!: 主题 if ! printf '%s' "$first" | grep -Eq "^${TYPE_RE}(\\([a-z0-9_.-]+\\))?!?: .+"; then echo "commit-msg: header 不符合 type(scope)!: 主题 格式(type ∈ feat/fix/perf/refactor/test/docs/build/ci/chore/style/revert):$first" >&2 echo "commit-msg: 提交被拒绝。规范全文见 skills/commit-style/SKILL.md" >&2 exit 1 fi subject=$(printf '%s' "$first" | sed -E "s/^${TYPE_RE}(\\([a-z0-9_.-]+\\))?!?: //" | sed -E 's/^[[:space:]]+//') hdr_type=$(printf '%s' "$first" | sed -E "s/^(${TYPE_RE}).*/\1/") # 主题结尾不加句号("v1.0" 这类数字点结尾不算) case "$subject" in *。) fail "主题结尾不加句号" ;; esac if printf '%s' "$subject" | grep -Eq '[^0-9]\.$'; then fail "主题结尾不加句号" fi # 显示宽度:全角记 2 / 半角记 1,上限 100(约 50 个汉字) # 探测 wc -m 是否按码点计数(UTF-8 locale);不行则依次尝试常见 UTF-8 locale; # 都不行(无 UTF-8 环境的极端情况)跳过宽度检查(echo 0),其余规则仍生效。 dw_width() { _probe=$(printf '\344\275\240' | wc -m) if [ "$_probe" -eq 1 ] 2>/dev/null; then _c=$(printf '%s' "$1" | wc -m); _b=$(printf '%s' "$1" | wc -c) echo $(( _c + (_b - _c) / 2 )); return fi for _l in C.UTF-8 en_US.UTF-8; do _probe=$(printf '\344\275\240' | LC_ALL="$_l" wc -m) if [ "$_probe" -eq 1 ] 2>/dev/null; then _c=$(printf '%s' "$1" | LC_ALL="$_l" wc -m); _b=$(printf '%s' "$1" | wc -c) echo $(( _c + (_b - _c) / 2 )); return fi done echo 0 } width=$(dw_width "$subject") if [ "$width" -gt 100 ] 2>/dev/null; then fail "主题显示宽度 $width > 100(全角记 2/半角记 1,约 50 个汉字)" fi # 空泛词封闭匹配(与 extension/rules.cjs 一致):黑名单词单独,或仅带泛称宾语 # en 归一化与 JS 的 trim+小写+折叠空白对齐(tr 只做小写与压缩空格会漏多空格/tab 形态) zh=$(printf '%s' "$subject" | tr -d '[:space:]') for _w in 更新 优化 修复 完善 调整 修改 重构 小改动; do for _g in "" 代码 性能 bug BUG Bug 功能 问题 文件 逻辑 一些问题; do if [ "$zh" = "$_w$_g" ]; then fail "主题为空泛词(单独或仅带泛称宾语):\"$subject\" —— 宾语须具体到模块/现象/数字" break 2 fi done done en=$(printf '%s' "$subject" | tr '[:upper:]' '[:lower:]' | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//') for _w in update fix optimize refactor cleanup tweak improve wip; do for _g in "" code codes performance perf bug bugs file files logic stuff things; do if [ -z "$_g" ]; then _pat="$_w"; else _pat="$_w $_g"; fi if [ "$en" = "$_pat" ]; then fail "主题为空泛词(单独或仅带泛称宾语):\"$subject\" —— 宾语须具体到模块/现象/数字" break 2 fi done done # 验证行:feat/fix/perf 必须有(token 双语:验证: / Verified:) case "$hdr_type" in feat|fix|perf) if ! grep -Eq '^验证:[[:blank:]]*[^[:space:]]' "$msg_file" && \ ! grep -Eq '^[Vv]erified:[[:blank:]]*[^[:space:]]' "$msg_file"; then fail "type 为 $hdr_type,必须有\"验证:\"行(英文项目 Verified:)——只写真实跑过的检查与结果;没跑过就先别提交" fi ;; esac # BREAKING CHANGE 脚注必须全大写 if grep -Eiq '^[[:blank:]]*breaking[[:blank:]]+change[[:blank:]]*:' "$msg_file" && \ ! grep -Eq '^[[:blank:]]*BREAKING CHANGE[[:blank:]]*:' "$msg_file"; then fail "脚注 BREAKING CHANGE 必须全大写" fi if [ "$errors" -gt 0 ]; then echo "commit-msg: 提交被拒绝($errors 个问题)。规范全文见 skills/commit-style/SKILL.md" >&2 exit 1 fi exit 0