【问题标题】:Pull with rebase up to a specific commit使用 rebase 拉到特定的提交
【发布时间】:2020-05-18 00:10:09
【问题描述】:

这是 older question 的变体,带有变基。

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

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

假设我的本地特征分支 HEAD 指向 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 的哈希在操作结束时与远程主机匹配。我强调这一点,因为某些类型的交互式变基编辑会导致该属性消失。

我该怎么办?

【问题讨论】:

  • 在前两张图中,您说的是A<-B<-D,然后是A<-B<-C<-D。您是否只是忘记了C?或者你真的是这个意思?因为对于第二种情况,您保留 DE 的 ID 的目标无法实现:一旦 DC 之上重新建立,它的 ID 就会更改。
  • 我不小心编辑掉了 C。现在已修复。谢谢!

标签: git rebase


【解决方案1】:

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)

即,我们和他们共享提交 AD,包括他们的链接,并且我们的 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,其中包含我们将提交 EF 添加到我们的存储库所需的内容。这是您在运行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

(有那个向上和向左的箭头,从ED,不使用箭头字符绘制。)

我们现在准备运行git rebase

如果我们像 git pull 那样运行 git rebase 那样

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

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

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

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

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-patchgit 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 修复,以便可以编写提交 EF,同时有人将真正的修复写为提交 GH 和/或 @987654442 @?

虽然git rebase 试图聪明地忽略已经在上游的提交——例如,如果提交G 匹配提交Dgit rebase 知道不复制D——这在所有情况。特别是,通常不会放弃实际上只是禁用或消除了某个功能的紧急“修复”,而不是真正修复该功能中的错误。

您可以使用git rebase -i 来处理这个问题,但早在git rebase -i 之前,就有git rebase --onto。使用--onto,您可以从上游限制参数中拆分目标选择。

也就是说,在此图中,我们想要的结果是 复制提交EF,留下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) 会将 EF 的副本放在 @ 之后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)

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


4自己试试吧!您将获得一个哈希 ID 列表,每行一个。它们以 Git 的首选顺序出现——倒退——这不适合 git rebase,并且它们不会省略 git rebase 将省略的提交。尽管如此,rebase 实际上确实在内部使用git rev-list,只是它添加了很多选项:--no-merges 删除合并提交,--topo-order --reverse 获得正确的顺序。最后,排除与上游提交具有相同补丁 ID 的提交有一点魔力,如脚注 1 所述。这涉及使用三点语法&lt;upstream&gt;...HEAD,并添加--right-only --cherry-pick。当 rebase 是一个 shell 脚本时,这很容易找到;既然已经用 C 代码重写了,那就更难弄明白了。

当 fork-point 选项生效时,这里的&lt;upstream&gt; 参数被git merge-base --fork-point 的结果替换,它使用你的origin/master reflog 来猜测是否应该省略一些提交。参见,例如,Revision selection in git using fork-pointWhat 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 rebasegit reset)会以突然的方式移动它们,并且可能以“不太自然”的方式移动它们,而不仅仅是推进以合并更多的提交。

  • git rebase 是关于 复制 提交。我们使用不同的哈希 ID 制作新的和改进的副本,并让分支名称指向最后一次 复制 提交。原件无法更改,但如果您移动分支name,任何未保存原件哈希 ID 的人将无法找到原件。

  • git fetch 涉及两件事:从另一个 Git 获取新提交根据另一个 Git 中的内容更新远程跟踪名称,例如 origin/master时间>。如果我们确实获得了新的提交,此时我们只需要记住这些远程跟踪名称,因此我们通常需要第二个命令。

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

  • 有时,这很方便。实际上,我发现它不方便的时候比方便的多。在这种情况下,不要使用它

【讨论】:

    【解决方案2】:

    尝试交互式变基:

    git rebase -i e3f8704
    

    e3f8704 是您的提交哈希码。

    【讨论】:

    【解决方案3】:

    从远程获取更改:

    git fetch origin
    

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

    git rebase origin/master~<n>
    

    其中&lt;n&gt; 是您要忽略的master 尖端的提交数。

    如果你有你想要变基的提交的 id,你可以使用它来代替:

    git rebase <commit-id>
    

    【讨论】:

      猜你喜欢
      • 2015-10-06
      • 1970-01-01
      • 1970-01-01
      • 2022-10-20
      • 1970-01-01
      • 2017-11-04
      • 1970-01-01
      • 2016-11-22
      • 1970-01-01
      相关资源
      最近更新 更多