GIT:如何对嵌套分支进行变基?

4
我的结构看起来像这样 ->
master
  develop 
    project
      <sprint_number>
        <task_number>

我在task_number分支上工作。然后我将任务与sprint分支合并。然后我将sprint与项目分支合并。这样,项目上的所有提交都是sprints,而sprint上的所有提交都是tasks。合并到项目分支后,我提交合并请求,并在合并到develop之前进行代码审查。
我应该一直向下执行rebase吗?例如:
git checkout develop
git rebase master
git checkout project
git rebase develop
git checkout <sprint_number>
git rebase project
git checkout <task_number>
git rebase <sprint_number>
3个回答

10

Git 分支名称实际上没有任何嵌套意义:它们只是指向特定提交的指针。

首先,画出提交 DAG(一部分)

通常情况下,我们需要做的是绘制一些提交有向无环图(DAG)片段,并考虑重新基于的情况。因此,我们从您的示例开始:

master
  develop 
    project
      <sprint_number>
        <task_number>
并添加一些节点(并将它们的“真实名称”哈希值,例如a1cf93a...替换为单个大写字母,因为这些名称过于庞大和笨重):

并添加一些节点(并用单个大写字母代替它们的“真实名称”哈希值,如a1cf93a...,因为这些名称太大且难以使用):

A <- B <- C                <-- master
      \
       D <- E              <-- develop
             \
              F <- G       <-- project
                    \
                     H     <-- <sprint_number>
                      \
                       I   <-- <task_number>

(这里的反斜杠应该是向上和向左箭头,但是在普通文本中很难画出来)。

也就是说,在这种情况下,我们在 master 分支上有(至少)三个提交记录(在提交 A 之前可能有任意数量的提交记录,我们只是没有画出来)。master 的尖端是提交记录 C,它指向回提交记录 B,而 B 又指向回 A

我们在 develop 分支上有两个提交记录,它们不在 master 分支上:提交记录 Edevelop 分支的尖端,它指向回 D,而 D 又指向回 B。提交记录 B 和它的所有祖先(包括 A 和之前的任何提交记录)都在 两个 分支 masterdevelop 上。

同时,提交记录 Gproject 分支的尖端;G 指向回 FF 指向回 E,以此类推。这意味着提交记录 AB 实际上存在于所有 三个 分支中。但是等等,还有更多!H<sprint_number> 分支的尖端,H 指向回 G,以此类推;I<task_number> 分支的尖端,I 指向回 H

最终,这意味着提交记录 AB 存在于(至少)五个分支(如图所示),而提交记录 DE 存在于至少四个分支,以此类推。

决定是否需要并允许变基

在 git 中,变基实际上是指将提交记录复制到新的、稍微不同/修改的提交记录中。(可能不是正确的方法。稍后我们将讨论这个问题,因为在你了解更多之前,它没有意义。)

现在,master 的尖端是提交记录 C,而不是提交记录 B。可以想象,在早些时候,master 的尖端是B,那时我们做了提交记录 D(也可能是 E)。但现在你正在考虑将 develop 分支变基到 master 的新尖端上。

要实现这一点,您必须将提交DE复制到新的不同提交中。 我们将称这些副本为D'E'。即使没有其他变化 - 实际上可能会有其他变化,特别是在BC之间有所不同的任何内容都会进入新的D'中 - 原始提交D的副本D'必须指向提交C而不是提交B
仅绘制此复制阶段(略去原始E的所有内容),我们得到:
A - B - C             <-- master
     \    \
      \     D' - E'   <-- develop (after rebase)
       \
        D - E         [abandoned]

这次我也简化了向左箭头,现在我们知道提交指向左边了。但是,虽然原始的 DE 不再被分支名称 develop 指向,但是一旦我们填写绘图的其余部分,它们仍然是可以到达的:

A - B - C             <-- master
     \    \
      \     D' - E'   <-- develop (after rebase)
       \
        D-E
           \
            F-G       <-- project
               \
                H     <-- <sprint_number>
                 \
                  I   <-- <task_number>

此时特别重要的是,原始提交 DE 已经*不再在 develop 上。

如何使用 rebase 命令

忽略 --fork-point(这里可以是一个解决方案),git rebase 命令实际上需要三个参数,其中一个通常只取自于 HEAD:

  • 要复制的最顶层提交(这通常只是“你当前的分支”,即 HEAD);
  • 限制要复制的提交的指示符,即指定-间接地-不复制的提交;以及
  • 将添加第一个复制提交的提交的身份。

后两者通常合并为一个 <upstream> 参数。与此同时,您首先需要使用 git checkout 命令切换到要重新定位的分支来设置第一个参数。例如,如果我们决定将 develop 重新定位到 master 上:

git checkout develop
git rebase master

在这里,要复制的顶部提交是像往常一样的HEAD提交,因为由于git checkout,它是develop的最顶端提交,并且新副本将生长的起始位置是master的顶部。 Git会考虑复制每个develop上的提交(这将是ABDE),但在此之前被告知避免复制master上的每个提交(这意味着ABC)。

(等等,什么?我们不应该复制C吗?但我们一开始就不打算复制C!那么没问题了,我们就不复制它!)这就是我们如何将两件事合并成一个<upstream>参数。我们希望在C之后添加新副本,并同时避免复制C和从C向后导致的所有路径。

所以如果我们选择继续进行这个git rebase,我们将复制DED'E',最终得到我们绘制的新图形片段。

这对于develop来说很好,但如果我们现在执行以下操作会发生什么:

git checkout project
git rebase develop

这一次,我们会要求git将project的尖端可达的所有内容(即GFEDBA,以及可能还有其他东西)复制到已经变基的develop的尖端,即提交E'

这是一个问题。如果我们运气好的话,它可能是一个自解决的问题,因为rebase会检测到某些被复制的提交并避免重新复制它们。也就是说,当git试图将D复制到一个(另一个)新的副本D''中时,它可能会检测到D已经存在于E'中。如果它确实检测到了,它就会跳过复制。当它试图将E复制到E''时,同样的事情也可能发生:它可能会检测到不需要复制,然后跳过复制。

另一方面,git的检测器可能会被愚弄,它可能会复制D和/或E。我们绝对不想要那样,所以最好完全避免要求git复制它们。
有很多种方法可以提出要求,包括交互式变基(在这里我们可以编辑pick指令,因此可以删除提交DE的两个pick行),或者更聪明地使用git rebase的参数:

git checkout project
git rebase --onto develop 'project@{1}'
这第二个命令使用reflog历史记录告诉git要复制的提交是那些在project(当前分支)上但不在project之前的尖端包含的提交。也就是说,'project@{1}'解析为未复制的原始提交E的提交ID。因此,这将只复制提交FG,到F'G'
(顺便说一下,如果你用彩色记号笔在白板上画你的DAGs,你可以用颜色来代表原始提交和它们的副本。我觉得这比所有的D'D''符号更容易阅读。我只是无法在StackOverflow上画它。)
我们可以使用reflog重复这个过程来处理sprint和task,以识别要留下的提交。
自从git 1.9,git rebase现在有了--fork-point,它本质上自动化了我们在这里使用reflogs所做的事情。(在git 2.1中有一个修复bug的git rebase --fork-point,无法发现不需要复制的提交,因此明智的做法是将此选项限制为2.1或更高版本。)因此,这可能是一种做法。
最后,在回到是否这是一个好主意的问题之前,我要再做一点说明。我们可以先重新定位任务,而不是将develop重新定位到master,将project重新定位到develop等等。这会告诉git将提交D复制到D',将E复制到E',将F复制到F',一直到将I复制到I'。任务分支将指向新提交I',其历史链回溯到C。现在,我们只需要通过找到正确的副本来重新定位sprint分支、project分支和develop分支的副本提交。更新后的develop应该指向E';更新后的project应该指向G';更新后的sprint分支应该指向H'
如果还有其他的sprint分支和/或task分支,它们可能需要复制一些提交,这些提交不会被上述方法复制,因此必须小心使用此技巧。像往常一样,画DAG会有所帮助。
重定位是否正确?

如果你的分支结构如此复杂,那么变基可能不是正确的方法。即使不是这样,也可能不是正确的做法。

要记住,正如我们刚刚看到的那样,变基涉及将提交复制一份,然后将分支标签移动到新的副本上,而不是原始版本。当你在仅 使用的存储库中执行此操作时,通常不会太混乱,因为你移动了所有分支标签,并且现在已经完成:你可以选择旧的、预复制状态或者新的、复制后的状态,你可以忽略所有中间(中途变基)状态,除非你需要进行所有这些变基的短暂期���。

但是,如果有其他人分享这个存储库,请考虑你将会给他们带来的影响。在进行大规模的变基之前,他们认为自己拥有正确的developproject、sprint和task分支指针。他们正在使用原始(尚未复制)提交并创建依赖于这些原始提交的新提交。

现在,你来告诉他们:“哦,嘿,忘掉那些旧的提交吧!使用这些全新的、闪亮的提交代替它们!” 现在,他们必须去找到所有依赖提交的工作,并将其更新为依赖于新提交。

换句话说,他们必须应对一个“上游变基”,或者实际上是很多次的上游变基。这通常不是一件有趣的事情(尽管使自动化这个过程成为可能的--fork-point代码也使他们能够自动恢复从上游变基中)。

--fork-point有一个时间限制,因为它使用reflog条目,并且reflog条目会过期。如果你没有重新配置这些内容,git会在30天后默认过期关键reflog条目,所以,如果你要做这个操作,其他人有大约一个月的时间来恢复。


很棒的答案。不过,如果能提供如何使用--fork-point的明确示例就更好了。 - ijoseph
1
@ijoseph: 我不太喜欢--fork-point。如果使用不当,我记得你可能会失去自己的提交记录:我记得我曾经想过这个问题并创建了一个测试案例,结果让我很烦恼。因此,我建议使用没有--fork-pointgit rebase -i,并从那里开始工作。 - torek

0
以这种方式,项目上的所有提交都是冲刺,而冲刺上的所有提交都是任务。
你不需要这样做。只需使用您创建的拉取请求,因为所有代码最终都将合并到develop中。
一旦您打开拉取请求,您还可以删除本地分支,并仅保留最新的分支以进行未来更改(如果需要)。

我不明白,我要把所有的东西都拉下来吗?我想让实际工作分支包含最新的主代码版本。 - Musical Shore
如果你从主分支中分离出去,想要再次获取主分支的所有最新更改,只需拉取更改即可。这不是你在问题中描述的内容。 - CodeWizard

0

在 Git 中不存在嵌套分支的概念。如果你有一个名为 foo 的分支,以及另一个名为 foo/bar 的分支,它们只是两个不同的分支。这些分支之间没有关系 - 将它们视为任何两个独立的分支。


它们在提交结构方面是嵌套的。我不想让develop分支上存在所有的提交记录。 - Musical Shore
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - eddiemoya
我希望将develop分支中的所有提交作为发布,将project分支中的所有提交作为迭代,将每个迭代分支中的所有提交作为与该迭代相关的任务。每个任务分支将有多个每日提交以保存代码。 - Musical Shore

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