强制推送后重新定位

Rebase after force push

我有一个特性分支 A。然后我开始开发依赖于 A 的第二个特性,所以我将我的新特性分支 B 建立在 A 上:

git checkout A
git checkout -B B

我在 B 上做了一些工作,所以现在在 B 上我有提交 1(来自 A)和新的提交 2。 我们公司总是尽可能地将一个 PR 的所有提交压缩在一起,所以有一次我强制推动 A,以便 A 只提交 1'。现在我想将 B 变基为 A(或 master,在 A 合并后),但由于我强制推送 A,git 尝试应用提交 1,这显然失败了。

2 种解决方法,但都不是很好:

使用git cherry-pick:

git checkout B
git checkout -B B2
git log // copy latest commit id
git checkout B
git reset --hard A
git cherry-pick <commit-id>

使用软重置:

git checkout B
git reset --soft HEAD~1
git stash
git reset --hard A
git stash pop
git commit -a -m "msg"

是否有 "git method" 来解决这个问题?我知道总是压缩提交可能不是最佳实践,但我无法改变。或者是否有更好的方法将一个分支建立在另一个分支之上?

最终,您会想要 git rebase --onto。不过,有时您不需要做任何特别的事情。

设置

让我们画出你的初始情况:

...--A--B   <-- master
         \
          C   <-- feature/A
           \
            D   <-- feature/B

也就是说,在一些主线上有一些提交(我在这里称之为 master 但它可能是 develop 或其他),然后在你的 feature/A,然后在 feature/B 上提交一个。在您的 feature/B 上提交 D 的父项是您在 feature/Bfeature/A 上的提交 C

稍后,您向 feature/A 添加了第二个提交,给出:

...--A--B   <-- master
         \
          C--E   <-- feature/A
           \
            D   <-- feature/B

最终,feature/A 将合并到 master,并且根据某些策略规则,您已经提交了一个新的提交 F,它是 [=39= 的组合] 和 E 所以你现在有:

          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   [abandoned]
           \
            D   <-- feature/B

此时你想将 D 复制到某个新的提交 D' 中,它看起来与 D 与其父项的差异完全相同,但是 D' 的父级是 F 而不是 C.

Git 提供了一种简单的方法来获得你想要的东西:

git checkout feature/B
git rebase --onto feature/A something-goes-here

问题出在 something-goes-here 部分。那里有什么?

正在复制一些提交

git rebase命令本质上只是一系列git cherry-pick命令,后面跟着一个b运行ch标签动作。正如您已经发现的那样,git cherry-pick 做您想要的:它复制提交。事实上,它可以复制多个提交(使用 Git 在内部调用 sequencer)。

也就是说,它将每个要复制的提交与提交的 parent 进行比较,以查看发生了什么变化。然后,它对 current 提交进行相同的更改,如果一切顺利,提交结果。

比如,我们先从这种情况说起。目前,我使用了一个新标签 saved-A,以记住提交 E,并且添加了名称 new-B 并在括号中添加了 HEAD显示 current b运行chnew-B 并且 current commit 是 commit F:

          F   <-- feature/A, new-B (HEAD)
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   <-- feature/B

我们现在可以 运行 git cherry-pick feature/B。我们告诉 Git:将提交 D 与其父 C 进行比较,然后在提交 F 处对我们现在所在的位置进行相同的更改,并提交结果。 如果一切顺利,我们得到:

            D'   <-- new-B (HEAD)
           /
          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   <-- feature/B

我们现在需要做的就是将名称 feature/B 拉到指向提交 D',然后删除名称 new-B:

            D'   <-- feature/B (HEAD)
           /
          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   [abandoned]

同样,第一部分正是 git cherry-pick 所做的:复制一个提交。 last 部分是 git rebase 所做的:移动 b运行ch 标签,如 feature/B.

这里的关键是 git rebase 复制了 一些 提交。 哪些?默认答案对你来说是错误的答案!

git rebase 的作用,简而言之

让我们看一张略有不同的图:

...--A--B   <-- target
      \
       C--D--E   <-- current (HEAD)

这里,我们是"on"b运行chcurrent,即git status会说on branch currentcurrent 的tip commit 是commit E: E 的hash ID 是name refs/heads/current.

中存储的hash ID

如果我们现在运行:

git rebase target

Git 将 复制 提交 C-D-E 到新提交 C'-D'-E' 并将新提交放在 target 之上,然后移动b运行ch 名称,像这样:

          C'-D'-E'   <-- current (HEAD)
         /
...--A--B   <-- target
      \
       C--D--E   [abandoned]

这通常是我们想要的。但是:git rebase怎么知道复制C-D-E却不知道复制A呢?

答案是git rebase使用Git的内部"list some commits"操作,git rev-list,有一个停止点 . rebase 文档声称 git rebase 所做的是 运行:

git rev-list target..HEAD

这是一个善意的谎言:它足够接近,并且具有说明性。确切的细节比较棘手,我们稍后会讲到。现在,让我们看看 target..HEADtarget.. 部分。这告诉 Git:不要列出您可以通过从目标开始并向后工作找到的任何提交。

因为target命名提交B,这意味着:不要复制提交B。好吧,我们已经不打算复制提交 B,所以没什么大不了的。但它意味着:不要复制提交A。为什么不?因为提交 B 指向回提交 A。提交 A 在 b运行ches,targetcurrent 上。所以我们复制A,但我们没有,因为它在不复制列表中。 before A 也有提交,但它们都在 不要复制 部分,所以 none被复制。

因此它的提交 C-D-E 被复制到这里:它们在要复制的列表中,并且不会因为在不复制列表中而停止。

所以,git rebase 的作用,简而言之,是这样的:

  1. 记住 b运行ch HEAD 附加到哪个。
  2. 列出一些要复制的提交哈希 ID。
  3. 从当前 b运行ch 中分离 HEAD
  4. 复制列出的提交,一次一个,如同 git cherry-pick
  5. 移动 HEAD 附加的 b运行ch 名称,到我们现在所在的位置。
  6. 重新附加 HEAD 到移动的 b运行ch。

请注意,在第 4 步中可能会出错。特别是,复制提交,就像 git cherry-pick 一样——无论是否实际使用 git cherry-pick——都可能有一个 合并冲突。如果是这样,rebase 会在中间停止,并带有一个分离的 HEAD。这就是了解第 3 步很重要的原因。但我们会把它留给其他问题和答案(以及关于 rebase 是否真的使用 cherry-pick 本身的细节:有时它使用,有时它伪造它)。

关于哪些提交被复制的真相

我们提到上面的 target..HEAD 是一个善意的谎言:一种简化,旨在更容易理解哪些提交被复制。现在是真相的时候了。

  • 首先,git rebase 通常 完全忽略 合并提交。如果是合并(有两个或多个父项),则上面的 git rev-list 生成的任何提交都会被淘汰。只要您的列表中没有合并提交,这就没有关系。

  • 其次,git rebase 省略了 补丁 ID 等效于 的提交.这使用了 git patch-id 程序。这里就不细说了,只是观察得到"some other commits"部分,Git其实要用git rev-list target...HEAD,三个点。这会产生一个 对称差异 列表,其中的提交可以从 HEAD 到达但不是目标,并且还可以从 target 而不是 HEAD 到达。有关可达性的(更多)信息,请参阅 Think Like (a) Git。然后,rebase 命令对两个列表中的每个提交使用 git patch-id(它是在内部生成的,因此它知道哪个提交哈希与哪个列表对应),并剔除那些具有匹配补丁 ID 的提交。这样做的效果是,如果提交 B,例如 已经 与提交 D 相同(精挑细选),而不是复制 C-D-E,我们只需复制 C-E,得到:

              C'-E'   <-- current (HEAD)
             /
    ...--A--B   <-- target
          \
           C--D--E   [abandoned]
    

    因为提交 BD "do the same thing"。

  • 最后,也是对我们来说最重要的,--onto 让我们可以使用不同的 target

在上面的例子中,我们运行:

git rebase target

target 都是我们的 停止参数 对于 git rev-list stop..HEAD 我们的目标,因为 Git放份。但是我们可以 运行:

git rebase --onto target stop

现在 git rebase 将对 git rev-liststop 部分使用我们的 stop 参数,同时继续使用我们的 目标 副本去向的参数。

所以,假设我们现在 this:

...--A--B   <-- target
      \
       C   <-- another
        \
         D--E   <-- current (HEAD)

我们运行:

git rebase --onto target another

我们现在已经告诉 Git 我们的 rebase 的 stop 参数是 another,它选择提交 C。我们的 rebase 将在 another..HEADC..E 上使用 git rev-list,这意味着要复制的提交列表将仅包含 D-E.

该列表将通过 patch-id 和 no-merges 规则进一步过滤,但只要 B 不同 [=37] =],我们最终会得到:

          D'-E'  <-- current (HEAD)
         /
...--A--B   <-- target
      \
       C   <-- another
        \
         D--E   [abandoned]

也就是说,我们将只复制 可以从 current 访问的两个提交 D-E,省略提交 C可从 another.

访问

综合起来

这是您在进行提交复制时的设置:

          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   <-- feature/B (HEAD)

请注意,我们添加了名称 saved-A 以记住要复制的内容 而不是 。我们不想复制提交 CE。无论如何我们都不会复制 E,但这是记住 所有内容 不要复制的简单方法。

我们目前已经 feature/B 签出(提交 D)。我们不需要创建名称 new-B,因此我们没有这样做。现在我们只是 运行:

git rebase --onto feature/A saved-A

Git 现在将列出要复制的提交:当前 b运行ch、feature/B 除了 [=373= 之外的每个提交] saved-A 上的每个提交。这就是提交 D.

Git 现在分离 HEAD,移动到提交 F——我们的 --onto 目标——并复制 D 以产生 D'。这完成了要复制的提交列表,因此成功将 D 复制到 D',Git 强制移动名称 feature/B 指向 D' 并重新附加 HEAD,给我们:

            D'  <-- feature/B (HEAD)
           /
          F   <-- feature/A
         /
...--A--B   <-- master
         \
          C--E   <-- saved-A
           \
            D   [abandoned]

这正是我们想要的。

我们现在可以删除名称 saved-A

如果你没有保存名字怎么办?

如果您已经变基 feature/A 但忘记将提交 E 的提交哈希 ID 保存在某处怎么办?

幸运的是,您没有保存EC的散列ID。您可以:

  • 使用 git log
  • 找到它们
  • 使用git reflog查找feature/A用于命名的散列ID,或
  • 做任何你想做的事来找到他们。

原始哈希 ID 有效,因此您只需 运行:

git rebase --onto feature/A <hash-ID-of-E-or-C>

找到哈希 ID 后。 (使用剪切和粘贴或类似方法获得正确的散列 ID;手动输入它,甚至是它的唯一前缀,很容易出错。)

Reflog 名称也有效,因此您通常可以这样做:

git rebase --onto feature/A feature/A@{1}

其中 feature/A@{1} 是当您 运行 git reflog feature/A 列出以前的哈希 ID 时,您将看到的提交 E 的哈希 ID 的引用日志名称feature/A。 (feature/A@{2} 可能命名为 commit C,这样也可以。)

关键是找到您要省略的提交,并使用带有 git rebase --onto 的提交。根据副本的去向设置 target,并设置停止点——the git rebase documentation 调用 upstream 参数— 一个散列 ID 停止提交你想要复制。

什么时候不需要特别的东西?

如果压缩后的提交与原始提交具有相同的补丁 ID,git rebase 将删除具有匹配补丁 ID 的提交 将完成所有工作你。这通常只会发生在你只有 one 提交被挤压合并到其他 b运行ch.

--onto 技巧总能奏效,所以您真的不必担心这种情况,但如果这种情况经常发生,很高兴知道。