一次修改基本分支并重新设置所有子级的基础



我有这样的东西:

A //branch 1

B - C //branch 2

D //branch 3

我想要这样的东西:

A - E //branch_1

B' - C' //branch_2

D' //branch_3

我想执行一个命令并一次重新设置所有分支的基础,这样我就不必一个接一个地重新设置它们的基础。这可能吗?

简短回答:不,不可能。。。但是你可以最小化你必须做的工作量。下面是关于最小化工作量的长答案。

理解这一点的关键是Git的分支名称——示例中的名称branch_1branch_2branch_3——只是"指向"一个特定提交的标识符。是提交本身形成了实际的分支。有关详细信息,请参阅我们所说的";分支"?同时,git rebase所做的是复制一些提交,而新的副本通常是在新的基础上制作的(因此是"重新基础")。

在您的特定情况下,只有一个提交链需要复制。这就是B--C--D链。如果我们去掉所有的标签(分支名称),我们可以这样绘制图片段:

A--E

B--C--D

您的任务是将B--C--D复制到B'--C'--D',它们类似于BD,但在E之后,而不是在A之后。我会把它们放在上面,这样我们也可以在图片中保留原始的B--C--D链:

B'-C'-D'
/
A--E

B--C--D

制作好副本后,可以更改标签,使其指向副本,而不是指向原件;现在我们需要将D'向上移动,这样我们就可以将branch_2指向C':

D'   <-- branch_3
/
B'-C'     <-- branch_2
/
A--E           <-- branch_1

这至少需要两个Git命令才能完成:

  1. git rebase,将B-C-D复制到B'-C'-D',并将branch_3移动到指向D'。通常这将是两个命令:

    git checkout branch_3 && git rebase branch_1
    

    但您实际上可以使用一个Git命令来完成此操作,因为git rebase可以选择执行初始git checkout:

    git rebase branch_1 branch_3
    
  2. 23以重新指向CCD_ 24。

    我们知道(从我们仔细绘制的图表中可以看出,我们可以执行branch_3中的单个git rebase来复制所有提交),branch_2指向branch_3所指向的提交"后退一步"。也就是说,在开始时,branch_2名称提交Cbranch_3名称提交D。因此,一旦我们都完成了,branch_2就需要将commit命名为C'

    由于它从旧branch_3的尖端后退了一步,因此之后它必须从新branch_3的尖端后退一步1既然我们已经完成了rebase并拥有了B'-C'-D'链,我们只需指示Git将标签branch_2branch_3指向的位置向后移动一步:

    git branch -f branch_2 branch_3~1
    

因此,对于这种情况,它至少需要两个Git命令(如果您喜欢单独的git checkout,则需要三个)。

请注意,有时需要更多或不同的命令,即使我们只移动/复制两个分支名称。例如,如果我们从开始

F--J        <-- br1

G--H--K   <-- br2

I    <-- br3

并且想要复制所有的G-H-(K;I),我们不能用一个git rebase命令来做到这一点。我们可以做的是首先重新建立br2br3的基础,复制四个提交中的三个;然后对剩余的分支使用git rebase --onto <target> <upstream> <branch>来复制剩余的一个提交。

(事实上,git rebase --onto是最通用的形式:我们总是可以用一系列git rebase --onto <target> <upstream> <branch>命令来完成整个工作,每个分支一个。这是因为git rebase确实做了两件的事情:(1)复制一些提交集,可能是空的;(2) 将分支标签移动到git branch -f。复制集由<upstream>..HEAD的结果确定——见下文gitrevisions和脚注1——并且复制位置由--onto设置;并且分支的目的地是CCD_ 54在进行复制之后结束的任何地方。如果要复制的集合为空,则HEAD将直接到达--onto目标。因此,看起来就像一个简单的脚本可以完成所有的工作。。。但见脚注1。)


1然而,这假设git rebase最终会复制所有提交。(此假设的安全级别根据所讨论的重新基准而变化很大。)

事实上,虽然要复制的初始提交集是通过运行git rev-list --no-merges <upstream>..HEAD(或者等效的CCD_58)来确定的,但通过计算"要复制"one_answers"不需要复制,因为现在将是上游"范围内的每个提交的git patch-id,可以立即进一步缩减初始集。也就是说,rebase代码使用<upstream>...HEAD--right-only --cherry-pick组合而不是<upstream>..HEAD2所以我们不仅省略了合并提交,还省略了已经在上游的提交。

我们可以自己编写一个脚本来完成这项工作,这样我们就可以定位每个分支名称在我们希望重新基准的分支集中的相对位置。(我不久前做了大部分实验。)但还有另一个问题:在挑选樱桃的过程中,由于冲突解决,一些提交可能会变为空,您将git rebase --skip它们。此更改剩余复制提交的相对位置。

这最终意味着,除非对git rebase进行一点扩充,以记录哪些提交映射到哪些新提交和哪些提交被完全删除,否则不可能制作出一个完全正确、完全可靠的多重基脚本。我认为,在不修改Git本身的情况下,使用HEADreflog就可以足够接近:只有当有人开始这种复杂的rebase时,这才会出错,但在中间,做一些git reset --soft之类的事情,然后恢复rebase。

2这对某些形式的git rebase来说确实如此,而对其他形式则不然。生成列表的代码因您使用的是--interactive和/或--keep-empty和/或--preserve-merges而异。基于非交互式git am的重库使用:

if test -n "$keep_empty"
then
# we have to do this the hard way.  git format-patch completely squashes
# empty commits and even if it didn't the format doesn't really lend
# itself well to recording empty patches.  fortunately, cherry-pick
# makes this easy
git cherry-pick ${gpg_sign_opt:+"$gpg_sign_opt"} --allow-empty 
--right-only "$revisions" 
${restrict_revision+^$restrict_revision}
ret=$?
else
rm -f "$GIT_DIR/rebased-patches"
git format-patch -k --stdout --full-index --cherry-pick --right-only 
--src-prefix=a/ --dst-prefix=b/ --no-renames --no-cover-letter 
"$revisions" ${restrict_revision+^$restrict_revision} 
>"$GIT_DIR/rebased-patches"
ret=$?
[snip]
git am $git_am_opt --rebasing --resolvemsg="$resolvemsg" 
${gpg_sign_opt:+"$gpg_sign_opt"} <"$GIT_DIR/rebased-patches"
ret=$?
[snip]

--cherry-pick --right-only通过使用的任何命令(cherry-pick或format-patch)传递给git rev-list代码,这样它就可以从对称差异中获得右侧的提交ID列表,其中删除了"预先挑选的cherry-"。

交互式重定基准要复杂得多(!)。

这个问题是How do I rebase a chain of local git branches的超集?

我今天使用的解决方案是一个名为git-chain的工具,旨在解决这个问题。

在您的示例中,您可以指定一个链

git chain setup -c myfeature branch1 branch2 branch3

一旦建立了这个链,您就可以对branch1进行新的提交,然后运行git chain rebase -c myfeature,该工具就会很好地为您计算出所有的refs和rebases。

作为额外的好处,它还计算和处理对branch1branch2branch3的任何修改或新提交。

为了至少找出在初始重新基本后必须更新的分支,您可以运行

$ git branch --contains <commit A>

git无分支工具套件可以巧妙地解决这个问题:

$ git checkout branch1
$ git commit -m 'E'
$ git move -s B -d branch1
# the above is the same as the following,
# because `branch1` is already checked out:
$ git move -s B
# or, if you want to address branches instead of commits
# (this will also move all descendant commits/branches):
$ git move -b branch2 -d branch1

免责声明:我是作者。

请参阅git move的文档。

最新更新