git status 在大仓库里为什么能这么快?答案是 index 里的 stat 缓存:只在文件的 stat 变了的时候才去读内容。这篇解析 index 条目、验证 touch 与原地改写时 status 的判断,再测 10,000 个文件时缓存有效与失效的差别,以及 cp -a 之后为什么第一次 status 特别慢。
index 版本 2,3 个条目,扩展块: [('TREE', 82)]
路径 size mtime(秒.纳秒) ino mode 与 stat 一致?
a.txt 6 1791728834.904695532 44030464 100644 True
src/b.txt 7 1791728834.904872532 44030466 100644 True
src/deep/c.txt 2 1791728834.904997991 44030468 100644 True每个 index 条目除了路径、blob hash、flags,还存着该文件当时的 stat:ctime、mtime(秒与纳秒)、dev、ino、权限、uid、gid、大小。解析 .git/index 后逐字段与 os.stat 对比完全一致。
只 touch(内容不变,mtime 变了):status 无输出,但要重新读文件算 hash,再把新的 stat 写回 index $ git status --short; echo "(status 为空:内容没变)" (status 为空:内容没变) a.txt 在 index 里的 mtime: 1791728834.904695532 → 1791728836.059909000(git status 顺手刷新了 index)
git status 对每个文件先比较 stat:全部一致就认定没改,不读内容;只要有字段不同,才去读内容算 hash。如果内容其实没变(比如只是 touch),git 会把新的 stat 写回 index,下次又能走快速路径。
$ cat a.txt; git status --short; echo "(默认配置:原地改写会更新 ctime,git 发现 stat 不同,重新比较内容,报告 M)"
HELLO
M a.txt
(默认配置:原地改写会更新 ctime,git 发现 stat 不同,重新比较内容,报告 M)
$ git config core.trustctime false; git config core.checkStat minimal; cat a.txt; git status --short; echo "(trustctime=false + checkStat=minimal:只比较 mtime 和 size,没有任何输出,看不出改动)"; git diff --stat | wc -l
HELLO
(trustctime=false + checkStat=minimal:只比较 mtime 和 size,没有任何输出,看不出改动)
0
$ echo "工作区文件的 blob hash : $(git hash-object a.txt | cut -c1-12)"; echo "index 里记的 blob hash : $(git rev-parse :a.txt | cut -c1-12)"; echo "(两者不同:内容确实变了,只是 git status 的 stat 比较没发现)"
工作区文件的 blob hash : e427984d4a2c
index 里记的 blob hash : ce013625030b
(两者不同:内容确实变了,只是 git status 的 stat 比较没发现)
$ git config --unset core.trustctime; git config --unset core.checkStat; git status --short; echo "(恢复默认比较后,ctime 暴露了改动)"
M a.txt
(恢复默认比较后,ctime 暴露了改动)stat 比较是启发式的:内容改了但大小不变、mtime 又被恢复,只要 ctime 也不比较(core.trustctime=false 加 core.checkStat=minimal),git 就看不出来。默认配置下原地写文件会更新 ctime,把这个改动暴露出来。
stat 缓存有效:git status 29 ms 把 10,000 个文件的 mtime 全部改掉(内容不变)后的第一次 git status(要重读全部文件算 hash)204 ms 等过 1 秒、再跑一次让 index 刷新之后的 git status(原因见下一篇 racy git:刚被改过的文件会被认为“可能 racy”)29 ms
10,000 个小文件,缓存有效 29 ms;缓存全失效要重读所有文件,204 ms,大约 7 倍。文件越多、越大,差距越大。
cp -a 之后第一次 git status 734 ms,第二次 28 ms 同样 cp -a,设置 core.checkStat=minimal(只看 mtime 和 size)和 core.trustctime=false 之后第一次 git status 29 ms
cp -a 保留 mtime 但 inode 与 ctime 变了,index 里的缓存整体失效,第一次 git status 慢(734 ms),第二次恢复。把 stat 检查放宽到 mtime 和 size 可以避开,但放弃了 ctime 带来的保护。