使用 rebase 拉取特定的提交

Pull with rebase up to a specific commit

这是 的变体,带有 rebase 扭曲。

我想做一个 git pull --rebase 但只到特定的提交。这不是拉取特定的提交,而是拉取 upto 特定的提交。远程主机如下所示。

A<-B<-C<-D<-E<-F<-HEAD (Remote master HEAD)

假设我的本地特征分支 HEAD 指向 G,G 指向 D:

A<-B<-C<-D<-G<-HEAD (Current local feature branch HEAD).

我想通过变基提升到 E,这样我的分支最终看起来像:

A<-B<-C<-D<-E<-G<-HEAD (local feature branch end goal).

不过,这只是特例。我想选择任何符合条件的提交哈希,而不仅仅是上面示例中的倒数第二个。

当然,我希望提交 E 的散列在操作结束时与远程主机匹配。我强调这一点,因为某些类型的交互式 rebase 编辑会导致 属性 消失。

我该怎么办?

从远程获取更改:

git fetch origin

变基到 master 的远程版本,忽略一些提交:

git rebase origin/master~<n>

其中 <n> 是您要忽略的 master 顶端的提交数。

如果您有要变基的提交的 ID,则可以改用它:

git rebase <commit-id>

尝试交互式变基:

git rebase -i e3f8704

e3f8704 是您的提交哈希码。

TL;DR

是正确的:您必须将 git pull 分成两个独立的组成命令:git fetch 后跟 git rebase。不过,它可能值得更多解释。

让我重画一下你的图表,让它们看起来(我认为无论如何)更清晰一点。不过,我打算用更简单的连接破折号替换内部 backwards-pointing 箭头,因为我需要画一些 "arrows" 指向 up-and-left 或 down-and-left,而这不会在这里工作得不好。 (我的系统上有 arrow-drawing 个字符并不总是显示在其他系统的其他 Web 浏览器中。)

在遥控器上我们有:

A--B--C--D--E--F   <-- master

也就是说,有一个提交链,带有一些大而难看的哈希 ID,为方便起见,我们用字母替换了这些 ID,该链在提交 F 处结束。 他们的 Git 的名称master 包含提交F 的哈希ID。 (名称 master 可能是也可能不是远程上的当前 b运行ch:这对我们的目的无关紧要,因此我们不需要绘制特殊名称 HEAD 在这里。)

同时我们在本地有这个:

A--B--C--D--G   <-- feature (HEAD)

即,我们和他们通过 D 共享提交 A,包括他们的链接,我们的 Git 的名称 feature 包含提交的哈希 ID G 指向 D.

I want to pull up to E with a rebase so that my branch ends up looking like:

A--B--C--D--E--G   <-- feature (HEAD)

However, this is just a special case. I want to pick any eligible commit hash, not just the second to last one as in the example above.

你需要做的是避免 git pull 命令。

git pull所做的是运行两个more-basicGit命令:第一个git fetch,然后一个您选择的第二个命令。第二个命令通常是 git merge 但如果你使用 --rebase 或其他各种配置方法,你可以用 运行 git rebase 代替。你不能做的是将正确的参数传递给git rebase,其中"right arguments"我的意思是那些解决有问题。

我们先运行git fetch。这让我们的 Git 调用了他们的 Git。他们的 Git 告诉我们的 Git 它的各种 b运行ch 和标签以及其他类似的名称,包括他们的 master 标识提交 F 的事实。我们的 Git 检查存储库并发现我们 没有 提交 F,所以我们的 Git 要求获取它。他们的 Git 然后也提供提交 E — 发送者 必须 提供我们需要的一切,我们需要 E 来持有 F——而我们的 Git 也要求这样做。他们提供 D,但我们已经有了,所以我们告诉他们就此打住。

他们现在构建了一个 so-called thin pack,其中包含我们需要将提交 EF 添加到我们的存储库的内容。这是你在 运行 git fetch 或 运行s git fetch 的任何命令时看到的 "counting objects and "compressing objects" 内容。他们向我们发送了薄包;我们的 Git 接受了那个精简包并对其进行了修复,以便它可以使用,现在我们拥有了他们拥有的提交,而我们没有,我们需要完成 git fetch-ing.

如果我们不需要其他任何东西,我们的 Git 对他们的 Git 说 'kthanksbye 并继续更新我们的 remote-tracking 名字。在我们的 Git 中,我们有名字 origin/master 等等:这些是 我们的 Git 记得什么 他们的 Git 说 他们的 哈希 ID 是,对于 他们的 b运行ch 名字,上次我们谈过跟他们。只要我们有 他们 b运行 们记住的提交,我们的 Git 就可以相应地更新我们的 remote-tracking 名称,所以它确实如此。这给我们留下了:

A--B--C--D--G   <-- feature (HEAD)
          \
           E--F   <-- origin/master

(有那个 up-and-left 指向箭头,从 ED,没有使用箭头字符绘制。)

我们现在准备 运行 git rebase

如果我们 运行 git rebasegit pull 那样

如果我们 运行 git rebase origin/mastergit pull 实际上使用了 `git rebase 但这做同样的事情—Git 将:

  • 列出我们当前 HEAD 可访问的提交:GDC,等等;
  • 列出可从 origin/master 访问的提交:FEDC 等;
  • 从第一个列表中删除第二个列表中的任何内容;
  • 删除任何不应复制的额外提交;1
  • 以正确的顺序排列列表(与 Git 的内部反向顺序相反);和
  • 开始复制这些提交,一个接一个,就好像 git cherry-pick.2

由于此列表仅包含提交 G,这是我们将复制的一个提交。但是:这个副本去了哪里?

常规 git rebase 放置副本,或者如果有多个提交要复制,则复制复数,紧跟在命令行上指定的提交之后 。由于 git rebase as 运行 by git pull 名称提交 F——我们的 origin/master 指向的提交,在 git fetch 更新了我们的 origin/master 来匹配 originmaster——我们会得到:

A--B--C--D--G
          \
           E--F   <-- origin/master
               \
                G'

作为结果。 (我删除了一些名字,因为对于这个 m换句话说,这些名字并不是很有趣,git rebase 也会摆弄它们;我们只是还没有画那部分。)

如果有更多的提交,例如 G-H-I,我们最终会在此处的底行得到 G'-H'-I'。在所有情况下,原始 提交,pre-copying,仍然存在。此时,git rebase 通过 移动我们的 HEAD 所附加的名称 以指向最终复制的提交来完成其工作:3

A--B--C--D--G   [was `feature`, now abandoned]
          \
           E--F   <-- origin/master
               \
                G'   <-- feature (HEAD)

1根据 git rebase 的参数,这通常包括 所有合并提交 ,加上 git patch-id 计算相同的补丁 ID。 patch-ID 计算部分描述起来有点棘手:它涉及使用 git rev-list --left-right 和对称差分 triple-dot 运算符。对于许多变基,这些都不重要,这就是为什么我只有这个脚注。

2某些类型的 git rebase 字面上 运行 git cherry-pick。其他的——包括默认的和 git pull 的 运行——使用一种更快但更廉价的机制,涉及 git format-patchgit am,可能会错过重命名操作。您可以通过添加 -m 或进行交互式变基或添加其他选项来强制变基使用较慢但 more-accurate cherry-pick 的方法。不过很少有真正需要这样做的。

3从技术上讲,Git 运行 每个复制操作都有一个 分离的 HEAD,其中HEAD复制完成后直接指向副本。但是当然 git rebase 开始 通过保存附件的事实——HEAD 附加到 feature 的事实——这样当 rebase 完成时, Git 知道 (1) 移动 feature 和 (2) re-attach HEAD.


您想改为:指定副本的去向

如果您 运行 git rebase 自己, 可以选择 git rebase 调用 upstream。当 git pull 执行此操作时,它会向 git rebase 提供更新后的 origin/master 指向的哈希 ID。如果你这样做:

git rebase origin/master

你给它起名字 origin/master,它解析为相同的哈希 ID,你得到我们看到的结果。

但是如果你手动运行,可以输入哈希ID,或者任何其他命名你想要的承诺。这告诉 git rebase 副本去哪里

在这种情况下,那么,如果您以任何方式命名提交 E – 原始哈希 ID,或 origin/master^,或 origin/master~ 都可以工作 – 您的 git rebase 会将 G 复制到 E:

之后的 G'
A--B--C--D--G   [was `feature`, now abandoned]
          \
           E--F   <-- origin/master
            \
             G'   <-- feature (HEAD)

你得到了想要的结果。

现在还有一个控制旋钮供您使用

当您手动 运行 git rebase 时,无需 git pull 为您完成,您就多了一个选择。再次查看上面的 bullet-point 步骤列表,其中 git rebase 确定要复制的提交。如果你 运行:

git rebase <upstream>

Git 列出提交,如同通过:4

git log <upstream>..HEAD

(如the git rebase documentation所示;根据需要添加--fork-point,见脚注4)

然后它复制列出的提交,使用 upstream 作为复制的目标。但是如果你有,例如:

...--B--C--D--E--F   <-- branch (HEAD)
         \
          G--H--I   <-- origin/master

其中提交 D 是您所做的紧急 hack 修复,因此可以编写提交 EF,同时有人将真正的修复写为提交 G , H, and/or I?

虽然 git rebase 试图聪明地忽略已经在上游的提交——例如,如果提交 G 匹配提交 Dgit rebase 知道不copy D——这并不是在所有情况下都有效。特别是,它通常 不会 删除实际上只是禁用或删除功能的紧急情况 "fix",而不是真正修复功能中的错误。

您可以使用 git rebase -i 来处理这个问题,但在 git rebase -i 之前很久就有 git rebase --onto。使用 --onto,您可以从 upstream-limiting 参数中 拆分 target-selection。

也就是说,在此图中,我们想要的结果是仅复制 提交 EF,留下 D——我们的紧急修复不太正确——落后了。要告诉 Git 这个,我们使用 git rebase --onto:

git rebase --onto origin/master <hash-of-D>

或:

git rebase --onto origin/master branch~2

我们的 upstream 参数现在命名为提交 D。这是要复制的提交 not(也不是任何较早的提交)。

如果我们 运行 a git rebase 像这样但是没有 --onto 参数,Git (a) 不会复制 D 但是 (b ) 会将 EF 的副本放在 D 之后。结果是我们想要的(画出来看看)。但是当我们添加 --onto origin/master 时,它告诉 rebase 在提交 I 之后放置副本。结果是:

...--B--C--D--E--F   [abandoned]
         \
          G--H--I   <-- origin/master
                 \
                  E'-F'  <-- branch (HEAD)

提交 D-E-F 全部被删除,以支持新的和改进的 E'-F' 提交。我们不必手动删除 D,因为我们的 git rebase 参数为我们。用不可见的废弃提交重新绘制链给我们:

...--B--C--G--H--I   <-- origin/master
                  \
                   E'-F'  <-- branch (HEAD)

如果除了我们没有人知道提交 EF,我们可以假装 我们只写了新的副本,而不是比原来的:没有人(除了我们)会知道。5


4自己试试吧!您将获得一个哈希 ID 列表,每行一个。它们以 Git 的首选顺序出现——向后——这不适合 git rebase,并且它们 不会 忽略 git rebase 将省略。尽管如此,rebase 实际上 确实 在内部使用 git rev-list,只是它添加了很多选项:--no-merges 删除合并提交,--topo-order --reverse 到得到正确的顺序。最后,如脚注 1 中所述,排除与上游端的提交具有相同 patch-IDs 的提交有点神奇。这涉及使用 three-dot 语法,<upstream>...HEAD ,并添加 --right-only --cherry-pick。当 rebase 是一个 shell 脚本时,这很容易找到;现在已经用 C 代码重写了,更难理解了。

当 fork-point 选项生效时,此处的 <upstream> 参数将被 git merge-base --fork-point 的结果替换,它使用您的 origin/master reflog 来猜测是否某些应省略提交。参见,例如, and .

我仍然有点不相信 fork-point 模式是正确的默认模式(有时会令人惊讶)并且我不确定新的 Git 2.24 --keep-base 选项是否使用fork-point 类型合并库,或真正的合并库。但请注意,如果你使用任何不是 name 的变基——例如,如果你的变基 upstream 参数是一个哈希 ID —禁用 fork-point 模式,因为 fork-point 基数是通过扫描 reflog 计算的,只有 names 有 reflogs.

5我们可能会忘记。谁能记住原始哈希 ID?


结论

  • Git 实际上就是 提交 。 B运行ch 名称,当你使用它们时——以及当 Git 使用它们时——只是为了帮助你在某些 b[=525 中找到 last 提交=]通道。提交将永远冻结,并且(大部分)永久冻结(如果您无法 找到 它们,从 b运行ch 或其他名称,它们最终会消失)。

  • B运行ch names move。 B运行ch 名称让我们和 Git 找到提交。它们以可预测的方式移动,通过 添加 提交到 b运行ch。一些操作,例如 git rebasegit reset,以突然的方式移动它们,并且可能以 "less natural" 的方式移动它们,而不仅仅是前进以合并更多提交。

  • git rebase 是关于 复制 提交。我们制作 new-and-improved 个副本,使用不同的哈希 ID,并使 b运行ch 名称指向最后一个 copied 提交。原件无法更改,但如果您移动 b运行ch name,任何未保存原件哈希 ID 的人将无法 找到原件

  • git fetch 关于两件事:从另一个 Git 获取新提交和 更新 remote-tracking 名称如 origin/master 基于其他 Git 中的内容。如果我们 did 得到新的提交,此时我们只需要记住这些 remote-tracking 名称,所以我们通常需要第二个命令。

  • git pull 是为了方便:它 运行 是 git fetch,然后它 运行 是第二个命令,通常是 git merge.

  • 有时,这方便。实际上,我发现它带来的不便多于它带来的便利。在那种情况下,不要使用它