
脚本只想问「工作区有没有改动」,用 git diff --quiet 正合适。麻烦在于,它把「有差异」也用非零状态表示;若整行命令直接放进 set -e 脚本,正常改动可能被误当成程序故障。判断前要先定清楚:调用方需要的是布尔结果,还是还要把无法比较、权限异常一并报告出来。
--quiet 到底把什么交给调用方?
它不输出补丁,只通过退出码报告比较结果。工作区与索引之间没有差异时返回 0;发现差异时返回 1。这里的 1 是一种结果,不是 git 执行失败的通用标志。文档对 --quiet 的定义也提到,它会关闭程序输出,并隐含 --exit-code 的行为。
实测在一个新建的临时仓库完成,没有碰博客仓库或其他项目。脚本先提交 sample.txt,检查干净状态,再改写文件;另外在仓库外的临时子目录运行一次,用来观察真正的错误状态。完整脚本在 tools/verify-git-diff-quiet-status.py,运行命令是 python3 tools/verify-git-diff-quiet-status.py。
clean: exit=0, stdout='', stderr=''
changed: exit=1, stdout='', stderr=''
not-a-repository: exit=129, stdout='', stderr='warning: Not a git repository. Use --no-index to compare two paths outside a working tree'
结果把边界摆得很清楚:干净时 0,有改动时 1;仓库外运行则是 129,并且 stderr 有警告。错误状态可能随 Git 版本、具体错误而不同,不应把 129 当成固定错误码;关键在于,不要把任意非零都解释成「存在差异」。
为什么 if git diff --quiet 不会自动报错?
shell 的 if 会把命令状态交给分支判断,非零进入 else。这刚好适合区分两种状态,但要记得再看一次 $?,否则仓库错误也会落入「有差异」的分支。可以这样写:
if git diff --quiet; then
printf '%s\n' '工作区干净'
else
status=$?
if [ "$status" -eq 1 ]; then
printf '%s\n' '工作区有差异'
else
printf 'git diff 出错,退出码 %s\n' "$status" >&2
exit "$status"
fi
fi
status=$? 必须是 else 里的第一条命令。紧跟在失败命令后面保存,后续任何 printf、赋值或其他命令都可能覆盖状态。这样即使脚本启用了 set -e,处于 if 条件位置的命令也不会仅因状态 1 就触发立即退出;状态分支仍然把其他非零作为错误交还调用方。
默认比较范围是不是整个仓库?
不是。普通 git diff --quiet 比较工作区与索引,也就是检查尚未暂存的改动。已经 git add 的改动不属于这个比较范围;若要检查暂存区相对最近一次提交的变化,使用 git diff --cached --quiet。两者问的不是同一个问题,合并成一条检查前,得先明确脚本要挡住哪一类变化。
忽略规则、未跟踪文件也容易造成误会。git diff 默认不会把未跟踪文件算作差异;只有被 Git 跟踪的文件发生相对索引变化,才会进入这个检查。新建文件如果还没 git add,普通 diff 安静地返回 0 并不代表目录完全干净。若目标是「目录里有没有任何新文件或改动」,只看 git diff --quiet 不够,还要结合 git status --porcelain 等专门检查。这里不建议把输出重定向或错误抑制后,靠一个非零值猜发生了什么。
检查命令的返回值时,哪条边界不能省?
shell 里的 $? 只保留最近一条命令的结果。假如 git diff --quiet 失败后,先打印一行提示,再赋值给 status,保存到的就会是 printf 的状态,原来的 Git 状态已经丢了。把赋值放在 else 第一行,是避免错误分类的必要条件。若把这段逻辑包进函数,函数本身也应按约定返回错误码,不要只把提示写进日志。
一个实用的判定边界是:只关心跟踪文件的未暂存改动时,用 git diff --quiet,并区分状态 1 与其他错误;关心暂存内容时加 --cached;未跟踪文件也算时,另做状态检查。我不建议把所有非零都当成差异,因为仓库路径写错时,自动化反而会把真正的问题伪装成一次正常变更。