某支付项目上线前夜,我打开仓库看提交记录,入眼是一排“fix”、“update”、“test”,想查个改动来源得靠猜。更让人崩溃的是,本地跑得好好的代码,推到远端集成环境里就挂,查了半天才发现是有人把临时调试代码混进了提交。那一晚之后,我花了一个周末把 Git Hooks 系统地摸了一遍,用脚本把提交校验、代码检查、推送前测试全部串进 Git 原生流程里,仓库从此干净得像换了个人在管。这篇就是把那一段实战经验沉淀下来,从机制原理到可直接抄的脚本,写给想用 Git Hooks 又不知道从哪下手的朋友。
我会先讲清楚这玩意儿到底怎么工作,再手把手写几个高频场景的完整 hook,最后把我在真实项目里踩过的坑和排查思路毫无保留地整理给你。
1. 先弄懂机制:Git Hooks 本质上是一套“事件回调”
1.1 一句话解释:Git 在关键动作发生时执行你的脚本
Git Hooks 不是某个插件,也不是需要额外安装的独立软件,它是 Git 内置的一个扩展点。你可以把它理解成 Git 在提交、推送、合并等关键动作前后预留的“回调位”:只要对应事件发生,Git 就会去一个固定目录找指定名字的文件,找到就执行,找不到就跳过。
这个机制最大的价值是拦截。你可以让一些检查发生在不可逆操作之前。比如 commit 之前跑一遍代码规范检查,不通过就不允许提交;push 之前跑一遍关键单测,挂了就不允许推上去。这些都是靠 hook 脚本的退出码实现的:脚本以状态码 0 退出,表示放行;非 0 退出,表示拦截,Git 会中止当前操作。
为什么这比“团队自觉”靠谱?因为它是 Git 本身的行为,跑不跑不由人的记忆力决定。只要 hooks 装好了,路径都对,执行权限有,那么每次操作都会自动触发。你不需要每提交一次就提醒自己“哦对,先跑下 lint”,Git 帮你把这个“提醒”变成了强制动作。
1.2 客户端 hooks 与服务端 hooks 的分工
Hooks 按部署位置分成两类,理解这个区别很重要,因为它们的用途、可控范围和生效方式完全不同。
第一类是客户端 hooks,放在每个开发者的本地仓库.git/hooks/目录下,围绕 commit、merge、push 等个人操作展开。这类 hooks 的优点是反馈极快,在代码还没离开本机之前就把问题拦下来;缺点是每个开发者都可以自行修改,甚至可以通过--no-verify跳过,所以它更像“个人第一道防线”。
第二类是服务端 hooks,部署在远端仓库上,围绕 receive、update 等接收操作展开。服务端 hooks 不受开发者控制,普通开发者连日志都看不到,所以它才是真正不可绕过的“制度性防线”。我在第 5 部分会专门讲这块的落地用法。
两者配合的正确姿势是:客户端 hooks 做快速反馈,把低级问题消灭在本地;服务端 hooks 做强制兜底,不管客户端有没有守规矩,统一在远端再验一次。下面这张表列出我实践中用得最多的几个 hooks 和触发时机,便于你对照理解。
| Hook 类型 | 名称 | 触发时机 | 典型用途 |
|---|---|---|---|
| 客户端 | pre-commit | 输入提交信息之前 | 对暂存区做 lint、格式检查、密钥扫描 |
| 客户端 | prepare-commit-msg | 提交信息编辑器打开前、默认信息生成后 | 自动填充默认提交信息、添加 ChangeId 等 |
| 客户端 | commit-msg | 提交信息编辑完成后 | 校验提交信息格式 |
| 客户端 | post-commit | commit 完成后 | 发通知、统计提交数、触发本地构建缓存 |
| 客户端 | pre-push | push 操作执行前 | 跑单测、做冒烟验证、检查分支名 |
| 客户端 | post-merge | 合并操作完成后 | 检测依赖文件变更并自动安装依赖 |
| 客户端 | post-checkout | 切换分支完成后 | 根据分支切换环境配置、安装依赖 |
| 服务端 | pre-receive | 远端收到所有 refs 更新前 | 一次性校验所有推送的提交,可统一做合规审查 |
| 服务端 | update | 远端每个 ref 更新前 | 按分支做权限控制、分支名校验 |
| 服务端 | post-receive | 远端更新完成后 | 触发 CI/CD、部署、发通知 |
另外还有一个容易忽视的细节:客户端 hooks 并不是通过“提交”进入仓库的。.git/这个目录不会参与版本控制,你 push 到远端时,hook 文件并不会跟着走。所以团队要想统一 hooks,必须专门设计分发机制,这个我放到 2.3 部分细说。
2. 让第一个 Hook 跑起来:命名、权限和通信约定
2.1 hook 脚本的书写位置与命名规则
打开任意一个本地仓库,执行ls .git/hooks,你会看到一堆以.sample结尾的文件,比如pre-commit.sample、commit-msg.sample。这些是 Git 自带的样例,默认不会执行。想启用某个 hook,有两个动作缺一不可:一是把文件后缀.sample去掉,改成 Git 规定的标准文件名;二是给文件加上可执行权限。
命名必须精确匹配,绝对不能写错。比如pre-commit和component一字之差都不行,pre-commit写成precomit也不会有任何反应。还要注意文件名没有固定扩展名概念,Git 不认扩展名,只认文件名本身。
权限检查也常被忽略。很多人把脚本内容写对了,文件名也写对了,结果怎么都不触发,最后发现是chmod +x没做。在 Linux 或 macOS 上,执行ls -l .git/hooks/pre-commit,输出里必须有-rwxr-xr-x这种至少带一个x的权限位才表示可执行。Windows 环境下的挂载文件系统对可执行位的处理比较特殊,这也是 Windows 用户经常觉得 hook 不生效的原因之一。
写 hook 脚本不限定语言。最简单的写法是用 shell(#!/bin/sh),复杂逻辑可以用 Node、Python、Ruby,只要第一行的 shebang 指向对应解释器。注意 shebang 里写死绝对路径会带来可移植性问题,比如#!/usr/bin/python在别人机器上未必存在,建议优先用#!/usr/bin/env node、#!/usr/bin/env python3这类写法,让系统从环境变量里去解析解释器。
2.2 退出码、参数与标准输入:Hook 和 Git 的通信协议
Hook 脚本和 Git 之间只有三个通信通道:退出码、命令行参数、标准输入。理解这三个通道,你就能看懂任何 hook 示例代码。
退出码是放行和拦截的唯一依据。脚本最后一条命令的退出码会被 Git 拿到,0 表示继续,非 0 表示中止。所以写 if 判断时特别容易犯一个错:判断逻辑本身是对的,但忘了明确exit 1,导致脚本最终结束在一条成功命令上,退出码变成 0,检查形同虚设。我在初学阶段就被这个坑坑过,脚本里明明打印了“校验失败”,可 commit 还是成功了,因为脚本最后没有返回非 0 状态。
参数和标准输入则决定了 hook 能“看到”什么信息。不同的 hook,传参方式完全不同。比如commit-msg会收到一个参数,是本次提交信息临时文件的路径,你需要用cat "$1"去读取内容再做校验。pre-push会收到两个参数:远端名称和远端 URL,同时 Git 会通过标准输入向脚本传入一组待推送引用信息,每一行格式是<本地引用> <本地对象ID> <远端引用> <远端对象ID>,用while read循环去取即可。这些细节不记清楚,你写出来的 hook 可能连正确的数据都拿不到。
还有一个环境变量值得单独强调:GIT_DIR。Git 在执行 hook 时会把这个变量设置成.git目录的路径。如果你的脚本里需要调用 git 命令,并且依赖当前仓库上下文,最好留意这个变量的含义;在某些仓库场景下,必须对GIT_DIR做显式处理才能让嵌套的 git 命令工作正常,否则会出现“明明在仓库里,git 却找不到仓库”的怪异现象。
2.3 把 Hook 变成团队共享资源:三条路怎么选
因为.git/hooks不随仓库分发,想让全团队用同一套 hooks,就必须额外设计分发机制。我见过新手直接写个文档让大家“手动复制”,结果文档吃灰,hooks 只有自己用。要想正经落地,有以下三条路。
第一条路是设置core.hooksPath,把 hooks 目录指向仓库内的.githooks目录。执行git config core.hooksPath .githooks后,Git 会忽略默认的.git/hooks,改为读.githooks下的同名文件。好处是.githooks可以正常纳入版本控制,push 给同事;坏处是每个开发者 clone 下来后,都要手动执行一次git config。所以通常要配合安装脚本,在项目初始化时统一执行。
第二条路是使用 Git 模板目录。git init --template=模板目录会把这些模板文件复制进新仓库的.git/hooks里。这适合“每次新建仓库都要带默认规范”的场景,但对已有仓库没有作用。
第三条路是直接引入现成的 hooks 管理工具。Node 生态里常用的有 husky,配合 lint-staged 可以做到只对暂存区文件做检查;Python 生态里有 pre-commit 框架,用.pre-commit-config.yaml声明式配置。这类工具最大的优势是安装即生效,hooked 目录由工具自动处理,开发者几乎不需要手动干预。我团队里的新项目,默认就是 husky + lint-staged。工具的选择没有绝对标准,核心是降低团队成员的“使用成本”,真实环境里一条手动命令都嫌多,一键安装才是能长期跑的方案。
3. 四个高频场景,从脚本到完整落地
3.1 pre-commit:把烂代码挡在提交门口
pre-commit是提交动作的第一个闸门,触发于输入提交信息之前。它最适合做的事情,是对暂存区文件做快速检查:代码规范、格式、是否把密钥提交进来、有没有意外加入的调试文件。
一个必须注意的细节是,这个阶段能看到的应该是“暂存区里的内容”,而不是工作区。我在项目里写过一个经典的脚本:只对暂存区里改动过的 JS/TS/Vue 文件跑 ESLint,严格区分git diff --cached和git diff。如果把整个项目都 lint 一遍,项目一大,光耗时就不可接受;如果对工作区直接检查,可能会把还没打算提交的文件错误地变成拦截理由。
下面是一个 Node 项目 pre-commit 的参考脚本。它先取出暂存区中有变化的源码文件,再交给 ESLint 检查。检查不通过就中断提交。
#!/bin/sh # 取出暂存区中新增、修改、复制但没有删除的源码文件 STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|vue)$') if [ -n "$STAGED_FILES" ]; then echo "Running ESLint on staged files..." npx eslint $STAGED_FILES if [ $? -ne 0 ]; then echo "" echo "ESLint failed. Fix the errors, re-add the files, then commit again." exit 1 fi fi exit 0这里有个新手最容易踩的大坑:如果脚本里顺手帮你执行了prettier --write,文件内容被格式化了,但暂存区里还是旧版本,直接 commit 的话,提交进去的仍然是未格式化的内容。正确做法是先改文件,再执行git add把它们重新加入暂存区,然后脚本才放行。这也是 lint-staged 这类工具存在的原因——它内部帮你处理了“修改后重新暂存”这个环节。你要是自己写 hook,务必记得在脚本末尾对改动过的文件重新git add。
3.2 commit-msg:提交信息格式的强制校验
提交信息的价值在单人项目里还体现不出来,一旦进入团队协作,尤其是需要自动生成 changelog 的场景,格式混乱的提交信息就是灾难。
我在这里强烈建议采用 Conventional Commits 规范,格式是type(scope): subject。比如feat(user-center): add login page、fix(oss): fix image upload timeout。commit-msghook 就是保证这种格式从源头不合规就进不了仓库的阀门。
这个 hook 收到的是提交信息临时文件的路径,不是内容本身,所以脚本里第一件事是先读文件。处理时还要过滤注释行,因为 Git 默认编辑器模板里会有一堆以#开头的说明文字,它们并不是真正的提交信息。
#!/bin/sh COMMIT_MSG_FILE="$1" MSG=$(grep -v '^#' "$COMMIT_MSG_FILE" | sed '/^$/d' | head -n 1) if ! echo "$MSG" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|chore|build|ci)(\([a-z0-9-]+\))?: .{1,72}$'; then echo "Commit message format is invalid." echo "Expected pattern: type(scope): subject" echo "Examples:" echo " feat(user-center): add login page" echo " fix(oss): fix image upload timeout" exit 1 fi exit 0正则里的[a-z0-9-]+用来约束 scope 只能是小写字母、数字和连字符,.{1,72}用来限制 subject 长度在 72 个字符以内。太长的主旨说明要么拆行补充正文,要么压缩表达。这个长度约定不是拍脑袋定的,很多开源社区都遵循“标题不超过 72 字符”的经验值,因为在终端和邮件客户端里,超长标题会被折行,影响阅读。
如果团队对提交信息有更复杂的校验规则,比如强制填关联单号,可以在这个基础上再叠加正则匹配。校验失败时打印的提示信息一定要给足示例,别只丢一句“格式错误”,不然队友根本不知道该怎么改。
3.3 pre-push:推送前把关键测试跑一遍
pre-push是本地防线里离远端最近的一环,触发于推送之前。最好用的一个场景:把耗时可控的单测或冒烟用例在这里跑一遍,挂了就不让推。
为什么需要这一层?因为现实中的团队协作里,很多时候本地开发环境和 CI 配置并不一致,或者 CI 资源紧张,跑一次队列要等很久。在 push 前手动跑一遍关键测试,能大幅减少“推到 CI 才发现挂了”的低效循环。
写这个 hook 时要把握好“检查的粒度”。如果你把所有测试都塞进去,一套集成测试跑二十分钟,每次 push 都会严重拖慢节奏,开发者会本能地想跳过它。我个人的取舍标准是:pre-push里只跑“快速且稳定”的用例,比如单元测试、数据库无关的纯逻辑测试;而耗费资源的端到端测试留给 CI 去做。
#!/bin/sh echo "Running unit tests before push..." npm test if [ $? -ne 0 ]; then echo "Unit tests failed, push aborted." exit 1 fi exit 0注意脚本里的npm test要等它完整跑完并拿到退出码,所以不要用npm test &这种异步写法。还有一点,在 pre-push 阶段,远端的信息已经传进来了,除了跑测试,你完全可以在脚本里判断$1(远端名)和$2(远端 URL),实现“不同分支、不同远端走不同检查策略”。比如某个分支只允许推送到配对的远端,防止误推到不该推的地方。
3.4 post-merge / post-checkout:代码拉下来之后自动装依赖
团队协作中另一个烦人的问题:别人改动了依赖文件,你git pull下来项目直接跑不起来,提示一堆缺少的依赖。人有惯性,不是每个人都记得 pull 完看一眼依赖有没有变。
post-merge正好能在 merge 完成后自动执行。常见做法是判断这次拉取有没有涉及依赖锁文件的变化,如果有,就自动执行安装命令。使用场景最典型的就是 lock 文件:package-lock.json、yarn.lock、pnpm-lock.yaml 或 requirements.txt。
#!/bin/sh CHANGED=$(git diff ORIG_HEAD HEAD --name-only | grep -E '(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|requirements\.txt)$') if [ -n "$CHANGED" ]; then echo "Dependency files changed, running install..." npm install fi exit 0post-merge里的比对对象比较讲究。我用的是ORIG_HEAD,它指向合并前的位置。如果这次 merge 确实带来了依赖文件变化,git diff ORIG_HEAD HEAD --name-only就能把差异列出来。如果直接在 hook 里写死“每次 pull 都执行 install”,项目大了以后,没变依赖也会白白浪费几分钟,体验并不好。
post-checkout的用途也类似,只是它触发于分支切换之后。常见的做法是,在 monorepo 或不同分支环境差异大的项目里,切换分支后自动判断当前分支所需的依赖,避免在 A 分支装的依赖被带到 B 分支使用。这类自动化脚本看似很小,但实际省下来的是每个团队成员每天都在重复的隐性成本。
4. 排错实录:Hook 不生效、挂起、被绕过怎么办
4.1 hook 完全没反应?按这个清单逐条查
我在项目里帮同事排查过太多次“我写了 hook 为什么没反应”,总结下来 99% 都能命中下面这几条,按顺序查,基本能定位。
先看文件名。是不是真的叫pre-commit,有没有多打空格、少打字母。再执行ls -l看权限位,没有x就chmod +x。接着检查 shebang:脚本第一行如果写错解释器路径,系统可能直接返回一个错误而不是执行你的逻辑。然后确认仓库的core.hooksPath:执行git config core.hooksPath,如果输出不是空,Git 就不会再看默认的.git/hooks,你的脚本容易掉进“没挂在正确目录”的坑。最后在脚本第一行临时加一句set -x,手动跑一遍,确认执行过程是否符合预期。
还有一个很隐蔽的问题:脚本里用了 Windows 的换行符\r。shell 拿到带\r的 shebang 会报错,报错信息还不明显,因为它往往提示的是解释器找不到。跨平台协作时,hook 脚本建议统一保存为 LF 行尾,提交到仓库后一般不会出问题,但如果你在 Windows 上直接新建文件,默认 CRLF 行尾会埋下隐患。
4.2 脚本挂起或卡住:多半是交互命令惹的祸
hook 卡住的现象一般发生在图形化 Git 客户端里:提交卡住,或者 hook 一直没反应。最常见的元凶是脚本里有交互式命令,比如read -p "确认继续?",或者脚本调了npm install然后等输入确认。图形界面里往往没有能接收 stdin 的交互窗口,命令就一直挂在那里。
我踩过的坑是:pre-commit 里执行了一个带交互提示的格式化工具,CI 之外本地跑的时候总要按一次回车,后来在某个同事的 GUI 客户端上,commit 按钮点下去直接卡死。从那以后我给自己立了规矩:hook 脚本里禁止出现交互式输入,所有命令都要加非交互参数。常用的做法是设置环境变量CI=1或者在命令层面指定--no-input这类开关,让程序自动走默认值。
4.3 调试 Hooks 的实用手段
最笨但也最有效的方法:切到仓库目录,直接手动执行 hook 脚本。sh .git/hooks/pre-commit,或者直接跑./.git/hooks/pre-commit,这样能看到脚本真实输出,也能直接看到退出码。注意手动跑的时候,要还原和自动触发时相同的工作目录,不然某些依赖相对路径的逻辑会出错。
想看 Git 到底“调没调”你的 hook,可以用GIT_TRACE=1 git commit ...。开启追踪后,Git 会在调用 hook 的位置打出日志,输出类似trace: built-in: git hook pre-commit的信息,能很直观地确认触发链路是否正常。
如果脚本逻辑复杂,我推荐在脚本开头加set -ex。-e让脚本在任意一条命令出错时立刻退出,-x会把每条命令展开后的样子打出来。排错完成后记得把-e去掉,因为你的脚本需要自己控制退出码,而不是让中间某条无害命令的非 0 状态把整个 hook 弄挂。
4.4 --no-verify 跳过的边界:客户端能绕,服务端绕不了
Git 给客户端 hooks 留了后门:git commit --no-verify会跳过所有 pre-commit 和 commit-msg 检查,git push --no-verify会跳过 pre-push。这既是逃生通道,也是安全隐患。
一定要清楚,客户端 hooks 是“辅助防线”,不是“安全闸门”。因为脚本本身在开发者自己的机器上,开发者完全可以修改它,甚至直接用别的 Git 客户端绕过去。真正不可绕过的,只有服务端 hooks。所以对任何“必须拦住”的规则,比如禁止把某个分支 push 到生产环境、禁止包含某些敏感信息的代码进入主干,建议一定在服务端钩子里再设一层,别把希望全压在本地的自觉上。
团队规范里,我对--no-verify的态度是:允许紧急逃生,但留下审计痕迹。比如约定使用--no-verify的提交,必须在 commit 信息里注明原因。与其一刀切禁止,不如降低滥用它的收益,同时增加滥用的成本。
4.5 本地 hooks 与 CI 的分工
很多朋友问,既然CI 都能检查,为什么还要本地 hooks?答案是反馈速度和覆盖率不同。本地 hooks 的优势是毫秒级反馈,commit 时就能发现问题,不用等 push 到远端、排队、跑完再回来看报告。CI 的优势是环境干净、配置统一、服务端不可绕过。
正确的分工是:本地 hooks 做“快而窄”的检查,比如语法、格式、提交规范;CI 做“慢而全”的验证,比如全量测试、依赖审计、多环境构建、集成测试。如果本地也想把全套测试跑完,建议做成可选命令,而不是默认阻塞项。否则开发者对 hook 的体验是“每次提交都要等很久”,最终一定会出现大量手动禁用的现象,那就完全违背了引入 hooks 的初衷。
5. 落地到团队:从“个人工具”升级为“团队规范闸门”
5.1 服务端 hook:真正不可绕过的最后防线
客户端 hooks 可以被任何人改、被任何人跳过,这一点决定了它扛不起“强制规范”的责任。真正能扛强制的是服务端 hooks。比如pre-receive在远端接收所有引用更新之前触发,管理员可以在脚本里拒绝不符合规范的提交,拒绝信息会直接返回到推送者的终端。
我参与的一个基建项目里,服务端pre-receive脚本承担了两件事:一是限制不能直接 push 到主干分支,必须走合并请求流程;二是扫描所有新增提交信息,若有不符合规范格式的提交,直接拒绝整个推送。那次落地之后,之前“本地 hook 被绕过、乱七八糟的信息进入主干”的情况彻底消失了,因为服务端不看你的本地状态,只看实际推过来的内容。
服务端 hooks 的部署方式依赖你用的托管平台。自建仓库可以做中央仓库的 hooks 配置;托管平台则通常提供在管理后台配置服务端 hooks 的能力,具体入口要看平台文档。无论如何,只有放在远端仓库服务端的检查,才是定海神针。
5.2 用脚手架工具统一安装 hooks
既然客户端 hooks 无法随仓库传递,团队推行时必须有一套“零心智负担”的安装方案。我的建议是:不要把安装步骤写在 README 里指望同事自己看,而是写进项目初始化命令或启动脚本里。比如 Node 项目里,初始化阶段统一执行一次安装,把 husky 和 lint-staged 配好;Python 项目里,可以直接在构建流程里让 pre-commit 框架读取一份共享配置文件。
这类工具还有一个重要优势:它会自动处理 hooks 的升级和卸载。如果你手动写 hooks 脚本,每次要改规则就只能逐个仓库手动更新,时间一长必然有人没同步。工具化之后,规则只在配置文件里维护,团队统一 pull 一次配置就全部生效。
选型时我建议从团队主流技术栈出发。前端/Native 团队优先 husky + lint-staged,配置简单,和 npm 生命周期结合好;后端多语言团队优先 pre-commit 框架,它支持 Python、Shell、Go 等多种语言钩子,配置中心化。工具只是载体,真正重要的是把“强制规范”的成本降到最低,否则规范只会停留在纸上。
5.3 写 hooks 的安全底线
最后说几条安全底线,都是我在代码审查和运维事故里积累出来的教训。
第一,hook 脚本不要做不可逆的危险操作。它的工作是“检查”和“轻量加工”,不是“重构代码”或“批量替换文件”。一旦脚本逻辑有 bug,影响力会被放大到所有开发者机器上。
第二,hook 脚本必须幂等。同一次触发如果因为异常重复执行,应该得到相同结果。我见过一个 post-checkout 脚本里用了带自增逻辑的临时文件,导致分支切来切去时状态错乱,排查了很久才定位到是 hook 的问题。
第三,hook 脚本的输出要克制。每次 commit 时刷出一屏无关紧要的日志,开发者会逐渐习惯性忽略输出,反而把真正的错误提示淹没了。好的 hook 只在有问题的时候大声说话,没问题的安静放行。
第四,不要在你的 hook 脚本里硬编码任何“一次性的本地路径”或“个人账号信息”。因为这类脚本一定会被复制到别的开发者机器上,硬编码的个人配置会直接在所有人环境里炸开。
写到最后想分享的一点个人体会
如果你问我这套东西值不值得花时间搞,我的回答是:非常值,但别一口吃成胖子。我见过太多团队上来就规划十几条规范,结果过度设计,光装 hooks 就劝退了半个组。自己项目先从一个commit-msg校验或者一个pre-commitlint 开始跑,跑顺了再逐步加pre-push测试、post-merge依赖安装。hooks 的价值在于日复一日地帮你挡住小问题,时间拉长,仓库历史会越来越干净,review 代码时的噪音会少很多。
最后再分享一个小技巧:如果你的 hook 脚本逻辑比较复杂,别直接在.git/hooks里写一大坨,把真正做逻辑的脚本放到项目目录下(比如scripts/里),hook 本身只做两行调用。这样逻辑可以被项目内的单测覆盖,也可以在需要时手动运行,比把一切藏在.git深处容易维护得多。这个设计我一直沿用到现在,每次看到别人在 hooks 目录里堆几百行 shell 脚本时,我都想劝他们早点改过来。