考虑到您想保留与原始 A--B--C--D 提交系列相关联的树,您实际上并不想进行 rebase。Rebase 意味着将提交转换为差异(更改集),然后逐个将这些更改集应用于某个现有起点,但您想要做的只是将附加到 A 的树复制到新提交 A',其父项为 H,然后将附加到 B 的树复制到新提交 B',其父项为 A',以此类推。
这就是 git filter-branch 起作用的地方。当您运行:
git filter-branch <filter-list> <branch-name>
Git会找到从给定的
<branch-name>
可达的每个提交,并
复制这些提交。逻辑上讲,通过提取整个提交内容,运行
<filter-list>
中的每个过滤器,然后使用生成的树和消息创建新的提交来完成复制过程。它按照与Git正常顺序相反的顺序,即“朝向历史的前方”,而不是向后遍历。
如果新提交(其可能已更改或未更改的树、父级、消息等)与原始提交完全一致,则新提交的哈希ID不变。在这种情况下,下一个提交的默认“新父级”与原始父级相同。否则,下一个提交的默认“新父级”就是我们刚刚创建的那个。
在实践中,由于提交图可能会发散和再次合并,并且因为您可以跳过提交或添加新提交,filter-branch 实际上是将旧提交哈希映射到新提交哈希。每次制作副本时,它都会将一对 <old-hash、new-hash> 输入此映射中。尽管对于简单的线性链,您可以将其视为仅记住最近提交的新哈希 ID。
现在,您面临的问题是要更改一个特定提交的父哈希 ID,即根提交。有一个专门用于此目的的过滤器,即 --parent-filter。还有两种其他方法可以完成此操作,但是让我们先描述 --parent-filter。这是从 git filter-branch 文档中提取的。
--parent-filter <command>
这是重写提交的父级列表的过滤器。它将在标准输入上接收父字符串,并应在标准输出上输出新的父字符串。父字符串的格式如git-commit-tree(1)所述:对于初始提交为空,对于普通提交为“-p parent”,对于合并提交为“-p parent1 -p parent2 -p parent3 ...”。
因此,您可以测试标准输入是否为空,如果是,则输出-p <hash-of-H>
。结果将是:
E--F--G--H--A'-B'-C'-D' <-- master
虽然不完全符合您的要求,但可能更好。
要复制 E-F-G-H
链,您需要将 master-old
作为正参考传递,由于任何一次比特对比相同的提交必须具有与原始提交相同的哈希 ID,因此您至少必须对提交 E
进行一次更改,例如将提交者时间戳更改一秒钟。
另外两种方法也值得在此提到。一种是使用 --commit-filter
:这是实际创建新提交的命令。您可以在此处执行任何操作,包括完全省略某些提交;但是所有其他过滤器的原因是使事情更加容易,因此在这种情况下根本没有理由使用提交过滤器。
使用 git replace
最后,有
git replace
命令。
git replace
的作用是创建新对象并保留在存储库中,在
refs/replace/
名称空间中引用。每当 Git 按其哈希 ID 查看某个对象时,Git 通常首先检查是否存在
refs/replace/<hash-id>
。如果是这样,Git 就会查看该引用所指向的对象。
这意味着您可以构建一个新的 Git 对象,它非常类似于提交
A
,但略有不同。小差异是新提交对象具有一个父哈希 ID 存储在其中。父哈希 ID 是提交
H
的哈希 ID。 (请注意,它与
A
具有相同的
树。)
现在,您已经拥有了这个新对象——我们将其称为
A'
——您将其插入存储库并使
refs/replace/<big-ugly-hash>
指向它:
A--B--C--D <-- master
E--F--G--H <-- master-old
\
A' <-- refs/replace/deadcabf001...
(基于 A
的实际哈希值,它可能不是真正的 deadcabf001...
,因此请使用正确的标识符。)
当 git log
从提交 D
开始查看历史记录时,它将查看提交 D
,然后获取 D
的父 ID C
,查看提交 C
,获取 B
的 ID 并继续移动到提交 B
,获取 A
的 ID 然后...哇,嘿,这个有一个 refs/replace/
!我们不再查看 A
!让我们看看 A'
!它将 A'
显示为 B
的父节点,然后移动到 A'
的父节点并显示 H
,然后是 G
,以此类推。
当你使用
git replace
时,你不需要复制其他任何提交。你所拥有的是一个提交历史,其中新的“更好”的提交取代了旧的“不太好”的提交,但实际上两者都存在。Git在以下条件下使用替换:
- 当然,它必须有替换对象;
- 它必须要查找某个哈希值为hash的对象,但在引用中找到
refs/replace/hash
;以及
- 它必须以正常方式运行,而不是作为
git --no-replace-objects
。
第3个要求让你可以查看原始(未替换)历史记录,如果你愿意的话。第2条意味着在
git clone
时,默认情况下你不会得到替换。你必须显式地请求它们(这并不难,但也没有任何简单易用的前端界面)。
使用filter-branch进行替换
由于上述第二个条目,您可能希望进行替换,确保它按照您的喜好正常工作,然后运行
git filter-branch
。由于您没有运行
git --no-replace-objects filter-branch
,Git将看到
替换提交
A'
而不是原始提交
A
。因此,它将复制
A'
而不是
A
。您将不需要
--parent-filter
。当它复制
E
到
H
时,新的副本将与原始副本完全相同,因此这些副本将保持不变。最终结果将与使用正确的父筛选器运行
git filter-branch
相同。