将整个git分支重新定位到孤立分支,同时保持提交树不变

3
我有一个仓库,其中有两个分支mastermaster-old,它是作为孤立分支创建的。
现在我想将整个master分支变基到master-old上,但每个提交的树应保持不变,即mastermaster-old上每个提交的工作副本在变基前后应该完全相同。
Current state
-------------
A - B - C - D     <--- master

E - F - G - H     <--- master-old

Desired state
-------------
E'- F'- G'- H'- A'- B'- C'- D' <--- master

我试图使用git rebase --onto master-old --root来实现这一点。问题是,在master的初始提交和master-old的整个提交历史中,创建了许多相同的文件,因此我需要解决大量的冲突。

有没有一种方法可以以保持每个提交的树结构的方式重写历史?


你正在做你需要做的事情。如果有合并冲突,那是不可避免的,如果已经完成了。 - jhpratt
可能相关:https://dev59.com/I3zaa4cB1Zd3GeqPSJ3c可以孤立现有分支吗? - Gabriel Devillers
1个回答

5
考虑到您想保留与原始 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在以下条件下使用替换:
  1. 当然,它必须有替换对象;
  2. 它必须要查找某个哈希值为hash的对象,但在引用中找到refs/replace/hash;以及
  3. 它必须以正常方式运行,而不是作为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。当它复制EH时,新的副本将与原始副本完全相同,因此这些副本将保持不变。最终结果将与使用正确的父筛选器运行git filter-branch相同。

感谢您详细的回答。我需要认真处理您的解决方案,以便真正理解它们。事实上,“git-filter-branch”文档提供了我需要运行的确切命令:“git filter-branch --parent-filter 'sed "s/^$/-p <graft-id>/"' HEAD”。但是,我不确定为什么它说“<graft-id>”而不是“<commit-id>”。 - moktor
1
“graft-id” 的意思是新复制品的新父节点:新建的分支在那一点被嫁接上了。Filter-branch 有点有趣(尽管慢)可以进行实验。请务必在真正的存储库的备份克隆上尝试,以防万一,在实验时,您可能希望从简单的手工构建的存储库开始,以加快速度。 - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接