bigywater.tech
Git · 47

rename 检测:相似度阈值实测

git 不记录“改名”,那 git status 里的 renamed 是怎么来的?相似度阈值默认是多少、实际分数怎么随改动量变化、什么时候会检测失败?这篇逐项实测,再看 git log --follow、diff.renameLimit 和合并时的影响。

50%
默认的改名相似度阈值;改名同时改了一半的行,分数 46% 就被拆成 D+A
R100 … R036
改名同时改动 0%~60% 的行得到的相似度分数,每 10% 约降 11 分
1000
diff.renameLimit 默认值,1200 个改名超过上限时一个都检测不出来

改名是推断出来的

实测 · 本机执行输出
  $ 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 也追不到,历史在那里断开。

renameLimit

实测 · 本机执行输出
  $ 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 冲突。把“改名”和“大改”分成两个提交可以避免这个问题。

分步演示

改名检测
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。测试文件是固定种子生成的随机文本(每个文件 100 行,每行 30 个随机字母),“改动 N% 的行”是把对应的行整行替换成新的随机内容;分数是 git diff --name-status 里的 R<分数>;耗时一次测量(含进程启动)
    • 没有验证:-C / -C -C 的复制检测;merge.renames 与 merge.directoryRenames(目录改名);二进制文件、很短的文件(几行)的分数行为;分数的精确算法(只从行为推断了大致关系);git blame -M / -C(见后面 blame 一篇)
    京ICP备2026067502号-1京公网安备11010502063487号