如何更改在另一个分支上发生的提交范围?



我的假设是解决方案很简单,但是我找不到任何清楚地解决我的问题的示例。我在以下方面寻求指导:

我目前的历史如下:

A - - - C - - - - - F            Master
                    
    B - - - D - E - - - G - H    Topic

D, E, G, H应该都发生在Master上。在这一点上唯一的区别应该是commit B。所以它应该是这样的:

A - - - C - D - E - F - G - H    Master
  
    B                            Topic

或者更好:

A - C - D - E - F - G - H        Master
                          
                            B    Topic

…但我也不知道如何做到这一点。

我似乎不能同时合并DEGH。我总是在Master中出现B

我相信我可以挑选他们,但这似乎会带来未来的问题。最好的解决方案是什么?

如果您已经将这些分支推送到任何远程repo,那么重写过去的历史记录不是一个好主意。如果你已经推了,我会这样做来避免重写:

  1. MasterF开始。
  2. Master上选择D, E, GH
  3. 检出Topic并从Master合并。

如果你这样做,你将有:

A - - - C - - - - - F - - - - - D' - E' - G' - H'-       Master
                                                
    B - - - D - E - - - G - H - - - - - - - - - - - -    Topic

历史记录看起来不太整洁,但是你可以在分支中拥有你想要的所有代码。


如果你真的想重写历史记录,因为这只是你本地的,或者你不想打扰其他可能从你的repo中提取的人,那么:

  1. 回滚到C(回滚前记下F的哈希值)
  2. Master上选择D, E, F, GH
  3. MasterH头分支Topic2
  4. Topic2上选择B
  5. 删除Topic,将Topic2重命名为Topic

得到:

A - C - D - E - F - G - H        Master
                          
                            B    Topic

假设图纸是准确的,"你不能从这里到达那里"。(不过,你或许可以靠得足够近。)具体来说,绊脚石是commit F

当前在Master分支上的F的提交有C作为父提交。

在新的期望图中,标记为F的提交将E作为其父提交。

由于提交是其所有内容(其树,消息,作者/提交者和时间戳,以及问题所在的所有父id)的加密校验和,因此任何以E为父的新的"F-like"提交都必然是一个不同的提交。

"better yet"图显示提交B时,H是它的父节点,所以这再次不同于"existing"图。因此,这与F具有相同的问题(具有相同的潜在解决方案)。

分支标签MasterTopic也必须更改。这两项更改都不会是"快进"(git术语)。

这里你需要回答的问题是:除了你自己之外,还有谁可以访问这个存储库或这个存储库的副本,他们(所有这样的访问器)是否愿意做一些事情来处理一个非快进的分支标签更改?

如果答案是"没有其他人有这样的权限",或者"他们都愿意做需要做的事情",你就可以了。如果不是,你需要另一种方法。现在让我们做最好的假设。

你仍然不能从这里到达那里,因为我们需要对你所绘制的commit F进行更改,而你永远不能更改提交。你也不能改变DE,虽然你不需要直接改变H,但你需要改变G——它目前有两个父级——并且改变"冒进":我们马上会看到为什么,随着父级id的改变。

您需要做的是将复制提交到稍微不同的新版本。我们将复制提交D到一个版本,该版本省略了B的任何更改,并且只有C作为其父版本。让我们把这个副本命名为D',以表明它"很像D,但不完全相同"。

然后,我们将E复制到与旧DE具有相同更改的版本,但将D'列为其父。

接下来,我们将发现CF之间发生了什么,并对E'中的内容进行相同的更改,并将其提交为F',并带有与原始F相同的消息等。

最后,我们希望发生在FG之间的更改(而不是从EG),应用于F',并作为G'提交,具有相同的消息,等等,就像G一样;然后我们将GH的更改应用到G',并作为H'提交。

结果看起来像这样(我们还没有完全选择您不太喜欢的替代方案):

A---C---D'--E'--F'--G'--H'   <-- Master
     
      -----F   [old Master was here]
            
    B----D--E--G--H   <-- Topic

现在,为了使Topic结束于B,我们只需要将其重新指向提交B。其余的提交仍然在存储库中,但是没有标签指向它们(除了refflogs):

A---C---D'--E'--F'--G'--H'   <-- Master
     
      -----F
            
    B----D--E--G--H
     `.......................<-- Topic

我们现在有你想要的作为你不太喜欢的替代品。

为了获得您喜欢的替代方案,我们现在只需要将B重置到Master上。这将B复制到一个新的提交B',该提交具有与旧的AB相同的更改(省略任何由于在CH中而不再需要的更改),但将H'作为其父。重新绘制图,去掉所有不再标记的提交,得到:

A---C---D'--E'--F'--G'--H'   <-- Master
                         
                          B' <-- Topic

至于实际复制提交的机制,做这件事的主力是git cherry-pick。使用git rebase -i(没有-p),您可以让git做一系列的选择。但是,合并提交有一个小障碍。交互式重基可以省略一些简单的合并,但是对于更复杂的合并,您需要向git cherry-pick提供-m参数,以告诉它从合并的哪一边选择。

那么,从现有的起点开始,我将这样做(有许多可能的方法):

git checkout -b temp Master^  # make new "temp" branch pointing to commit C
git cherry-pick -m 2 Topic~3  # get C->D changes, make D'
git cherry-pick Topic~2       # get D->E changes, make E'
git cherry-pick Master        # get C->F changes, make F'
git cherry-pick -m 2 Topic~1  # get F->G changes, make G'
git cherry-pick Topic         # get G->H changes, make H'

在此过程中,或者在最后,在每次提交时使用git show,以确保您得到了您想要的(并且我在这里有正确的-m参数)。

如果一切顺利,移动分支Master以匹配新的temp:

git checkout master; git reset --hard temp # reset Master to equal temp
git branch -d temp             # and delete temp, no longer needed
现在你可以重置和重置Topic:
git checkout Topic; git reset --hard HEAD~4 # reset to point to B
git rebase Master              # copy B to B' and reset to B'

(稍微高级一点的技巧:不是git checkout -b temp,而是git checkout,并使用分离的HEAD模式来构建所需的新Master,然后手动移动它。交互式rebase shell脚本就是这样做的。但它更令人困惑,所以最好是显式的,如上所述。


如果你有其他人使用这些分支,他们必须通过自己的重置和/或重置来从你的"非快进"更改中恢复。该方法在git rebase文档中被描述为"从上游rebase中恢复"。

最新更新