git 不记录“改名”,那 git status 里的 renamed 是怎么来的?相似度阈值默认是多少、实际分数怎么随改动量变化、什么时候会检测失败?这篇逐项实测,再看 git log --follow、diff.renameLimit 和合并时的影响。
$ git cat-file -p HEAD^{tree}; git cat-file -p HEAD~1^{tree}
100644 blob c6f6b1d6e95fd577c7d8bf7134654ca0a93f3f29 new.txt
100644 blob c6f6b1d6e95fd577c7d8bf7134654ca0a93f3f29 old.txt
$ git diff --name-status HEAD~1 HEAD; git diff --name-status --no-renames HEAD~1 HEAD
R100 old.txt new.txt
A new.txt
D old.txt
(内容完全相同时 blob 的 hash 相同,所以 100% 的改名只要比 hash,不用比内容)commit 的 tree 只记录“文件名 → blob”。改名只在比较两个 tree 时被推断:先把删掉的文件和新增的文件按 blob hash 配对(100% 相同,极快),剩下的再按内容相似度配对。
默认(-M,50%):
D f05.txt
D f06.txt
D f07.txt
D f08.txt
D f09.txt
R100 f00.txt g00.txt
R088 f01.txt g01.txt
R077 f02.txt g02.txt
R066 f03.txt g03.txt
R056 f04.txt g04.txt
A g05.txt
A g06.txt
A g07.txt
A g08.txt
A g09.txt
改动行数 默认 -M(50%) -M30% -M70% -M90%
0% R100 R100 R100 R100
10% R088 R088 R088 D+A
20% R077 R077 R077 D+A
30% R066 R066 D+A D+A
40% R056 R056 D+A D+A
50% D+A R046 D+A D+A
60% D+A R036 D+A D+A
70% D+A D+A D+A D+A
80% D+A D+A D+A D+A
90% D+A D+A D+A D+A默认阈值 50%(-M 或 -M50%)。本例中改动 10% 的行,分数是 88%,每多改 10% 约降 11 分,改动 50% 时是 46%,低于阈值就被当成删除加新增。-M30% 放宽、-M90% 收紧。
$ git log --oneline -- manual.txt | sed -E "s/^[0-9a-f]+ //"; echo "--- --follow:"; git log --oneline --follow -- manual.txt | sed -E "s/^[0-9a-f]+ //" 改名并大改 --- --follow: 改名并大改 修改 创建 $ git diff --name-status HEAD~1 HEAD R093 doc.txt manual.txt $ git diff --name-status HEAD~1 HEAD; echo "--- --follow:"; git log --oneline --follow -- manual.txt | sed -E "s/^[0-9a-f]+ //" D doc.txt A manual.txt --- --follow: 改名并改了60%
git log -- 路径 不追改名,--follow 沿相似度向前追。如果改名时改动过大,推断失败,--follow 也追不到,历史在那里断开。
$ git diff --name-status HEAD~1 HEAD 2>&1 | cut -c1-1 | sort | uniq -c; git diff --name-status HEAD~1 HEAD 2>&1 >/dev/null | head -2
1200 A
1200 D
2 w
warning: exhaustive rename detection was skipped due to too many files.
warning: you may want to set your diff.renameLimit variable to at least 1200 and retry the command.
$ git diff -l2000 --name-status HEAD~1 HEAD 2>&1 | cut -c1-1 | sort | uniq -c
1200 R
$ git -c diff.renameLimit=2000 diff --name-status HEAD~1 HEAD | cut -c1-1 | sort | uniq -c
1200 R
-l2000 的检测耗时: 372 ms不精确检测要比较(删除 × 新增)的每一对,所以有上限 diff.renameLimit(默认 1000)。超过时 git 只认 100% 相同的改名并警告;调到 2000 之后 1200 个改名全部识别,用了约 372 ms。
改名时改动 改动 20%: Auto-merging manual.txt | CONFLICT (content): Merge conflict in manual.txt | Automatic merge failed; fix conflicts and then commit the result. 改名时改动 改动 70%: CONFLICT (modify/delete): doc.txt deleted in ren and modified in HEAD. Version HEAD of doc.txt left in tree. | Automatic merge failed; fix conflicts and then commit the result.
合并用同样的检测:另一边修改了旧名字时,识别出改名就把修改带到新名字上;识别不出就变成 modify/delete 冲突。把“改名”和“大改”分成两个提交可以避免这个问题。