TL;DR
Calum Halpin's answer 是正确的:您必须将您的 git pull 分解为两个独立的组成命令:git fetch,然后是 git rebase。不过,它可能值得更多解释。
长
让我重新绘制一下你的图表,让它们看起来(无论如何我认为)更清晰一些。不过,我将用更简单的连接破折号替换内部的后向箭头,因为我需要绘制一些指向左上或左下的“箭头”,这不太好用这里。 (我的系统上的箭头字符并不总是显示在其他系统上的其他 Web 浏览器中。)
在遥控器上:
A--B--C--D--E--F <-- master
也就是说,有一个提交链,为了方便,我们用字母替换了一些大而丑陋的哈希 ID,以提交 F 结束。 他们的 Git 的名称 master 包含提交 F 的哈希 ID。 (名称master可能是也可能不是遥控器上的当前分支:这对我们的目的来说并不重要,所以我们不需要在这里画出特殊的名称HEAD。)
同时我们在本地有这个:
A--B--C--D--G <-- feature (HEAD)
即,我们和他们共享提交 A 到 D,包括他们的链接,并且我们的 Git 名称 feature 包含提交 G 的哈希 ID,它指向 D。
我想用变基拉到 E,这样我的分支最终看起来像:
A--B--C--D--E--G <-- feature (HEAD)
但是,这只是一个特例。我想选择任何符合条件的提交哈希,而不仅仅是上面示例中的倒数第二个。
您需要做的是避免git pull 命令。
git pull 所做的是运行 两个 更基本的 Git 命令:首先是 git fetch,然后是您选择的第二个命令。第二个命令通常是git merge,但如果你使用--rebase,或者其他各种配置方法,你可以让它运行git rebase。您不能做的是将正确的论点传递给git rebase,其中“正确的论点”是指解决问题的那些。
让我们先运行git fetch。这让我们的 Git 调用了他们的 Git。他们的 Git 告诉我们的 Git 它的各种分支和标签以及其他此类名称,包括他们的 master 标识提交 F 的事实。我们的 Git 检查存储库并发现我们没有提交 F,因此我们的 Git 要求获取它。然后,他们的 Git 也提供提交 E — 发送者 必须 提供我们需要的一切,我们需要 E 来保存 F — 我们的 Git 也要求这样做。他们提供D,但我们已经有了,所以我们告诉他们到此为止。
他们现在构建了一个所谓的 thin pack,其中包含我们将提交 E 和 F 添加到我们的存储库所需的内容。这是您在运行git fetch 或任何运行git fetch 的命令时看到的“计数对象和“压缩对象”内容。他们将精简包发送给我们;我们的Git 接受该精简包并对其进行修复,以便可用,现在我们有了他们的提交,我们没有,我们需要完成git fetch-ing。
如果我们不需要其他任何东西,我们的 Git 会向他们的 Git 说'kthanksbye,然后继续更新我们的远程跟踪名称。我们在我们的 Git 中有名称 origin/master 等等:这些是 我们的 Git 记住 他们的 Git 所说的 他们的 哈希 ID 的地方,对于 他们的 分支名称,我们上次与他们交谈时。只要我们有 他们的 分支记住的提交,我们的 Git 就可以相应地更新我们的远程跟踪名称,它确实如此。这给我们留下了:
A--B--C--D--G <-- feature (HEAD)
\
E--F <-- origin/master
(有那个向上和向左的箭头,从E 到D,不使用箭头字符绘制。)
我们现在准备运行git rebase。
如果我们像 git pull 那样运行 git rebase 那样
如果我们运行 git rebase origin/master—git pull 实际上使用 `git rebase 但这做同样的事情—Git 会:
- 列出可以从我们当前的
HEAD 访问的提交:G、D、C 等等;
- 列出可从
origin/master 访问的提交:F、E、D、C 等;
- 从第一个列表中删除第二个列表中的任何内容;
- 删除任何不应复制的其他提交;1
- 将列表按正确的顺序排列(与 Git 的内部倒序相反);和
- 开始一个一个地复制这些提交,就像
git cherry-pick.2
由于此列表仅包含提交 G,因此这是我们将复制的一个提交。但是:这个副本去哪里了?
一个普通的git rebase 放置副本,或者如果有多个提交要复制,则复制复数,就在命令行上命名的提交之后。由于git rebase 由git pull 运行,名称提交F——我们的origin/master 指向的提交,在git fetch 更新了我们的origin/master 以匹配origin 的master——我们会得到:
A--B--C--D--G
\
E--F <-- origin/master
\
G'
结果。 (我已经删除了一些名称,因为目前这些名称并不是很有趣,git rebase 也会摆弄它们;我们只是还没有画出那部分。)
如果有更多的提交,例如G-H-I,我们最终会在最后一行出现G'-H'-I'。在所有情况下,原始 提交,预复制,仍然存在。此时,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 的任何提交。补丁 ID 计算部分描述起来有点棘手:它涉及使用 git rev-list --left-right 和对称差分三点运算符。对于许多变基,这些都不重要,这就是为什么我只有这个脚注。
2某些类型的git rebase 字面上运行git cherry-pick。其他的——包括默认的和git pull 运行的——使用更快但更简单的机制,涉及git format-patch 和git am,可能会错过重命名操作。您可以通过添加 -m 或执行交互式 rebase 或添加其他选项来强制 rebase 使用更慢但更准确的 cherry-pick 方法。但很少有真正需要这样做。
3从技术上讲,Git 使用 分离的 HEAD 运行每个复制操作,其中HEAD 在复制完成后直接指向副本。但当然git rebase 开始 通过保存附件的事实——HEAD 被附加到feature 的事实——所以当 rebase 完成时,Git 知道 (1) 移动 @ 987654410@ 和 (2) 重新附加HEAD。
您想要做什么:指定副本的去向
如果你自己运行git rebase,你可以选择git rebase调用上游的参数。当git pull 执行此操作时,它会为git rebase 提供更新后的origin/master 指向的哈希ID。如果你这样做:
git rebase origin/master
您将其命名为 origin/master,它会解析为相同的哈希 ID,然后您就会得到我们看到的结果。
但是如果你做手动运行它,你可以输入哈希ID,或者任何其他命名你想要的提交的东西。这告诉git rebase 副本去哪里。
在这种情况下,如果你以任何方式命名 commit E - 原始哈希 ID、origin/master^ 或 origin/master~ 都可以使用 - 你的 git rebase 将复制 G 到 @987654425 @ 在E 之后出现:
A--B--C--D--G [was `feature`, now abandoned]
\
E--F <-- origin/master
\
G' <-- feature (HEAD)
你会得到想要的结果。
现在还有一个控制旋钮可供您使用
当您手动运行 git rebase,而不是让 git pull 为您运行时,您可以获得更多选择。再次查看上述步骤的要点列表,其中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
commit D 是您进行的紧急 hack 修复,以便可以编写提交 E 和 F,同时有人将真正的修复写为提交 G、H 和/或 @987654442 @?
虽然git rebase 试图聪明地忽略已经在上游的提交——例如,如果提交G 匹配提交D,git rebase 知道不复制D——这在所有情况。特别是,通常不会放弃实际上只是禁用或消除了某个功能的紧急“修复”,而不是真正修复该功能中的错误。
您可以使用git rebase -i 来处理这个问题,但早在git rebase -i 之前,就有git rebase --onto。使用--onto,您可以从上游限制参数中拆分目标选择。
也就是说,在此图中,我们想要的结果是仅 复制提交E 和F,留下D——我们的紧急修复并不真正正确-在后面。为了告诉 Git,我们使用git rebase --onto:
git rebase --onto origin/master <hash-of-D>
或:
git rebase --onto origin/master branch~2
我们的 upstream 参数现在命名为 commit D。这是要复制的提交不是(也不是任何早期的提交)。
如果我们像这样运行 git rebase 但没有 --onto 参数,Git (a) 不会复制 D 但 (b) 会将 E 和 F 的副本放在 @ 之后987654465@。结果是我们不想要的(画出来看看)。但是当我们添加--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)
如果除了我们之外没有人知道提交 E 和 F,我们可以假装我们只编写了新副本,而不是原件:没有人(除了我们) 会知道的。5
4自己试试吧!您将获得一个哈希 ID 列表,每行一个。它们以 Git 的首选顺序出现——倒退——这不适合 git rebase,并且它们不会省略 git rebase 将省略的提交。尽管如此,rebase 实际上确实在内部使用git rev-list,只是它添加了很多选项:--no-merges 删除合并提交,--topo-order --reverse 获得正确的顺序。最后,排除与上游提交具有相同补丁 ID 的提交有一点魔力,如脚注 1 所述。这涉及使用三点语法<upstream>...HEAD,并添加--right-only --cherry-pick。当 rebase 是一个 shell 脚本时,这很容易找到;既然已经用 C 代码重写了,那就更难弄明白了。
当 fork-point 选项生效时,这里的<upstream> 参数被git merge-base --fork-point 的结果替换,它使用你的origin/master reflog 来猜测是否应该省略一些提交。参见,例如,Revision selection in git using fork-point 和 What does `git rebase --fork-point master` mean?。
我仍然有点不相信叉点模式是正确的默认值(有时可能会令人惊讶)并且我还不确定新的 Git 2.24 --keep-base 选项是使用叉点类型合并基础,还是真正的合并基地。但请注意,如果您使用非 name 的任何内容进行变基(例如,如果您的变基 upstream 参数是哈希 ID),则会禁用分叉点模式,因为叉点基数是通过扫描 reflog 来计算的,并且只有 names 有 reflog。
5我们可能会忘记。谁能记住原始哈希 ID?
结论
Git 真的是关于提交。分支名称,当你使用它们时——以及 Git 何时使用它们——只是为了帮助你在某个分支中找到 last 提交。提交被永久冻结,并且(大部分)是永久的(如果您无法找到它们,从分支或其他名称,它们最终会消失)。
分支名称移动。分支名称让我们和 Git 可以找到提交。通过向分支添加提交,它们以可预测的方式移动。某些操作(例如 git rebase 或 git reset)会以突然的方式移动它们,并且可能以“不太自然”的方式移动它们,而不仅仅是推进以合并更多的提交。
git rebase 是关于 复制 提交。我们使用不同的哈希 ID 制作新的和改进的副本,并让分支名称指向最后一次 复制 提交。原件无法更改,但如果您移动分支name,任何未保存原件哈希 ID 的人将无法找到原件。
git fetch 涉及两件事:从另一个 Git 获取新提交和根据另一个 Git 中的内容更新远程跟踪名称,例如 origin/master时间>。如果我们确实获得了新的提交,此时我们只需要记住这些远程跟踪名称,因此我们通常需要第二个命令。
git pull 是为了方便:它运行git fetch,然后运行第二个命令,通常是git merge。
有时,这不很方便。实际上,我发现它不方便的时候比方便的多。在这种情况下,不要使用它。