首页 > 解决方案 > 在“挤压和合并”之后,我的分支机构出现了分歧。我该如何解决?

问题描述

我做了一个 'squash and merge' 来关闭一个 PR。虽然现在所有的变化都出现在 中master,但它仍然说分支已经分道扬镳,前后仍有变化。

This branch is 1 commit ahead, 28 commits behind develop. 

我该如何解决?后面的 28 个提交是在“压缩和合并”期间被压缩的那些。我已经通过https://help.github.com/en/github/administering-a-repository/about-merge-methods-on-github#squashing-your-merge-commits,但找不到任何连接。

我假设(错误地?)“挤压和合并”会进行快进合并以防止分歧。我不希望所有的提交都从develop移动到master,这就是为什么我做了一个“压缩和合并”,而不是变基。我想我错过了一些关于壁球和合并的东西。

我在 SO 上遇到过类似的问题,但最终建议在合并到master. 但我不想失去历史develop

标签: gitgithubmergepull-requestsquash

解决方案


我该如何解决?

它只是一种可修复的。您通常应该现在完全删除分支。然后,您可以创建一个分支,甚至是仍然命名为develop. 然后它是一个不同的分支,即使它的名称相同。

这一系列命令可能会修复它,但要非常小心并确保您理解每个命令:

git checkout master
git reset --hard develop
git push --force-with-lease origin master

git reset --hard命令将丢弃任何未提交的工作,因此在使用此命令时要格外小心。

我将在下面给出一些替代方案。有许多选项,具有不同的效果,尤其是对于可能正在使用 GitHub 存储库的任何其他人。如果是唯一使用 GitHub 存储库的人,您可以做任何您想做的事情。

无论如何,请记住:这是 squash-and-merge 的目标:杀死分支。这个想法是,在这个 squash-and-merge 之后,您将删除分支名称并永远放弃所有 28 个提交。squash-and-merge 做了一个的提交,其中包含与之前的 28 个提交相同的工作,所以一个新的提交是对这 28 个提交的新的和改进的替换

要了解您在做什么,请继续阅读。

要真正“获得”Git,您需要绘制图表

我假设(错误地?)“挤压和合并”会进行快进合并以防止分歧。我不希望所有的提交都从develop移动到master,这就是为什么我做了一个“压缩和合并”,而不是变基。我想我错过了一些关于壁球和合并的东西。

是的。GitHub 在这里也抛出了一个额外的皱纹——或者也许,会消除你想要的皱纹,这取决于你如何看待它。我们首先需要一点点。

Git 是关于提交的。这些提交形成一个,特别是一个Directed A循环DAG它的工作方式实际上非常简单:

  • 每个提交都有一个唯一的哈希 ID。实际上,此哈希 ID 是提交的真实名称。

  • 每个提交都由两部分组成:它的数据——你所有文件的快照——和一些元数据,关于提交本身的信息,比如谁做了它(你的名字和电子邮件地址),什么时候(日期和时间-邮票),以及为什么(您的日志消息)。大多数元数据只是为了在您查看提交时向您展示,但有一条信息对 Git 本身至关重要:每个提交都列出了其父提交的原始哈希 ID。

大多数提交只有一个单亲。当某物包含提交的哈希 ID 时,我们说某物指向该提交。因此,提交指向其父级。如果我们有一个连续的提交链,我们可以像这样绘制它们:

... <-F <-G <-H

其中大写字母代表实际的哈希 ID。最新的提交是 commit H。它指向它的父级,它就在它之前: commit G。提交G指向其父级的点F,依此类推。

在Git 中,分支名称只是一个指向最新提交的名称:

...--G--H   <-- master

当我们创建一个新分支时,我们真正在做的是告诉 Git:创建一个指向某个现有提交的新名称。因此,如果我们现在创建一个分支develop,我们会得到:

...--G--H   <-- develop, master

现在我们需要知道我们实际使用的名称HEAD,因此我们将特殊名称附加到该名称:

...--G--H   <-- develop (HEAD), master

当我们进行新的提交时,我们只需让 Git 写出提交——这将计算新提交的唯一哈希 ID——带有一个快照并将其父级设置为当前提交(H目前):

...--G--H
         \
          I

现在名称的变化也很简单:HEAD附加的名称,Git 将新提交的哈希 ID 写入那里。所有其他的:什么都没有发生。所以我们的新图是:

...--G--H   <-- master
         \
          I   <-- develop (HEAD)

随着我们添加更多提交,它们只会不断增长分支:

...--G--H   <-- master
         \
          I--J--K   <-- develop (HEAD)

(这里develop3 aheadmaster

还有另一件关键的事情要实现:提交GH,以及在那些 on 之前的所有那些,master也是 on develop 换句话说,提交通常在多个分支上。(这对 Git 来说很奇怪:许多其他版本控制系统都不是这样工作的。)

真正的合并和快进

此时,我们可以git merge使用以下命令在我们自己的存储库(但不在 GitHub 上)本地运行:

git checkout master; git merge develop

Git 会走捷径——你提到的快进。我们稍后再谈。

或者,我们可以运行git merge --no-ff develop,强制 Git 进行真正的合并。 这就是 GitHub 上的“合并”按钮会做的事情,让我们来看看。 “真正的”合并总是会产生一个新的合并提交。如果两个分支已经分歧,则需要真正的合并:例如,如果我们有:

       o--o   <-- branch1
      /
...--o
      \
       o--o   <-- branch2

我们想合并它们,我们将被迫使用真正的合并。那不是我们所拥有的;我们有:

...--G--H   <-- master (HEAD)
         \
          I--J--K   <-- develop

(请记住,我们现在master正在进行:我们做了一个git checkout master所以H当前提交。 K是最后一次提交develop。)

GitHub 在所有情况下都强制进行真正的合并,包括我们自己的简单合并。要进行真正的合并,Git 会找到一个合并基础提交(在本例中为 commit)H,并将该合并基础比较两次,一次与当前提交H,一次与 commit K

显然, commitH匹配 commit H。所以我们这边没有任何改变可以继续推进。我们只想要他们所做的任何更改,从HK。结果将与 中的快照相同K。但是我们正在强制合并,所以 Git 进行了新的合并提交,我将要求这样Mmerge

...--G--H---------M   <-- master (HEAD)
         \       /
          I--J--K   <-- develop

让它变得特别的东西M——使它成为一个合并——是它不仅有一个父级,而且有两个。它的第一个父级是 commit H,这是我们刚才所处的位置。它的第二个父级是 commit K。它的快照是我们在H-vs-上所做的H工作(根本没有工作)与他们在H-vs-上所做的工作相结合的结果K,因此新的合并提交的快照与提交的快照匹配K,但最终结果是这个新的合并提交。

如果我们允许 Git 进行快进而不是 merging,我们会得到:

...--G--H
         \
          I--J--K   <-- develop, master (HEAD)

(这使我们可以消除绘图中的扭结,尽管我没有打扰)。Git 将此称为快进合并,即使没有实际发生合并。不幸的是,GitHub 的按钮不允许我们进行快进合并

壁球合并是真正的合并,但没有第二个父级

如果我们进行真正的合并,我们会得到这个图表:

...--G--H---------M   <-- master (HEAD)
         \       /
          I--J--K   <-- develop

提交M将具有与提交相同的快照,K并且将有两个父级,H并且K.

做一个壁球合并,我们得到这个图:

...--G--H---------S   <-- master (HEAD)
         \
          I--J--K   <-- develop

CommitS与 commit 具有相同的快照K,但只有一个父级,H。这里的想法是您想要一个与您所做的所有提交具有相同效果的普通提交develop。这个单一的提交是完成这项工作的新的和改进的方式。旧的方式——旧的I-J-K提交——不再有用,应该被抛弃;最终,在 30 天或更长时间后,您的 Git 会真正删除它们。所以你只需运行name develop的强制删除,产生:

...--G--H---------S   <-- master (HEAD)
         \
          I--J--K   [abandoned]

如果您愿意,您现在可以再次创建名称 develop,但这次指向 commit S

...--G--H--S   <-- develop, master (HEAD)
         \
          I--J--K   [abandoned]

分支名称的目的是让您(和 Git)找到提交

请注意,当我们操作图表以添加提交时,我们会移动分支名称。分支名称总是指向最新的提交。这就是 Git 定义工作的方式。

但是,我们可以强制分支名称移动到我们喜欢的任何地方。如果我们有一个特定的分支检出,就像在这个状态:

...--G--H--S   <-- master (HEAD)
         \
          I--J--K   <-- develop

我们可以使用git reset --hard它来切换提交,并且可以将提交推S到一边:

          S   [abandoned]
         /
...--G--H   <-- master (HEAD)
         \
          I--J--K   <-- develop

请注意,提交S仍然存在。只是从 name 开始master,我们找到 commit H,然后我们向后工作并找到 commit G,依此类推。我们从不前进,所以我们无法承诺S。它已被放弃,就像I-J-K我们删除 name 时提交一样develop

如果您将提交的原始哈希 ID 保存在某处——例如,将其写在纸上或白板上,或者将其剪切并粘贴到文件中——您可以稍后直接使用该哈希 ID 让 Git 找到该提交. 如果它仍在存储库中(默认情况下至少会保留 30 天),您可以通过这种方式找到它。但是您不会通过大多数普通的git log和其他 Git 命令找到它。

请注意,如果提交在多个分支上——例如,如果我们有:

...--P   <-- branch1
      \
       Q   <-- branch2
        \
         R   <-- branch3

我们删除了这个名字branch2——它可能仍然可以找到。删除名称 branch2的结果是:

...--P   <-- branch1
      \
       Q--R   <-- branch3

毕竟。无论哪种方式,提交Q仍然存在,但这一次,我们可以R通过从头开始并向后工作来找到它。

这就是为什么绘制图形或其一部分很重要的原因。通过从某个名称开始并向后工作,您可以找到的提交也是您明天能够找到的提交——假设您不移动名称,也就是说。

到目前为止,这为您提供了两种选择

您可以通过完全删除名称来保留您的 squash-merge 提交并忘记其他提交develop

...--o--o--S   <-- master
         \
          o--o--...--o   [was develop; 28 commits, all abandoned]

或者您可以形成自己的本地 Git 图,如下所示:

          S   <-- origin/master
         /
...--o--o   <-- master (HEAD)
         \
          o--o--...--o   <-- develop, origin/develop

请注意,您master在 commit 之前标识了提交S。nameorigin/master是 Git 的 GitHub 本地副本master,指向 commit S

如果您master 已经看起来像这样,那么您已经设置好了。如果你master看起来像这样:

          S   <-- master (HEAD), origin/master
         /
...--o--o
         \
          o--o--...--o   <-- develop, origin/develop

你会跑git reset --hard HEAD~1回去把它移回去,让它看起来像第一幅画。

现在您的(本地)Git 存储库已正确设置,您必须在 GitHub 告诉 Git 将其后 master退一步。为此,您需要使用git push --forceor git push --force-with-lease

git push --force-with-lease origin master

两者之间的区别在于,--force-with-lease他们的 Git(GitHub 上的那个)会检查您认为他们拥有mastermaster. 因此,如果其他人也与这个其他存储库一起工作,那么--force-with-lease与更简单的相比,它会增加一些安全性。--force

正常git push情况下,您的 Git 会调用另一个 Git(在本例中是 GitHub 上的那个),并确定您是否有新的提交。如果您确实有新的提交,您的 Git 会将它们发送过来。然后,最后,你的 Git 会礼貌地向他们提出请求:如果没问题,请将你的分支名称 ______ 设置为 commit hash _______。Git 将填写两个空白:名称是您使用的分支名称,即masterin git push origin master,哈希 ID 是名称当前在您的Git 中表示的哈希 ID。

force-push 做同样的事情,除了最后的请求一点也不礼貌:这是一个要求,将你的分支名称 _______ 设置为 ________!

git push如果它不会丢失任何提交,则参与的另一个 Git将接受礼貌的请求。在这种情况下,您的请求或命令专门设计为使它们失去commit S。也就是说,他们有:

...--o--o--S   <-- master
         \
          o--o--...--o   <-- develop

并且您希望他们推开Smaster指出之前的提交S。因此,您将需要某种有力的推动。

您可以恢复壁球合并,然后变基

git revert命令通过添加一个新的提交来撤销先前提交的效果。由于这增加了提交,Gits 很乐意接受新的提交。这个特殊的方法在这里有点傻,所以虽然我会画出效果,但它可能不是你想要的:

...--o--H--S--U   <-- master
         \
          o--o--...--o   <-- develop

H我在这里给出了develop一个名字的起点提交。原因是commit中的快照U,undo for S,将匹配 commit 中的快照H

您现在可以使用git rebase将整个系列的提交复制develop到新的一系列提交中。这些新提交具有不同的哈希 ID 和不同的父链接,但具有与以前相同的快照

                o--o--...--o   <-- develop (HEAD)
               /
...--o--H--S--U   <-- master, origin/master
         \
          o--o--...--o   <-- origin/develop

您现在处于与意外提交之前相同的情况S。您必须强制将您的更新推develop送到 GitHub。(然后,当然,您仍然必须合并所有内容!)

您可以继续进行真正的合并

您可以保留壁球提交S并进行真正的合并:

git fetch                           # if needed
git checkout master
git merge --ff-only origin/master   # if needed

这样你就有了:

...--G--H--S   <-- master (HEAD), origin/master
         \
          I--...--R   <-- develop, origin/develop

进而:

git merge develop

生产:

             ,--------<-- origin/master
            .
...--G--H--S--------M   <-- master (HEAD)
         \         /
          I--...--R   <-- develop, origin/develop

你现在git push origin master可以让他们接受合并提交M,给你:

...--G--H--S--------M   <-- master (HEAD), origin/master
         \         /
          I--...--R   <-- develop, origin/develop

在您自己的本地 Git 存储库中。

您可以删除 S、快进合并您的develop和强制推送

最后,在执行任何强制推送之前,这可能是您希望在个人本地 Git 存储库中看到的内容:

          S   <-- origin/master
         /
...--G--H--I--...--R   <-- develop, master (HEAD), origin/develop

这也许是你所拥有的:

          S   <-- origin/master
         /
...--G--H   <-- master (HEAD)
         \
          I--...--R   <-- develop, origin/develop

或者你现在有:

          S   <-- master (HEAD). origin/master
         /
...--G--H
         \
          I--...--R   <-- develop, origin/develop

为了得到你想要的,在任何一种情况下,你都可以:

git checkout master
git reset --hard develop

由于您develop当前指向 commit R,这将检查 commit R,使您master指向 commit R,并为您留下您想要的图表。然后,您可以使用强制推送:

git push --force-with-lease origin master

发送您拥有但他们没有的任何提交(没有,他们已经拥有所有这些提交)并命令它们:我认为您master标识了提交S。如果是这样,请立即识别提交R 他们通常会服从这个命令,你就会得到你想要的。


推荐阅读