【问题标题】:How Git select the rebase starting point?Git如何选择rebase起点?
【发布时间】:2016-12-05 17:58:12
【问题描述】:

git rebase 如何从源(通常是功能)分支中选择开始提交?

我猜 git 可以追溯到 src 和 dst 分支的共同祖先。

如果两个分支没有共同提交怎么办?

【问题讨论】:

  • 这取决于你传递给rebase的参数的refspec

标签: git rebase


【解决方案1】:

要知道的一件有用的事情——你可能已经知道了——是 rebase 通过 复制 提交来工作。它只复制正确的提交,使新副本在新基础结束后立即生效。

选择要变基(复制)的提交实际上使用了了解 Git 及其提交选择的最重要的事情之一。了解了这一点,也就了解了git loggit rev-list 的工作原理。1

首先,请记住 Git 的提交形成一个 图表(特别是 D 直接 A循环 G 图表或DAG,但您不必担心很长时间)。每个提交都会记住它的父级,或者对于合并提交,它的所有父级。当我们绘制的图形部分没有合并提交时,我们将得到一个 tree 结构而不是任意 DAG。当你没有合并时,rebase 效果最好,因为 rebase 通常会丢弃合并。

我们可以——而且你应该——画出这些图表。您可以让 Git 为您执行此操作,例如 git log --graph。它垂直绘制它们,这对我们的目的来说占用了太多空间,所以我们将它们水平绘制,右侧是较新的提交,左侧是较旧的提交。

这是一个示例图表:

...<- o <- o <- o <- o
            \
             o <- o <- o <- o

每个o 代表图中的一些提交。在正式的图论中,每个提交都是图中的一个 vertex,但有时这些被称为 nodes,我倾向于使用“nodes”和“commit nodes”这两个词"来描述它们。

每个提交节点的“真实名称”是一个 Git 哈希,是那些丑陋的 40 个字符 a234567... 的东西之一。给定一个 Git 哈希,Git 可以在存储库中查找任何对象(当然包括提交)。但不知何故,我们必须记住这些“真名”,而这些“真名”是完全无法记住的。

由于每个提交都会记住其父级,因此我们可以从任何提交开始并在历史中向后工作(但不能向前!)。我们需要记住一个分支的最近tip-most提交。我们让 Git 为我们执行此操作,让 Git 将丑陋的大哈希保存在 分支名称 中,例如 masterdevelop

您可以使用git rev-parse 将这样的名称转换为哈希:

$ git rev-parse master
08bb3500a2a718c3c78b0547c68601cafa7a8fd9

这意味着master 指向真实名称为08bb350... 的提交。该提交在其中包含先前提交的真实名称,等等。

让我们再次绘制该示例图,但这次添加分支名称。我也会让它更紧凑:我们知道提交总是指向“向后”(指向他们的父母)所以没有必要把它们画成箭头,我们可以使用连接线。这次我要用* 标记其中的两个提交:

...--*--*--o--o        <-- master
         \
          o--o--o--o   <-- develop

请注意,name master 特别选择了master 分支的tip。同样,名称develop 仅选择develop 分支的尖端。但是 Git 通常不只是选择 one 提交。通常,当我们告诉 Git 特别查看一个提交时,我们实际上是在要求 Git 考虑该提交它的所有父提交。

当我们从master 开始并向后工作时,我们会得到两个只在master 上的提交(提示,以及提示之前的一个),然后我们得到第二个* 提交,以及第一个*,以此类推。

当我们从develop 开始并向后工作时,我们会得到四个专门针对develop 的提交,然后是第二个* 提交,然后是第一个*,依此类推。

也就是说,两个* 提交实际上是在两个 分支上。

请注意,我们可以像这样轻松地绘制图形:

          o--o          <-- master
         /
...--*--*--o--o--o--o   <-- develop

或者像这样:

          o--o        <-- master
         /
...--*--*
         \
          o--o--o--o   <-- develop

所有这些图都代表同一张图,master 并没有什么特别之处。

这是rebase必须解决的问题的核心

如果我们想在master 上重新设置developgit rebase 必须以某种方式选择 onlydevelop 上的四个提交,同时排除所有 的提交也master

这就是 Git 的 X..Y 语法的用武之地。奇怪的是,rebase 不使用它!这是有原因的,但让我们暂时看一下语法。使用这种语法——在这种情况下,使用master..develop——我们要求 Git 从一个提示提交开始,develop 的提示,并选择每个可以追溯到过去的提交,一直到开始,它可以从那里;但也可以从master 的提示开始,un-选择每个返回时间的提交。

我喜欢将其视为临时绘画提交绿色(开始)和红色(停止)。我们可以先做绿色油漆,在develop 上绘制四个os 加上两个*s 加上它们之前的任何东西,然后从master 上的两个os 开始在顶部涂上红色油漆并继续到两个*s 以及之前的所有内容。或者,我们可以先画红色,然后再画绿色,但一旦找到红色节点就停止绘画。无论哪种方式,我们都将只得到四个独占develop 的提交“涂成绿色”。

这就是git rebase 知道复制这四个提交而不是任何其他提交的方式。

rebase 开始的地方通常是你当前的分支:

$ git branch
  diff-merge-base
  master
  precious
* stash-exp

(所以在这种情况下,我目前在stash-exp)。

rebase 复制的地方——或者更确切地说,“在之后复制”——来自git rebase 的参数:

$ git rebase master

事实证明,这也是git rebase 获得“红色提交”概念的地方(要复制的内容)。

Rebase 有效地接受您的论点,例如 master,以及您当前的分支名称(在我的例子中为 stash-exp,但假设为 develop)并使用 git rev-list2获取要复制的提交的 ID:

$ git rev-list master..develop

(当然,您必须在 rebase 之前运行此命令)。

额外的皱纹 #1

当您运行 git rebase 时,它会尝试检查 other 分支(您正在变基的分支)是否具有您拥有的提交的副本。也就是说,假设我们看我们画的版本图是这样的:

          o--o
         /
...--*--*
         \
          o--o--o--o

在这张图中,有两个来自最终共同 * 提交的分叉。我们可以轻松地将其中一个重新定位到另一个上。但是,如果顶线o 提交之一或多或少匹配底线o 提交之一怎么办?省略额外的会很好。让我们将底线重新定位到顶部,但让我们将这些提交标记为 ABCD,并注意在顶部,os 之一是 Just Like @987654396 @:

          o--B'
         /
...--*--*
         \
          A--B--C--D

(例如,当您使用cherry-pick 时,您会得到这种图表)。提交BB' 基本上是彼此的副本。因此,当我们对较低的四个提交进行 rebase 时,我们真的应该只复制 ACD,给出:

          o--B'
         /    \
...--*--*      A'-C'-D'
         \
          A--B--C--D

最后,让我们重新贴上标签。我们希望master 指向B',而develop 指向D',如下所示:

          o--B'          <-- master
         /    \
...--*--*      A'-C'-D'  <-- develop
         \
          A--B--C--D     [abandoned]

原来的A--B--C--D 链会发生什么变化?我们在这里将它标记为“已放弃”,但实际上,Git 会使用 reflog 机制在它上面挂起一段时间——例如,我们可以让 Git 找到 develop@{1},它会找到原始提交D——还有特殊名称ORIG_HEADrebase 设置为指向D。默认情况下,reflog 条目会保留 30 天,3 而名称 ORIG_HEAD 会一直保留,直到有东西(通常是另一个变基)覆盖它。

额外的皱纹 #2

有时,这一点 Git 魔法——使用像 master 这样的名称来“将提交涂成红色”,然后使用 相同的 名称来决定将副本放在哪里——是不够的。在某些情况下,您需要告诉git rebase 在某个特定点停止 复制,但将 副本放在其他地方。在这种情况下,您可以使用git rebase --onto

git rebase --onto <em>target</em> <em>upstream</em>

the rebase documentation 调用 red-paint “停止” 参数 upstream)。默认情况下 upstream 既是 --onto 目标 是停止复制红色油漆指示器,它在停止点在右侧时起作用放置在要复制到的点的历史记录中。这通常是正确的——而且经常(但并非总是),作为 upstream 为某些分支 foo 提供的东西是您将设置的 origin/foo 远程跟踪分支4 作为fooupstream,我认为这就是为什么rebase 调用这个参数upstream

如果两个分支没有共同提交怎么办?

如果两个分支没有共同提交怎么办?

在这种情况下,“绘制提交节点红色”步骤对“绘制提交绿色”步骤没有影响:

o--o--o--o   <-- master

o--o--o      <-- unrelated

如果您在分支unrelated 上运行git rebase master,Git 有效地将三个unrelated-branch 提交绘制为绿色,将四个master-branch 提交绘制为红色,然后采用绿色绘制的提交,这是从unrelated 的提示提交中可以访问的三个提交。然后,rebase 代码会复制这些提交:

o--o--o--o           <-- master
          \
           o--o--o   <-- unrelated

o--o--o              [abandoned]

1嗯,git rev-list 有大约一百万个标志,所以这有点夸大其词,因为它对所有标志都没有太大帮助。 :-)

2这里有一些副作用:有时git rebase 实际上直接使用git rev-list,而有时却没有。不过效果差不多。

3这是可配置的:gc.reflogExpiregc.reflogExpireUnreachable 控制默认值,您可以为特定模式设置其他名称。

4您可以使用git branch --set-upstream-to 显式设置它,但是对于这些类型的分支,它通常在您最初使用git checkout 创建分支时自动设置。设置完成后,git rebase 无需额外参数,也会自动找到它。

【讨论】:

  • 我在心里数着:“Torek in 3...2...1”+1
  • “rebase 通过复制提交起作用”:我想更准确的术语是“通过挑选提交”。
  • @VonC:嗯,有时它使用git chery-pick,有时它使用git format-patch | git am。两种方式的效果都是一样的,我喜欢define rebase 在cherry-pick方面。
  • 有关修订图及其修订规范(点语法)的更多信息,请参阅stackoverflow.com/a/850695/6309。 “修订规范包含一组正面引用(起点)和负面引用(停止点)和附加过滤器(限制修订数量、grep 提交文本等)。Git 从正面引用开始并回溯修订历史,当它遇到可从负面引用到达的修订时停止(不一定只是在它到达负面引用之一时)。”
  • 您在stackoverflow.com/a/36437843/6309 上谈到了 LCA,但从未提及那些正面和负面的参考。不过,您确实在stackoverflow.com/a/38649887/6309stackoverflow.com/a/34542654/6309 中提到了它们。我喜欢stackoverflow.com/a/24186641/6309 中的图形表示。
猜你喜欢
  • 2017-07-18
  • 1970-01-01
  • 2011-09-23
  • 1970-01-01
  • 1970-01-01
  • 2014-03-20
  • 2017-12-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多