欢迎光临
我们一直在努力

Git Hook + Codex自动生成Commit说明:为什么我不让AI直接提交

在这里插入图片描述

目录

    • 环境信息
    • 1. 先把自动生成和自动提交分开
    • 2. Git的三个Hook,别放错职责
    • 3. 只把暂存区diff交给只读调用
    • 4. 让prepare-commit-msg填入候选文本
    • 5. 用commit-msg拦住明显不合规的文本
    • 6. 安装Hook并运行
    • 7. 先验证三个容易出错的边界
    • 8. 如果要扩展到团队仓库
    • 总结
    • 参考资料

环境信息

  • Git 2.51.1
  • Python 3.12.13
  • os、re、subprocess为Python标准库,无需额外安装依赖。
  • Codex CLI:命令行中可以执行codex;本文使用codex exec –sandbox read-only。

很多人的提交记录最后会变成三种样子:update、fix bug、临时改一下。过几周再回头看,真正有用的信息几乎没有。

我今天想拆的不是“如何让AI自动提交代码”,而是一个更具体、也更容易落地的问题:能不能让Codex根据已经放入暂存区的改动,生成一条可修改、可校验的Commit说明?

答案是可以,但不要把git add、git commit一起交给Agent。本文把整个过程拆成四个边界:输入只取暂存区diff,生成过程使用只读调用,输出先交给人确认,最终文本再交给Git Hook校验。

在这里插入图片描述

这条链路的关键不是“让AI更大胆”,而是让它只能在一个很小的输入和输出范围里工作。

1. 先把自动生成和自动提交分开

更稳妥的边界是:

  • 只读取git diff –cached,也就是已经明确放进暂存区的改动。
  • 先做截断和脱敏,再让Codex生成一条候选说明。
  • 把候选说明写进Git的提交信息文件,开发者仍然可以修改。
  • 用commit-msg检查格式,最后由人执行git commit。
  • 2. Git的三个Hook,别放错职责

    Git 官方文档对这些 Hook 的时机有明确区分。pre-commit 适合跑格式检查、单元测试和 staged 文件检查,但这个阶段还没有最终的提交说明;prepare-commit-msg 收到提交信息文件,可以修改文件内容;commit-msg 负责检查最终文本,可以拒绝不合规的提交。

    这里有一个容易被忽略的细节:prepare-commit-msg 不能被 –no-verify 跳过,而 pre-commit 和 commit-msg 可以被这个参数跳过。因此,本地 Hook 是开发辅助,不是不可绕过的安全边界。真正不能漏过的规则,应该在 CI、远端仓库或受保护分支上再检查一次。

    在这里插入图片描述

    3. 只把暂存区diff交给只读调用

    git diff 默认看到的是工作区变化;git diff –cached(也可写成 –staged)看到的是暂存区与 HEAD 的差异。提交说明应该描述“这次准备提交什么”,所以这里必须使用后者。

    下面这个脚本只使用 Python 标准库。它做了三件事:读取 staged diff、对常见的键名做一次粗粒度脱敏、限制输入长度。脱敏只是最后一道缓冲,不代表可以把 .env、私钥或生产配置直接加入暂存区。

    # scripts/generate_commit_message.py
    import os
    import re
    import subprocess
    import sys

    COMMIT_RE = re.compile(r"^(feat|fix|refactor|test|docs|chore): .{1,72}$")
    SECRET_RE = re.compile(
    r"(?i)(api[_-]?key|token|secret|password)\\s*[:=]\\s*[\\"']?[^\\s\\"']+"
    )

    def staged_diff() –> str:
    result = subprocess.run(
    ["git", "diff", "–cached", "–no-ext-diff", "–unified=3", "–"],
    check=True,
    capture_output=True,
    text=True,
    encoding="utf-8",
    errors="replace",
    )
    return result.stdout

    def clean_diff(diff: str, limit: int = 16000) –> str:
    diff = SECRET_RE.sub(r"\\1=[REDACTED]", diff)
    if len(diff) > limit:
    diff = diff[:limit] + "\\n…[TRUNCATED]"
    return diff

    def valid_subject(output: str) –> str:
    for raw in output.splitlines():
    line = raw.strip().strip("`")
    if COMMIT_RE.fullmatch(line):
    return line
    return ""

    def main() –> int:
    diff = clean_diff(staged_diff())
    if not diff.strip():
    return 0

    # 让测试可以不依赖网络和账号;真实运行时删除这个分支即可。
    if os.getenv("CODEX_DRY_RUN") == "1":
    print("chore: update staged changes")
    return 0

    prompt = (
    "Read the staged diff from stdin and output exactly one line. "
    "Use one of feat, fix, refactor, test, docs, chore followed by ': '. "
    "Write an imperative subject, no markdown, no explanation, max 72 chars. "
    "Do not invent behavior that is absent from the diff."
    )
    codex = os.getenv("CODEX_BIN", "codex")
    result = subprocess.run(
    [codex, "exec", "–ephemeral", "–sandbox", "read-only", prompt],
    input=diff,
    capture_output=True,
    text=True,
    encoding="utf-8",
    errors="replace",
    timeout=45,
    check=False,
    )
    subject = valid_subject(result.stdout)
    if subject:
    print(subject)
    return 0

    if __name__ == "__main__":
    try:
    raise SystemExit(main())
    except (OSError, subprocess.SubprocessError) as exc:
    print(f"Codex unavailable; keep commit message manual: {exc}", file=sys.stderr)
    raise SystemExit(0)

    这里用的是 codex exec,不是让 Agent 进入交互式终端。官方文档说明,非交互模式默认使用只读沙箱,并且最终文本会写到标准输出,适合接到脚本里。脚本还设置了失败即回退:Codex 没装、超时或输出不符合格式时,不阻塞正常提交。

    4. 让prepare-commit-msg填入候选文本

    #!/bin/sh
    # .githooks/prepare-commit-msg
    set -eu

    MSG_FILE="$1"
    SOURCE="${2-}"

    # 合并提交、压缩提交和已有提交信息不由模型重写。
    case "$SOURCE" in
    merge|squash|commit) exit 0 ;;
    esac

    git diff –cached –quiet && exit 0

    TMP_FILE="${MSG_FILE}.codex.tmp"
    trap 'rm -f "$TMP_FILE"' EXIT HUP INT TERM

    python3 scripts/generate_commit_message.py > "$TMP_FILE"
    if [ -s "$TMP_FILE" ]; then
    cat "$TMP_FILE" > "$MSG_FILE"
    fi

    不要在这个 Hook 里直接执行 git commit。否则一个本来只是“生成文本”的动作,就会变成会改变仓库历史的动作;而且提交前的人工检查也被绕开了。

    5. 用commit-msg拦住明显不合规的文本

    #!/bin/sh
    # .githooks/commit-msg
    set -eu

    MSG_FILE="$1"
    FIRST_LINE="$(sed -n '1p' "$MSG_FILE")"

    case "$FIRST_LINE" in
    feat:*|fix:*|refactor:*|test:*|docs:*|chore:*) ;;
    *)
    echo "commit subject must start with feat/fix/refactor/test/docs/chore:" >&2
    exit 1
    ;;
    esac

    if [ "${#FIRST_LINE}" -gt 72 ]; then
    echo "commit subject is longer than 72 characters" >&2
    exit 1
    fi

    if grep -Eiq '(api[_-]?key|token|secret|password)[[:space:]]*[:=]' "$MSG_FILE"; then
    echo "possible secret found in commit message" >&2
    exit 1
    fi

    格式校验故意保持简单。它不是为了把团队变成“提交信息警察”,而是让自动生成失败时有一个清晰的回退路径:删除候选文本,手动写一条准确的说明。

    6. 安装Hook并运行

    在仓库根目录执行:

    mkdir -p .githooks
    git config core.hooksPath .githooks
    chmod +x .githooks/prepare-commit-msg .githooks/commit-msg

    git add src/app.py
    git commit

    首次验证建议先不调用Codex:

    CODEX_DRY_RUN=1 git commit

    然后检查三种情况:

    场景预期结果
    工作区有改动,但没有 git add 不生成候选说明
    staged diff 正常 出现 chore: update staged changes,可以人工修改
    手动写成 update commit-msg 拒绝提交

    真实使用时,把 CODEX_DRY_RUN 去掉即可。Git Hook 文件可以提交到仓库,core.hooksPath 的配置则需要每位开发者在本地安装;团队若要统一执行,应在 CI 中复用同一套检查。

    7. 先验证三个容易出错的边界

    有三个风险不能靠一段脚本消失:

    第一,模型看到的 diff 可能带出密钥。本文的正则只能识别一小部分 key=value 形式,不能替代密钥扫描,更不能替代提交前的人工检查。

    第二,–no-verify 可以绕过部分本地 Hook。Git 官方 FAQ 也提醒,客户端 Hook 不适合作为强制策略;需要强制执行的规则必须放在服务器端或 CI。

    第三,提交说明不是事实证明。AI 可能把重命名写成重构,把局部修复写成“解决全部问题”。所以提示词里要限制“只能依据 diff”,而最终的责任仍在提交者。

    8. 如果要扩展到团队仓库

    在个人实验仓库里,自动生成提交说明很省心;在真实项目里,我只会让Codex生成候选文本,不会让它直接写入Git历史。工程上值得自动化的是重复劳动,不能自动放弃的是权限边界、敏感信息检查和最终确认。

    如果要在团队仓库中推广,建议把.githooks目录纳入版本控制,并把关键检查同步到CI或受保护分支。原因很现实:本地Hook可以被–no-verify绕过,不能把它当作唯一的强制边界。

    如果你也在用Agent生成Commit,最值得先检查的不是模型名称,而是这三个问题:它读的是工作区还是暂存区?它有没有git commit权限?本地检查被绕过之后,远端还有没有第二道门?

    总结

    这次真正值得自动化的,不是让AI替开发者写入Git历史,而是把“查看改动、整理提交说明、检查格式”这些重复劳动压缩成一个可回退的流程。

    最终的边界可以概括成一句话:Codex只读取经过人工确认的暂存区diff,只输出候选文本;开发者负责确认,Git负责校验,CI或远端仓库负责最后的强制检查。

    参考资料

  • Git Hooks 官方文档
  • git diff 官方文档
  • Codex CLI 非交互模式官方文档
  • 代码说明:本文示例默认在macOS/Linux的Git仓库中运行;Windows用户可将python3替换为本机Python命令,并保持Hook文件具备可执行权限。正则脱敏只覆盖常见键名,不能代替正式的密钥扫描。

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Git Hook + Codex自动生成Commit说明:为什么我不让AI直接提交
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址