Git:将一个分支的现有提交插入到另一个分支的历史记录中

Git: Insert existing commits from one branch into another branch's history

我想将一个分支的多个提交插入到另一个分支的历史记录中,以便:

A - B - F - G - H - I - J  (branch working)
    \
      C - D - E            (old branch)

变成...

A - B - C - D - E - F - G - H - I - J (continue with branch working)

到目前为止,我尝试过的任何事情都导致了多重冲突,使我无法继续,但我不关心它们,只要后续提交的状态相同即可。 commit F "doesn't know" 的问题是如何变成commit E 的child?

您实际上不能那个,因为那会涉及更改一些现有的提交。在这种情况下,现有提交 F 存储 B 的哈希 ID 作为其父项。无法更改现有提交:提交完全是只读的(事实上,Git 的所有内部对象都是只读的)。

我假设您想假装 FE 的哈希 ID 存储为它的父项,而不更改与任何现有提交关联的快照。您可以这个,并且您可以做很多类似的事情。不过,在我们看那些之前,让我们先看一下 git rebase.

这里使用 git rebase 的问题是通过 复制 提交的 rebase 函数(到目前为止一切都很好),每个副本都像 1 通过使用 git cherry-pick (这是事情开始出错的地方)。要挑选一个提交,Git 将:

  • 将提交的快照与提交的父快照进行比较,例如,JI,或 FB。您可以通过 运行ning git diff <hash1> <hash2> 或更简单地通过 git show <hash2>.

  • 自己完成此操作
  • 将此处显示的差异与父级(上面的 <hash1>)的差异合并到当前或 HEAD 提交,并使用合并结果创建一个新的提交。

  • 将原始提交的消息复制到新提交。

新提交一如既往地进入当前 b运行ch,使 b运行ch 一个提交更长,并更改 HEAD 提交以命名新提交.

合并步骤,加上 Git 将快照(提交)转换为变更集(差异)这一事实,意味着最终的精选结果可能是与原始快照截然不同的快照.这一切都取决于 <hash1>HEAD 在您开始该过程时的区别。

git rebase 命令实质上是一次自动完成对整个提交系列的挑选过程。在所有复制结束时,git rebase 强制 b运行ch 标签指向最终复制的提交。这将允许您将 F 复制到父级为 E 的新 F',然后将 G 复制到父级为 [=34= 的新 G' ],以此类推:

A--B--F--G--H--I--J   <-- (original)
    \
     C--D--E   <-- (target of rebase)
            \
             F'-G'-H'-I'-J'   <-- branch

1有时git rebase字面上的运行s git cherry-pick,有时它使用通常应该产生相同结果的方法,但是在一些奇怪的极端情况下,不会。


现在,假设我们创建了一个 Git 对象,Git 可以 "look aside" 访问它,只要它要使用 F。这个替换-F 像这样进入图表:

A--B--F--G--H--I--J   <-- branch
    \
     C--D--E   <-- some_name
            \
             Frepl   <-- refs/replace/<hash>

我们将 F 的提交消息复制到 Frepl 的提交消息,并复制大部分其他字段,包括树对象的内部 Git 哈希 ID提交。但是不是指向提交 B,提交 Frepl 指向提交 E.

现在我们只需要 Git 到 "turn its eyes" 从 FFrepl 每次它出于任何原因与提交 F 一起工作时。有一种方法可以做到这一点:我们给 Frepl 一个特殊的名字,refs/replace/<em>big-ugly-hash-id</em>,其中 big-ugly-hash-id 是提交 F.

的实际哈希 ID

进行替换的Git命令是git replace。后备是自动的:Git 总是这样做,除非我们 运行 git --no-replace-objects。这一切都是在 没有 更改任何 现有 Git 对象的情况下完成的,因此它只是添加到存储库中。

替换对象的最大缺点是 git clone 默认情况下 不会 复制替换对象(它不获取它们,也不获取它们的名称)。这意味着克隆人没有看到替代品,也从不把目光放在一边。您可以显式地将替换项添加到您的获取 refspecs 中以获取它们,但这有点麻烦。默认情况下,git push 操作也不会 运行 传递它们。

最大的优势是它们不需要每个人停止使用原始提交来支持新的和改进的提交。但是,如果您 可以 让每个人都切换过来,您可以使用 git rebase 复制许多提交并移动 b运行ch 标签。或者,您可以最初使用 git replace 来制作替换对象,然后 运行 git filter-branch 不带过滤器,但告诉它过滤 b运行ch(es)发生替换。

git filter-branch所做的是复制每个提交(在b运行ch或b运行ches上它被告知要过滤) .它——至少在逻辑上;有很多优化——提取每个提交,从最旧的提交到最新的提交,再到一个临时目录,按某种顺序应用每个过滤器,然后使用过滤器所做的任何事情进行新的提交。如果新提交与原始提交完全相同,则这两个提交实际上只是一个提交,否则副本是一个新的不同提交。每个新副本的默认父副本是在父副本的早期副本中所做的提交(尽管有一个 --parent-filter 让你改变它!)。它通过替换后备噱头来完成所有这些,2 所以当 Git 去复制 F 时,它实际上复制了 Frepl,并且然后跟随 Frepl 回到 E。如果我们有这个过滤器名称 branch,结果是 Git 复制 A,然后是 B,然后是 C,然后是 D,然后 E,然后 Frepl,然后 G,然后 H,然后 I,然后 J,给出:

A--B--F--G--H--I--J   <-- refs/original/refs/heads/branch
    \
     C--D--E   <-- some_name
            \
             Frepl   <-- refs/replace/<hash>
                \
                 G'-H'-I'-J'   <-- branch

注意 branch 指向提交 J',其父级为 I' 但其树(快照)与 J 的相同。提交 I' 指向 H',后者指向 G',后者指向 Frepl(我们在 运行 [=54 时创建的副本) =]:在过滤过程中保持不变)。这指向 E(它自己未更改的副本),指向 D,依此类推 A

最后的效果,然后,如果我们扔掉所有 refs/original/ 名称,如 refs/original/refs/heads/branch,就是 "cement in place" 任何替换提交。然后我们可以删除 Freplrefs/replace/ 名称,看起来好像我们在存储库中只有提交 A-B-C-D-E-Frepl-G'-H'-I'-J'。许多提交的hash IDs已经改变,所以这个仓库不再兼容原来的仓库;但是如果我们从名称 branch 开始,我们只会在所有正确的地方看到闪亮的新复制提交。


2当然,如果您 运行 git --no-replace-objects filter-branch,将禁用替换后备。可能从来没有任何理由这样做。