强制推送后重新设置基准

All*_*ght 9 git branch rebase

我有一个功能分支 A。然后我开始开发依赖于 A 的第二个功能,因此我将我的新功能分支 B 基于 A:

git checkout A
git checkout -B B
Run Code Online (Sandbox Code Playgroud)

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

有两种方法可以解决这个问题,但都不是很好:

使用 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>
Run Code Online (Sandbox Code Playgroud)

使用软复位:

git checkout B
git reset --soft HEAD~1
git stash
git reset --hard A
git stash pop
git commit -a -m "msg"
Run Code Online (Sandbox Code Playgroud)

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

tor*_*rek 26

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

\n\n

设置

\n\n

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

\n\n
...--A--B   <-- master\n         \\\n          C   <-- feature/A\n           \\\n            D   <-- feature/B\n
Run Code Online (Sandbox Code Playgroud)\n\n

也就是说,在某个主线上有一系列提交(我master在这里称之为它,但它可能是develop或其他什么),然后在您的 上进行一次提交feature/A,然后在您的feature/B. D在您的 上的提交的父级feature/B是您Cfeature/B和上的提交feature/A

\n\n

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

\n\n
...--A--B   <-- master\n         \\\n          C--E   <-- feature/A\n           \\\n            D   <-- feature/B\n
Run Code Online (Sandbox Code Playgroud)\n\n

最终,feature/Ais 被合并到master,并且根据某些策略规则,您已经做出了一个新的提交,它是和F的组合,因此您现在拥有:CE

\n\n
          F   <-- feature/A\n         /\n...--A--B   <-- master\n         \\\n          C--E   [abandoned]\n           \\\n            D   <-- feature/B\n
Run Code Online (Sandbox Code Playgroud)\n\n

此时,您想要复制D到一些新的提交,该提交在与父级的差异方面D\'看起来完全一样,但其中的父级而不是.DD\'FC

\n\n

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

\n\n
git checkout feature/B\ngit rebase --onto feature/A something-goes-here\n
Run Code Online (Sandbox Code Playgroud)\n\n

问题出在something-goes-here零件上。那里有什么?

\n\n

复制一些提交

\n\n

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

\n\n

也就是说,它比较要复制到该提交的父级的每个提交,以查看更改的内容。然后,它对当前提交进行相同的更改,如果一切顺利,则提交结果。

\n\n

例如,让我们从这种情况开始。saved-A我暂时使用了一个新标签 ,以记住 commit E,并且我添加了名称new-B并添加HEAD在括号中以显示当前分支new-B并且当前提交是 commit F

\n\n
          F   <-- feature/A, new-B (HEAD)\n         /\n...--A--B   <-- master\n         \\\n          C--E   <-- saved-A\n           \\\n            D   <-- feature/B\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
            D\'   <-- new-B (HEAD)\n           /\n          F   <-- feature/A\n         /\n...--A--B   <-- master\n         \\\n          C--E   <-- saved-A\n           \\\n            D   <-- feature/B\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
            D\'   <-- feature/B (HEAD)\n           /\n          F   <-- feature/A\n         /\n...--A--B   <-- master\n         \\\n          C--E   <-- saved-A\n           \\\n            D   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

同样,第一部分的作用就是git cherry-pick:复制一个提交。最后一部分的作用是git rebase:移动分支标签,例如feature/B.

\n\n

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

\n\n

git rebase简而言之,什么是

\n\n

让我们看一下稍微不同的图:

\n\n
...--A--B   <-- target\n      \\\n       C--D--E   <-- current (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

在这里,我们“在”分支上current,即,git status会说on branch current。提示commitcurrent是commit E: E\'s hash ID是name中存储的hash ID refs/heads/current

\n\n

如果我们现在运行:

\n\n
git rebase target\n
Run Code Online (Sandbox Code Playgroud)\n\n

Git 会将提交复制C-D-E到新提交C\'-D\'-E\'并将新提交放在顶部target,然后移动分支名称,如下所示:

\n\n
          C\'-D\'-E\'   <-- current (HEAD)\n         /\n...--A--B   <-- target\n      \\\n       C--D--E   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

答案是git rebase使用 Git 的内部“列出一些提交”操作,git rev-list并有一个停止点。rebase 文档声称git rebase运行的内容是:

\n\n
git rev-list target..HEAD\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

由于target名称为 commit B,这意味着:不要复制 commitB。好吧,我们已经不打算复制 commit 了B,所以没什么大不了的。但这意味着:不要复制 commitA。为什么不?因为 commitB指向 commit A。提交A在两个分支上,target并且current. 所以我们本来应该复制A,但我们没有复制,因为它在禁止复制列表中。之前 也有提交A,但它们都在不复制部分,所以没有一个被复制。

\n\n

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

\n\n

那么,git rebase简而言之,这是什么:

\n\n
    \n
  1. 记住HEAD附加到哪个分支。
  2. \n
  3. 列出一些要复制的提交哈希 ID。
  4. \n
  5. HEAD与当前分支分离。
  6. \n
  7. 复制列出的提交,一次一个,就像通过git cherry-pick.
  8. \n
  9. 附加的分支名称移动HEAD到我们现在所在的位置。
  10. \n
  11. 重新附加HEAD到移动的分支。
  12. \n
\n\n

请注意,在步骤 4 中,事情可能会出错。特别是,复制提交,就像通过git cherry-pick\xe2\x80\x94 一样,无论是否实际使用git cherry-pick\xe2\x80\x94 都可能会产生合并冲突。如果是这样,变基会在中间停止,并且 HEAD 分离。这就是为什么了解步骤 3 很重要。但我们将把它留给其他问答(以及有关 rebase 是否实际上确实使用了cherry-pick 本身的详细信息:有时确实如此,有时它会伪造它)。

\n\n

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

\n\n

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

\n\n
    \n
  • 首先,git rebase通常完全省略合并提交。git rev-list如果是合并(有两个或更多父级),则上述生成的任何提交都会被删除。只要列表中没有合并提交,这就无关紧要。

  • \n
  • 其次,git rebase 忽略补丁 ID与其他一些提交等效的提交。这就要用到git patch-id程序了。我们不会在这里详细介绍,除了观察到要获取“其他一些提交”部分,Git 实际上必须使用git rev-list target...HEAD, 和三个点。这会产生一个对称差异列表,其中包含可从目标可达但不可到达的提交HEAD,以及可从目标可达target但不可到达的提交HEAD。有关可达性的(更多)更多信息,请参阅Think Like (a) Git。然后,rebase 命令git patch-id在内部生成的两个列表\xe2\x80\x94 中的每个提交上使用,以便它知道哪个提交哈希与哪个列表\xe2\x80\x94 匹配,并剔除那些具有匹配补丁 ID 的列表。这样做的效果是,B例如,如果 commit已经与 commit 相同(择优选择)D,那么C-D-E我们只需复制C-E, 即可得到:

    \n\n
              C\'-E\'   <-- current (HEAD)\n         /\n...--A--B   <-- target\n      \\\n       C--D--E   [abandoned]\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    因为承诺BD“做同样的事情”。

  • \n
  • 最后,对我们来说最重要的是,--onto让我们使用不同的目标

  • \n
\n\n

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

\n\n
git rebase target\n
Run Code Online (Sandbox Code Playgroud)\n\n

既是target我们的停止参数git rev-list stop..HEAD 也是我们的目标,即 Git 放置副本的位置。但我们可以运行:

\n\n
git rebase --onto target stop\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在git rebase将使用我们的stop参数来表示stop的部分git rev-list,同时继续使用我们的target参数来表示副本所在的位置。

\n\n

所以,假设我们现在得到这个:

\n\n
...--A--B   <-- target\n      \\\n       C   <-- another\n        \\\n         D--E   <-- current (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们运行:

\n\n
git rebase --onto target another\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在我们已经告诉 Git我们的 rebase 的停止another参数是,它选择 commit C。我们的变基将使用git rev-liston another..HEAD, or C..E,这意味着要复制的提交列表将仅包含D-E.

\n\n

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

\n\n
          D\'-E\'  <-- current (HEAD)\n         /\n...--A--B   <-- target\n      \\\n       C   <-- another\n        \\\n         D--E   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

把它们放在一起

\n\n

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

\n\n
          F   <-- feature/A\n         /\n...--A--B   <-- master\n         \\\n          C--E   <-- saved-A\n           \\\n            D   <-- feature/B (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n
git rebase --onto feature/A saved-A\n
Run Code Online (Sandbox Code Playgroud)\n\n

Git 现在将列出要复制的提交:当前分支 上的每个提交,feature/B除了上的每个提交saved-A。这就是 commit D

\n\n

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

\n\n
            D\'  <-- feature/B (HEAD)\n           /\n          F   <-- feature/A\n         /\n...--A--B   <-- master\n         \\\n          C--E   <-- saved-A\n           \\\n            D   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n\n

这正是我们想要的。

\n\n

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

\n\n

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

\n\n

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

\n\n

幸运的是,您不必保存E或 的哈希 ID C。你可以:

\n\n
    \n
  • 使用git log, 或
  • \n
  • 用于git reflog查找feature/A用于命名的哈希 ID,或者
  • \n
  • 做任何你喜欢的事情来找到他们。
  • \n
\n\n

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

\n\n
git rebase --onto feature/A <hash-ID-of-E-or-C>\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n
git rebase --onto feature/A feature/A@{1}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

关键是找到您想要省略的提交,并将其与git rebase --onto. 根据副本应去的位置设置目标,并将停止点\xe2\x80\x94(文档中将上游参数称为上游参数\xe2\x80\x94)设置为哈希git rebaseID 以阻止您进行的提交想要复制。

\n\n

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

\n\n

如果您压缩的提交与原始提交具有相同的补丁 ID,git rebase则忽略具有匹配补丁 ID 的提交将为您完成所有工作。通常只有当您只有一次提交被压缩合并到其他分支时才会发生这种情况。

\n\n

这个--onto技巧总是有效的,所以你真的不需要担心这种情况,但如果这种情况经常发生,那么很高兴知道。

\n

  • 你让我今天一整天都感觉很好 !!!git rebase --ontobranch-you-wanna-rebase-from commit-id-from-where-in-your-current-branch-you-wanna-start (2认同)