"""增量行 codecheck 钩子。
云端 codecheck 是增量的(只查 PR 改动的行),本地若整文件放开这些规则会被
存量违规淹没(本仓存量遍地超长行/超大函数/print),比云端更严、堵死提交。
本钩子用 ruff 全量检查后,只保留命中【本次 staged 改动行】的告警,从而在本地
复现云端增量行为:push 前预警自己引入的违规,又不被存量违规阻塞。
统一用 ruff(一个工具、速度快),select 一组对应云端华为 codecheck 的规则:
- E501 line-too-long → G.FMT.02 行宽不超过 120
- PLR0915 too-many-statements → 超大函数
- PLR6301 no-self-use(preview) → G.CLS.07 应为 staticmethod/classmethod
- T201 print → G.LOG.02 使用 logging 而非 print
- S607 start-process-partial-path → G.EDV.05 调外部程序用绝对路径
- PLR1722 sys-exit-alias → G.ERR.11 相关(避免裸 sys.exit/exit)
(重复代码 R0801 为跨文件块级,ruff 不查;其余华为专有规则若 ruff 无对应,
仍以云端结果为准。新增可查规则时,往 RULES 里追加即可。)
"""
import logging
import shutil
import subprocess
import sys
logger = logging.getLogger("incremental_codecheck")
RULES = "E501,T201,S607,PLR0915,PLR6301,PLR1722"
MAX_LINE = "120"
def staged_added_lines(path):
"""返回 path 在 staged diff 中新增/修改的行号集合(新文件侧行号)。"""
git_bin = shutil.which("git")
if not git_bin:
logger.error("git not found in PATH")
return set()
out = subprocess.run(
[git_bin, "diff", "--cached", "--unified=0", "--", path],
capture_output=True, text=True, check=False,
).stdout
added = set()
new_ln = 0
for line in out.splitlines():
if line.startswith("@@"):
seg = line.split("+", 1)[1].split("@@", 1)[0].strip()
start = int(seg.split(",")[0])
new_ln = start
elif line.startswith("+") and not line.startswith("+++"):
added.add(new_ln)
new_ln += 1
elif line.startswith("-") and not line.startswith("---"):
pass
else:
new_ln += 1
return added
def ruff_msgs(path):
"""跑 ruff,返回 [(lineno, text)],只含 RULES 选中的规则。
用 sys.executable -m ruff 调用(sys.executable 为绝对路径,满足 G.EDV.05,
且不依赖 PATH)。--preview 为开启 PLR6301(no-self-use 属 preview);配合
--select 限定后只报选中规则,不会引入其他 preview 规则的噪声(已实测)。
"""
proc = subprocess.run(
[
sys.executable, "-m", "ruff", "check",
"--preview",
f"--select={RULES}",
f"--line-length={MAX_LINE}",
"--output-format=concise",
"--no-cache",
path,
],
capture_output=True, text=True, check=False,
)
tool_error = proc.returncode >= 2 or (
proc.returncode == 1 and not proc.stdout.strip() and proc.stderr.strip()
)
if tool_error:
raise RuntimeError(
f"ruff 执行失败 (exit={proc.returncode}),本地增量门禁不可靠:\n{proc.stderr.strip()}"
)
msgs = []
for line in proc.stdout.splitlines():
parts = line.split(":", 3)
if len(parts) == 4 and parts[1].isdigit():
msgs.append((int(parts[1]), parts[3].strip()))
return msgs
def main(argv):
files = [f for f in argv if f.endswith(".py")]
if not files:
return 0
hits = []
for path in files:
added = staged_added_lines(path)
if not added:
continue
for lineno, text in ruff_msgs(path):
if lineno in added:
hits.append(f"{path}:{lineno}: {text}")
if hits:
logger.warning("[incremental-codecheck] 本次改动行命中云端规则,请修复:")
for h in hits:
logger.warning(" %s", h)
logger.warning("(仅检查你改动的行,不含存量违规;规则: %s)", RULES)
return 1
return 0
if __name__ == "__main__":
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s")
sys.exit(main(sys.argv[1:]))