bigywater.tech
Git · 10

改一个字,后面所有 commit 的 hash 为什么全变

为什么改一个字,后面所有 commit 的 hash 都变了?这篇用 filter-branch 在一条 5 个提交的链上改说明和文件内容,逐个对比 hash 和 tree,再读新提交的原文验证 parent 行,最后看改最新的提交只影响一个。

4 / 5
改提交 2 的一个字,hash 变化的提交数是 4(提交 2 到 5),提交 1 不变
tree 不变
只改说明时五个 tree 的 hash 一个都没变,变的只有 commit 链
1 个
改最新的提交,受影响的只有它自己

实验设置

实测 · 本机执行输出
  $ git log --format='%h %s  parent=%p tree=%t'
  bd38bca 提交 5  parent=a36631e tree=bddd827
  a36631e 提交 4  parent=e8b2f85 tree=471da36
  e8b2f85 提交 3  parent=9d8ba3d tree=239ba4d
  9d8ba3d 提交 2  parent=5c02abe tree=b08dad9
  5c02abe 提交 1  parent= tree=91c7d88

一条 5 个提交的线性历史,每个提交往 f.txt 里多加一行。作者、时间固定,所以每次重做 hash 都一样,便于对照改写前后的变化。

改一个字的后果

实测 · 本机执行输出
  提交 1: 5c02abefe0 → 5c02abefe0  没变
  提交 2: 9d8ba3d409 → ecc1ba807a  变了
  提交 3: e8b2f85cc0 → 9c72ac6a02  变了
  提交 4: a36631e5d9 → 6e920838fd  变了
  提交 5: bd38bca693 → dd5eaf25f5  变了
  新历史:
  $ git log --format='%h %s  parent=%p'
  dd5eaf2 提交 5  parent=6e92083
  6e92083 提交 4  parent=9c72ac6
  9c72ac6 提交 3  parent=ecc1ba8
  ecc1ba8 提交 二  parent=5c02abe
  5c02abe 提交 1  parent=

把第 2 个提交的说明改一个字:它之前的提交 hash 不变,它自己和它之后的全部提交 hash 都变,即使后面提交的内容和说明一个字没动。

传播的原因

实测 · 本机执行输出
  新的第 3 个提交的原文:
  | tree 239ba4df1ae532d670a6f52531211979886f3ee8
  | parent ecc1ba807a23e4cc14c870d8163c55d0830d46b9
  | author Alice <alice@example.com> 1767261780 +0000
  | committer Alice <alice@example.com> 1767261780 +0000
  | 
  | 提交 3
  | 
  它的 parent 就是新的第 2 个提交: True
  tree 呢?只改说明,tree 没变:
  $ for c in HEAD~4 HEAD~3 HEAD~2 HEAD~1 HEAD; do git rev-parse --short $c^{tree}; done | paste -sd' ' -
  91c7d88 b08dad9 239ba4d 471da36 bddd827

commit 的 hash 是对整个文本算的,文本里有 parent 行。改了前面的提交,它的 hash 变了,后一个提交的 parent 行就必须改成新的 hash,于是后一个提交的 hash 也变,一路传到分支末端。这就是“hash 是历史的指纹”:一个 commit 的 hash 确定了它之前的全部历史,别人拿到旧 hash 就能发现历史被改写过。

tree 与 commit 两层

实测 · 本机执行输出
  提交 1: commit 5c02abe → 5c02abe   tree 91c7d88 → 91c7d88
  提交 2: commit 9d8ba3d → 40f3c8a   tree b08dad9 → 49913bf
  提交 3: commit e8b2f85 → 0e47b42   tree 239ba4d → 239ba4d
  提交 4: commit a36631e → d53fb70   tree 471da36 → 471da36
  提交 5: commit bd38bca → dfb2267   tree bddd827 → bddd827

只改说明时,所有 tree 都不变;改文件内容时只有包含这个改动的 tree 变,后面没碰这个文件的提交 tree 不变(本例中提交 3~5 的 tree 与原来相同)。但 commit 层始终是整条链往后传。

改写的代价

实测 · 本机执行输出
  提交 1: 5c02abefe0 没变
  提交 2: 9d8ba3d409 没变
  提交 3: e8b2f85cc0 没变
  提交 4: a36631e5d9 没变
  提交 5: bd38bca693 → fb51fd1b68
  改的位置越靠后,受影响的提交越少:改第 k 个,受影响的是第 k 到第 5 个,共 6-k 个

改的位置越早,要重写的提交越多;已经推送出去的历史改写之后,别人本地的分支基于旧 hash,需要强制推送和重新同步。改最新的提交最便宜。

分步演示

hash 的连锁变化
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。改写用的是 git filter-branch(命令里的警告被截掉了,不影响结果);作者、提交者与时间固定,所以改写前后的 hash 都可复现
    • 没有验证:用 rebase 或 git-filter-repo 改写的 hash 变化(原理相同,没有逐个验证);签名提交改写后签名失效的情形;改写后旧对象何时被回收(见 prune 一篇);有合并提交的非线性历史上的传播范围
    京ICP备2026067502号-1京公网安备11010502063487号