Git 改名检测:同一份暂存变化,为什么调高阈值就变成删除和新增
评审页面显示某文件改名了,换一个命令却显示旧文件被删除、新文件被添加。两次查看的是同一份暂存内容,究竟哪一个错了?可以先排除一种直觉:Git 并没有在每次提交里保存一条独立的“移动文件”命令。
差异计算可以根据旧路径消失、新路径出现以及内容相似程度,将一对变化解释为改名。这种解释帮助人阅读补丁,也受选项和候选范围影响。我们用同一份暂存区做三次对照,不在中间修改文件,就能看出展示与存储的区别。
只创建自己的临时仓库
把代码存为 demo.sh,用 bash demo.sh 运行。本文验证环境为 Git 2.52.0 与 Bash 5.2.37。脚本自行建立并清理临时目录;作者身份只通过单条命令参数提供,不写用户全局配置,也不依赖现有仓库。
AI概念示意图:表现本文的抽象关系,并非软件截图或真实运行结果。
set -euo pipefail work=$(mktemp -d) trap 'rm -rf -- "$work"' EXIT cd "$work" git init -q for ((i=1; i<=20; i++)); do printf 'line-%02d stable content\n' "$i" done > old.txt git add old.txt git -c user.name=Demo -c user.email=demo@example.invalid \ -c commit.gpgsign=false commit -qm base mv old.txt new.txt printf 'one extra line\n' >> new.txt git add -A without=$(git diff --cached --no-renames --name-status) loose=$(git diff --cached --find-renames=50% --name-status) strict=$(git diff --cached --find-renames=100% --name-status) [[ "$without" == $'A\tnew.txt\nD\told.txt' ]] [[ "$loose" =~ ^R[0-9]+$'\t'old.txt$'\t'new.txt$ ]] [[ "$strict" == "$without" ]] printf 'disabled:\n%s\n50%%:\n%s\n100%%:\n%s\n' \ "$without" "$loose" "$strict" printf 'rename checks passed\n'
改变阈值,并没有改变暂存内容
实测关闭检测时出现 A new.txt 与 D old.txt,百分之五十阈值时显示 R096 old.txt new.txt,百分之百阈值又回到新增和删除。实际输出用制表符分隔。断言只要求中间结果属于改名,不把九十六这个具体评分当成跨版本必须不变的接口。
新增一行后,两份内容高度相似,但已经不是完全相同。百分之百阈值要求精确匹配,自然不会把这一对归为改名。实验连续读取同一个暂存区,因此结果变化只能归于差异选项,不能解释为文件又被偷偷改了一次。
这里使用 mv 而不是 git mv,仍然能得到改名展示。因为最终的比较对象是旧快照与暂存快照,平时用哪个命令移动文件并不能成为永久的路径身份记录。git mv 的便利主要在于连同暂存状态一起处理。
评分不是业务上的“相同文件”保证
相似度用于判断内容保留程度,不表示文件用途相同,也不保证程序行为没变。少量配置改动就可能产生重大影响,而大规模格式调整也可能不改变行为。阅读 R 后面的数字时,仍要展开补丁审查关键行。
如果大量文件同时删除和新增,候选配对会更复杂,也可能受到检测限制影响。一个项目设置下能显示改名,另一个环境不显示,并不能立即推断有人丢失历史。先记录实际命令、阈值与配置,再在相同端点重复比较。
也不要把检测阈值当成修复仓库内容的开关。调整它只会改变本次比较的解释,不能恢复误删内容。需要找回文件时,应回到明确的提交或备份;需要审查移动时,则可在暂存前后分别查看路径和实际内容。
提交重排文件的工作时,可以尽量将纯移动与大量内容改写拆成容易理解的步骤,帮助评审者追踪变化,但这只是协作安排。验收标准仍是最终文件是否正确,而不是某个平台必须画出一条改名箭头。
脚本使用 --cached 明确比较暂存区与当前提交。若漏掉它,普通 git diff 会检查工作区与暂存区,而本例工作区已全部暂存,可能什么都看不到。比较端点与检测阈值是两组独立设置,排查时需要同时记录。


