为什么改一个字,后面所有 commit 的 hash 都变了?这篇用 filter-branch 在一条 5 个提交的链上改说明和文件内容,逐个对比 hash 和 tree,再读新提交的原文验证 parent 行,最后看改最新的提交只影响一个。
$ 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 bddd827commit 的 hash 是对整个文本算的,文本里有 parent 行。改了前面的提交,它的 hash 变了,后一个提交的 parent 行就必须改成新的 hash,于是后一个提交的 hash 也变,一路传到分支末端。这就是“hash 是历史的指纹”:一个 commit 的 hash 确定了它之前的全部历史,别人拿到旧 hash 就能发现历史被改写过。
提交 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,需要强制推送和重新同步。改最新的提交最便宜。