Git:将提交移动到分支之前

3
我有如下的提交历史记录:
  B-X
 /
A-C
 \
  D

我希望将X应用于所有分支BCD。虽然cherry-pick可以实现这个功能,但会产生以下历史记录:

  B-X
 /
A-C-X
 \
  D-X

现在提交X有三个副本,如果我有很多分支,这非常不方便。理想情况下,我希望历史记录如下所示:

    B
   /
A-X-C
   \
    D

我只想在出现一次的情况下查找 X,这样历史记录就会更清晰。我应该使用哪个命令来实现这个目标?

4个回答

2

首要的一点是,你所请求的修复将改写历史记录,这只有在没有其他人使用该分支时才应该这样做。例如,如果您将D移动到X之后,那么以前检出D的任何其他人在拉取时都会感到非常困惑。只有在没有其他人使用它或他们知道并预期更改时才应该这样做。

// Make a new temporary branch starting at A, and move X into it.
git checkout -b tmp A
git cherry-pick X

// For each of B, C and D, rebase them on top of the temporary branch.
git checkout <branch>
git rebase tmp

+1 提醒我们,如果其他人正在基于此存储库历史记录编写代码,则不应这样做。 - Code-Apprentice

1

其他人指出的“拣选”策略非常有效。只想指出,您也可以使用一组带有 --onto 标志的变基来执行此类操作,而不需要临时分支。

将您的提交克隆到新位置

git rebase --onto A B X

这意味着您应用存在于X点但不存在于B点的提交,并将它们应用到A的末尾。这将导致以下结果。
  B-X
 /
A--Y
|\
D C 

请阅读本答案底部关于Y的注释,了解为什么它与X有不同的名称,以及git如何处理历史中的任何更改。
现在你已经将X应用到了想要的地方,现在你只需要将其他分支rebase一下,这样它们也会在它们的历史记录中拥有它。
更新下游分支。
git checkout C
git rebase Y

git checkout D
git rebase Y

git checkout B  
git rebase Y

这里唯一的限制在于B。无论字母代表分支名称还是SHA1提交,上面的顺序都是有意义的。但是,如果BX实际上是一个分支,那么您需要检出该分支并将其倒回到X创建之前。为了举例说明,我们称该分支为“bee”。
git checkout bee
git reset --hard B  
git rebase Y

现在您有...
     B
    /
A--Y-C
    \
     D 

接下来,只需吃蛋糕。

序言

变基、挑选、修订,哦我的天啊!

提交及其SHA1与父提交、执行的更改和其他一些细节是唯一的。因此,当更改提交的父或祖先时,该提交实际上会被克隆并将具有新的SHA1。变基、挑选或任何修改现有提交或提交的父级的操作都将导致修改后的提交获得新的SHA1。而且,由于该提交的SHA1已更改,所有该提交的子代都认为它们有一个新的父级,因此它们也会获得新的SHA1哈希。

旧提交仍然存在,因此上图显示Y作为X的克隆版本,具有新名称和新父级。两者仍然存在,在大多数情况下,旧提交可以轻松检索,在某些情况下(如此例),您实际上需要有意将旧提交移开。这就是为什么在rebase --onto之后,您会看到图表显示X和Y。X提交没有“移动”,更像是克隆。之后,X仍然存在,并且需要通过检出B分支并将其重置为“X”之前的提交来处理。

实际上,BCD中的提交也会被克隆,并且它们都会有新的SHA1哈希值。
这就是为什么在与其他开发人员共享提交后重新定位或挑选,或修改提交可能会引起麻烦的原因。如果那些开发人员拉取克隆,他们的本地存储库将认为这是新的工作,他们将获得重复的提交,并且由于这些提交执行完全相同的操作,他们将遇到冲突。然后,克苏鲁将崛起并开始统治世界。
这种困难是由于在这些示例中,我们使用分支名称作为提交名称。如果这个问题特别涉及修改历史记录的副作用,我会添加更准确的图表。

谢谢提到 onto 选项!我认为它的工作方式类似于 cherry-pick,这里的 Y 是临时分支吗? - Yang
Y代表将X提交移动到A后的结果。它是一个新的提交,执行与X完全相同的更改。X实际上仍然存在于其原始位置,这就是为什么您必须重置B分支的原因。 - eddiemoya
无论何时更改提交的父节点,都会更改其SHA1。我使用Y来指出这一点。 - eddiemoya
我添加了一个长篇序言来解释修改历史记录的影响。只需要记住,Y 是 X 的克隆版本,当你克隆一个提交(这就是在进行 rebase、cherry-pick、amend 等操作时发生的),它必须要有一个不同的 SHA1 值,因为它有一个新的身份。所以 Y 不是一个临时分支,而是 X 的克隆。 - eddiemoya
谢谢您提到这个。我认为有一个命令可以执行此任务,但事实证明它很危险,因为它修改了所有分支的父级。 - Yang
我不会过分地称其为“危险”,我在序言中所说的是真实的——它是“麻烦的”。大多数情况下,在变基(rebase)或樱桃拣选(cherry-pick)之后(它们都修改历史记录),您可以通过运行git reset -hard ORIG_HEAD或筛选参考日志来撤消所有操作。如果您已经共享了这些提交,那么这可能非常麻烦,但即使如此,也可以通过在他们的工作分支上使用相同的变基--转移技巧来解决问题。 - eddiemoya

0

看起来对我来说是XY问题,也就是询问你的尝试解决方案而不是你的实际问题

为什么不从共同祖先开始,在单独的bugfix-X分支上创建bugfix提交“X”,然后将其合并到“A”、“B”和“C”中。例如,Junio C. Hamano在博客文章“早期解决主题分支之间的冲突/依赖关系”中描述了这个想法。

这意味着创建以下历史记录:

  B---------b
 /          |
A-C-----c   |
|\      |   /
| D-d   /  /
\   |  /  /
 \X/--/--/

其中 'b'、'c' 和 'd' 是合并提交。


可以使用以下命令完成:

  1. 首先,将提交记录'X'(rebase)移动到分支(提交记录)'B'、'C'和'D'的公共祖先上。

    $ git rebase --onto $(git merge-base B C D) B X

    这将导致以下情况,其中bugfix提交记录'X'位于单独的bugfix分支上:

      B
     /
    A-C
    |\
    | D
    \
     \X'
    
  2. 接下来,将bugfix合并到所有需要它的分支中。例如,将其合并到分支'D'中可以使用以下命令:

    $ git checkout D
    $ git merge X

    这将导致以下结果:

      B
     /
    A-C
    |\
    | D---d
    \    /
     \X'/
    
  3. 对于分支'C'和'B'也要执行相同的操作。


抱歉造成困惑,但这是我的尝试解决方案,其中X被重复3次。 - Yang

0

一个可能的解决方案是使用多个cherry-pick。检出A并cherry-pick X以创建

  B-X
 /  
A-X'
 \
  \-C
   \
    D

现在你应该在X上,这样你就可以挑选B:

  B-X
 /  
A-X'-B'
 \
  \-C
   \
    D

检出 X' 并挑选 C。再重复一次:检出 X' 并挑选 D。

这可能不是最有效的解决方案,但应该可以工作。


1
谢谢!使用rebase更好,这样我们就不会保留B、C、D的副本。 - Yang
@Yang 不错的观点。我本来想提到这个作为另一种选择,但出于某种未知的原因漏掉了它;-( - Code-Apprentice

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