我有一个repo,其中有两个分支master
和master-old
,它们是作为孤儿分支创建的。
现在我想将整个master
重新绑定到master-old
,但每个提交的树应保持不变,即master
和{{1}上每个提交的工作副本在rebase之前和之后应该看起来完全一样。
master-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
的整个提交历史记录中,创建了大量相同的文件,因此我需要解决大量冲突。 / p>
有没有办法以保持每个提交的树完整的方式重写历史记录?
答案 0 :(得分:4)
鉴于您希望保留与原始A--B--C--D
系列提交相关联的树,您根本不想 rebase 。 Rebasing意味着将提交转换为差异(变更集),然后将这些变更集一次一个地应用于某个现有起点 - 但您要做的就是将附加到A
的树复制到您的父提交为A'
的新提交H
,然后将附加到B
的树复制到父提交为B'
的新提交A'
,依此类推。
这是git filter-branch
运作良好的地方。当你跑:
git filter-branch <filter-list> <branch-name>
Git发现每个提交都可以从给定的<branch-name>
到达,然后复制每个提交。无论如何,通过按原样提取整个提交,运行<filter-list>
中的每个过滤器,然后使用生成的树和消息进行新提交,完成复制。它以与Git正常顺序相反的方式运行复制过程,即&#34;转发历史记录&#34;而不是向后。
如果新提交(可能已更改 - 可能不是树,可能已更改 - 可能 - 不是父,可能已更改 - 可能不是消息等)是100%与原始提交相同的bit-bit-bit,新提交的哈希ID不变。在这种情况下,默认的&#34;新的父母&#34;对于 next 提交与原始父提交相同。否则默认&#34;新父母&#34;下一次提交是我们刚刚提出的。
(实际上,因为提交图可以分散并再次合并,因为你可以跳过提交或添加新提交,所以filter-branch真正做的是将旧提交哈希的映射新的提交哈希。每次复制时,它都会输入一对:&lt; old-hash,new-hash&gt;到这个映射中。对于一个简单的线性链,你可以把它想象成只记住最近的提交& #39;新的哈希ID。)
现在,您遇到的问题是您希望更改一个特定提交的父哈希ID,即根提交。有专门针对它的过滤器--parent-filter
。还有两种方法可以做到这一点,但让我们首先描述--parent-filter
。这来自the git filter-branch
documentation:
- parent-filter&lt; command&gt;
这是用于重写提交的父列表的过滤器。它会 在stdin上接收父字符串并输出新的父字符串 stdout上的字符串。父字符串采用的格式为 git-commit-tree(1):对于初始提交为空,&#34; -p parent&#34;为一个 正常提交和&#34; -p parent1 -p parent2 -p parent3 ...&#34;为一个 合并提交。
因此,您可以测试stdin是否为空,如果是,则输出-p <hash-of-H>
。结果将是:
E--F--G--H--A'-B'-C'-D' <-- master
(不是你要求的,但可能更好)。
(要复制E-F-G-H
链,您必须将master-old
作为正参考传递,并且因为任何逐位相同的提交必须具有相同的哈希ID作为原始版本,您必须至少进行一次更改以提交E
,例如将提交者tiemstamp更改为一秒。)
另外两种方法是值得一提的。一种是使用--commit-filter
:这是实际进行新提交的命令。你可以在这里做任何事情,包括完全省略一些提交;但是所有其他过滤器的原因是为了使事情更容易,所以在这种情况下根本就没有理由使用提交过滤器。
git replace
最后,有the git replace
command。 git replace
做的是创建保留在存储库中的新对象,由refs/replace/
名称空间中的特殊名称引用。每当Git通过其哈希ID查看某个对象时,Git通常首先检查是否存在refs/replace/<hash-id>
。如果是这样,Git会查看该引用指向的对象。
这意味着您可以构造一个非常类似于提交A
的新Git对象,但略有不同。稍有不同的是,新的提交对象中存储了一个父哈希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...
,所以请在此处使用正确的ID。
当git log
转到查看从提交D
开始的历史记录时,它会查看提交D
,然后获取D
&# 39;父ID C
,查看提交C
,获取B
的ID,然后继续提交B
,获取A
& #39;的ID和......哇,嘿,这个人有refs/replace/
!我们毕竟不要看A
!我们来看看A'
!它会将A'
显示为B
的父级,然后转到A'
的父级并向您显示H
,然后{{1等等。
当您使用G
时,您不必复制任何其他提交。 您拥有的是新的&#的提交历史记录34;更好&#34;承诺取代旧的&#34;不那么好&#34;一个,但两者实际上是共存的。 Git在这些条件下使用替换:
git replace
;和refs/replace/hash
。如果您愿意,要求3可让您查看原始(未放置的)历史记录。第2项表示默认情况下,在git --no-replace-objects
上,您不获得替换。你必须明确地要求它们(这并不难,但也没有任何简单易用的前端)。
由于上面的第2项,您可能希望进行替换,确保所有内容都按照您喜欢的方式运行,然后运行git clone
。由于您没有运行git filter-branch
,Git会看到替换提交git --no-replace-objects filter-branch
而不是原始提交A'
。因此,它会复制A
而不是A'
。你不会需要A
。当它复制--parent-filter
到E
时,新副本将与原始文件一点一点地相同,因此这些副本将保持不变。最终结果与使用正确的父过滤器运行H
相同。