为什么 no-op filter-branch 会产生分歧,我该如何解决?

Why does a no-op filter-branch create divergence, and how do I fix that?

我有一种情况,我将几年的提交合并到一个存储库中。其中一个提交有一条评论,它是与修复相关的 Address Sanitizer 日志的粘贴。

这听起来还不错,只是地址 Sanitizer 日志看起来像这样:

==10856==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x62a00000b201 at pc 0x47df61 bp 0x7fffffff2ca0 sp 0x7fffffff2c98
READ of size 1 at 0x62a00000b201 thread T0
#0 0x47df60 in Expand_Series ../src/core/m-series.c:145
#1 0x47e5a7 in Extend_Series ../src/core/m-series.c:187
#2 0x466e0c in Scan_Quote ../src/core/l-scan.c:462
#3 0x46a797 in Scan_Token ../src/core/l-scan.c:918
#4 0x46e263 in Scan_Block ../src/core/l-scan.c:1188
...

在这种情况下,它上升到 #250 左右。 GitHub 扫描 #XXX 模式,如果它们与问题编号匹配,请在引用的问题上注明提及。所以突然 GitHub 认为这个提交是对每个问题和拉取请求的评论,并且会这样做一段时间。

我想我只需要使用 git filter-branch 因为我真的不介意打破历史记录 (我已经做了一个过滤器分支来摆脱一些东西我不想)。但是,在我进行合并并继续工作之前,我做了另一个过滤器分支。既然我已经注意到这个在 GitHub 中弹出,我想回去重写它并且不介意每个分支上的每个提交 之后得到一个新的哈希值。我没问题。

我开始重写了,但我不明白为什么会有这么多分歧。在我对评论进行任何更改之前,它似乎已经完成了影响事情的重写。作为一个简单的测试,我尝试了我认为应该是空操作的东西:

git filter-branch -f --msg-filter 'sed "s/a/a/g"' -- --all

我不是 sed 人,但我的理解是,这将重做所有提交消息并将 a 替换为 a(Ayn Rand 会很高兴。)

它并没有像我的实际替代者那么多的提交发散...600 而不是 1000。但是它完全发散表明我在这里有某种误解。我怎样才能重写 that commit message in the history 而不会损坏除了它之后发生的提交之外的任何提交...并在所有分支上产生影响?

如果现有消息 不是 以换行符结尾,sed 将添加一个(至少某些版本的 sed,包括我测试过的那个这里):

$ printf 'foo\nbar'
foo
bar$ printf 'foo\nbar' | sed 's/a/a/'
foo
bar
$ 

这意味着您的测试邮件过滤器可能更改了邮件。根据您的结果,我猜测至少有一个提交,从某些分支提示返回的大约 600 个提交,是以这种方式修改的。 (我以前亲眼见过这个问题。)

(另一种可能性是某种 Unicode 规范化,尽管我还没有看到 sed 会发生这种情况。)

假设是这种情况,您的诀窍就是找到一个不影响其他提交的命令。一个好的方法是使用环境变量 $GIT_COMMIT 来识别要触摸的提交,并确保你做的事情是真正的空操作(cat msg-filter 可能比sed,例如)所有其他提交:

... --msg-filter 'if [ $GIT_COMMIT == <the one> ]; then fix_msg; else cat; fi' ...

至于对所有分支产生影响,您的-- --all应该已经做到了。


听起来您已经知道为什么剩余的提交会获得新的 SHA-1,但为了完整起见,我也会将其包括在内。你可以跳过这部分,这是给其他人看问题的。

如果提交被修改,它会得到一个新的 SHA-1(根据定义,因为 SHA-1 是提交内容的校验和)。到目前为止没什么大不了的,但是假设只有五个提交(在这种情况下全部在 master 上,这无关紧要)我们将使用 filter-branch 过滤器修改中间的一个:

A <- B <- C <- D <- E        [original]

假设 C 的实际 SHA-1 以 30001 开头)。现在让我们在过滤器分支操作的中间构建一个部分结果:

A <- B <- C'

比方说,由于某种奇怪的巧合,新的 SHA-1 以 30002、提交 3 的版本 2 开头。

让我们看一下(部分)原始提交 D:

$ git cat-file -p HEAD^
tree 954019cba5244a4a135ff62258660b3d2e3a8087
parent 30001...

提交 D 按编号表示提交 C。所以 filter-branch,虽然它没有改变任何关于 Delse,但必须构建一个新的提交 D',它说 parent 30002...:

A <- B <- C' <- D'

同样,filter-branch 被迫将旧提交 E 复制到新 E':

A <- B <- C' <- D' <- E'     [replacement]

因此,任何更改某些提交的 filter-branch 也会更改所有后续提交。 (对于 git rebase 也是如此。事实上,git rebasegit filter-branch 是表兄弟。两者都只是读取现有提交,应用一些更改,并将结果写为新提交;filter-branch 以编程方式完成这一切——即没有 --interactive 模式——并且有一组非常广泛和复杂的规范来进行更改,然后可以将其应用于多个分支,而不是一个分支.)

还有一个地方可能是罪魁祸首(在我的例子中)。考虑:

$ git cat-file -p 20b9cd59c6c6a1a2bccfb2ddb9af68c083a28698
tree dee80bcd856b23aceb8946473bf64d9aef0fe629
parent b12dc8b9388dc0a2ae34563426043a612d296195
author XXX <xxx@example.com> 1355477802 +0200
committer XXX <xxx@example.com> 1355478447 +0200
encoding cp1251

Add (literally) three characters to one file that will
inadvertently create hours of fun for people years later.

这是编码,在本例中为 Windows 1251。 找到它的人是这样总结的:

msg-filter gets the raw message, no encoding meta-information. So even when you use an 8-bit transparent msg-filter (such as a plain cat), the re-created commit won't contain that encoding meta-information.

(That's slightly imprecise, because the filter gets the encoding information, it could read it via the GIT_COMMIT env variable. It's the output, that doesn't control encoding. At least I don't know how...)

他使用 Graft Points 修复了我们特定情况下的一般混乱情况。这超出了我目前的 git 知识范围,因此我不会尝试解释它。