如何在压缩和合并父分支后自动将所有子分支重定为主分支



基于这个问题,我有一个工作流程,我不断地在 PR 之上制作 PR,以便其他人更容易审查我的工作。目标是拥有更小的 PR 大小。所以我经常遇到以下情况:

G--H--I   <-- branch3
/    
D--E--F   <-- branch2
/    
A--B--C       <-- branch1
/
M          <-- master

以此类推,branch3之后的N分支.问题是,在我挤压并合并branch1之后,我必须手动变基分支 2, 3...N:

G--H--I   <-- branch3
/    
D--E--F   <-- branch2
/    
A--B--C 
/
M--S       <-- master, origin/master (branch1 changes are squashed in S)

在上述情况下,我必须运行:

git 结帐分支2 git rebase --onto master (SHA-1 of C)

Git 结帐分支3 git rebase --onto branch2 (SHA-1 of F)

等等...

有没有办法通过使用脚本自动重定所有分支来自动化此过程?我想不出的是一种自动检测正确的 SHA-1 作为每个变基参数传递的方法。

简介

我知道这个线程很旧,但是有一个专门解决此问题的新选项。这包含在 2022 年 10 月的 Git 2.38 中。

街区的新孩子

它被称为--update-refs,它是专门为解决这个问题而设计的。

溶液

因此,而不是您的示例:

git checkout branch2
git rebase --onto master
# switch branches and keep on rebasing

现在,您可以执行以下操作:

git checkout branch3 # you need to be on the 'farthest' branch
git rebase master --update-refs # this will rebase the whole tree
git push origin : --force-with-lease # this will push the updated branches

哗啦!整棵树都可以容纳。

只需确保您使用的是 GIT 2.39 或更高版本,因为版本 2.38 有一个错误选项。

也将此选项设置为默认值

如果您希望这是变基时的默认行为,为了避免每次都添加标志,您可以在.gitconfig中设置选项rebase.updateRefs

git config --global --add rebase.updateRefs true

有几个基本问题,或者可能是一个基本问题,这取决于你如何看待它。 那是:

  • 分支没有父/子关系,和/或
  • 分支,在你所说的意义上,不存在。我们所拥有的只是分支名称。 树枝本身就是海市蜃楼,什么的。 (这似乎不是看待它的正确方式,但它有助于摆脱大多数非 Git 系统对分支的更僵化的看法。

让我们从一个看似简单的问题开始,但由于 Git 是 Git,实际上是一个棘手的问题:哪个分支持有提交A-B-C

有没有办法通过使用脚本自动重定所有分支来自动化此过程? 我想不出的是一种自动检测正确的 SHA-1 作为每个变基参数传递的方法。

这个问题没有通用的解决方案。 然而,如果你有你画的确切情况,你的具体情况有一个特定的解决方案——但你必须自己写。

技巧问题的答案是提交A-B-Cmaster之外的每个分支上。 像branch3这样的分支名称只标识一个特定的提交,在本例中是提交I。 该提交标识另一个提交,在本例中为 提交H。 每个提交总是标识一些以前的提交——或者在合并提交的情况下,标识两个或多个以前的提交——而 Git 只是从末尾向工作。 "结束"正是其哈希ID存储在分支名称中的提交。

分支名称缺少父/子关系,因为可以随时移动或销毁每个分支名称,而无需更改存储在每个其他分支中的哈希 ID。 也可以随时创建新名称:创建新名称的唯一限制是您必须为该名称选择要指向的一些现有提交。

提交具有父/子关系,但名称没有。 不过,这导致了这种特定情况的解决方案。 如果提交Y 是提交 X 的后代,这意味着有一些向后路径,我们从Y开始,可以回到X这种关系是有序的——从数学上讲,它在提交集合上形成一个偏序——因此 X ≺ Y(X 在 Y 之前,即 X 是 Y 的祖先),然后 Y ≻ X(Y 继承 X:YX的后代)。

因此,我们采用我们的一组名称,将每个名称转换为提交哈希 ID,并执行这些 is-ancetor 测试。 Git 的 "is-ancestor" 运算符实际上测试 ≼(早于或等于),并且 is 相等的情况发生在:

...--X   <-- name1, name2

其中两个名称选择相同的提交。 如果发生这种情况,我们将不得不分析我们的代码可能会如何处理这种情况。 事实证明,这通常根本不需要任何特殊工作(尽管我不会费心证明这一点)。

找到"最后一个"提交后 - 每个提交"在"相关提交之前 - 我们现在需要执行变基操作。 我们有:

G--H--I   <-- branch3
/    
D--E--F   <-- branch2
/    
A--B--C 
/
M--S       <-- master, origin/master (branch1 changes are squashed in S)

正如您所展示的,我们知道S代表A-B-C序列,因为我们在制作S时选择了提交C(通过名称branch1)。 由于最后一次提交提交I,我们希望复制 - 就像变基一样 - 从DI的每个提交,副本在S之后登陆。 如果 Git 在复制操作期间根本没有移动任何这些分支名称,那可能是最好的,我们可以使用 Git 的分离 HEAD模式来实现这一点:

git checkout --detach branch3  # i.e., commit `I`

或:

git checkout <hash-of-I>       # detach and get to commit `I`

或:

git switch --detach ...        # `git switch` always requires the --detach

这让我们:

G--H--I   <-- branch3, HEAD
/    
D--E--F   <-- branch2
/    
A--B--C 
/
M--S       <-- master, origin/master

我们现在运行git rebase --onto master branch1名称branch1是否仍然可用,如果不可用,则运行git rebase --onto master <hash-of-C>。 这将根据需要复制所有内容:

G--H--I   <-- branch3
/    
D--E--F   <-- branch2
/    
A--B--C 
/
M--S       <-- master, origin/master

D'-E'-F'

G'-H'-I'  <-- HEAD

现在(?)我们需要做的就是回顾这些相同的分支名称集,并计算它们在原始提交链上的距离。 由于 Git 的工作方式 - 向后 - 我们将从它们结束的地方开始,然后向后工作以提交C。 对于此特定绘图,branch2为 3,branch3为 6。 我们还计算我们复制了多少次提交,当然也是 6 次。 所以我们从 6 中减去 3 表示branch2,从 6 中减去 6 表示branch3。 这告诉我们现在应该将这些分支名称移动到何处:对于branch3,从I'向零后退一步,对于branch2I'后退三步。 因此,现在我们对每个名称进行最后一次循环,并根据需要重新设置每个名称

(然后我们可能应该选择一个名字git checkoutgit switch

这里有一些挑战:

  • 我们从哪里得到这组名字? 名称有branch1branch2branch3等等,但实际上它们不会那么明显地相关:为什么我们要移动分支fred而不是分支barney

  • 我们怎么知道branch1是我们不应该在这里使用的,而应该用作"不要复制此提交"的参数到我们的git rebase-with-detached-HEAD?

  • 我们究竟如何进行这个祖先/是后代测试?

    这个问题其实有一个答案:git merge-base --is-ancestor是考验。 你给它两个提交哈希ID,它会报告左手的是否是右边的祖先:git merge-base --is-ancestorX Y测试XY。 它的结果是它的退出状态,适合在内置if的 shell 脚本中使用。

  • 我们如何计算提交?

    这个问题也有一个答案:git rev-list --countstop..start提交start开始,然后向后工作。 当它到达stop或其任何祖先时,它停止向后工作。 然后,它会报告访问的提交数。

  • 我们如何移动分支名称? 我们如何确定要登陆哪个承诺?

    这很简单:git branch -f将允许我们移动现有的分支名称,只要我们当前没有签出该名称。 由于我们在复制过程后处于分离的 HEAD 上,因此我们没有签出名称,因此可以移动所有名称。 Git 本身可以使用波浪号和数字后缀语法进行倒计时:HEAD~0是提交I'HEAD~1是提交H'HEAD~2是提交G'HEAD~3是提交F',等等。 给定一个数字$n我们只写HEAD~$n,所以git branch -f $name HEAD~$n就可以完成这项工作。

你仍然需要解决前两个问题。 解决方案将特定于您的特定情况。

值得指出的是,可能没有人为此编写适当的解决方案的原因 - 我多年前写了我自己的近似解决方案,但多年前也放弃了它 - 是如果你没有这个非常具体的情况,整个过程就会崩溃。 假设不是:

G--H--I   <-- branch3
/    
D--E--F   <-- branch2
/    
A--B--C       <-- branch1
/
M          <-- master

您首先是:

G--H--I   <-- branch3
/    
D--E--F   <-- branch2
/    
A--B--C       <-- branch1
/
M          <-- master

这一次,在提交I结束并复制所有通过但不包括提交C无法复制提交F的提交。 没有F'允许您在将分支名称复制到D'-E'-G'-H'-I'后移动分支名称branch2D-E-G-H-I

这个问题二十岁和二十年代就相当严重。 但是git rebase已经变得聪明了,新奇的-r(--rebase-merges)交互式变基模式。 它现在几乎拥有多分支变基到Just Work的所有机制。 这里有一些缺失的部分仍然有点困难,但是如果我们能够解决两个问题 -我们如何知道首先要多变基的分支名称- 我们可以编写一个git multirebase命令来完成整个工作。

最新更新