git - 在“挤压和合并”之后,我的分支机构出现了分歧。我该如何解决?
问题描述
我做了一个 '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
。
解决方案
我该如何解决?
它只是一种可修复的。您通常应该现在完全删除分支。然后,您可以创建一个新分支,甚至是仍然命名为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)
(这里develop
是3 ahead
。master
)
还有另一件关键的事情要实现:提交G
和H
,以及在那些 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
。所以我们这边没有任何改变可以继续推进。我们只想要他们所做的任何更改,从H
到K
。结果将与 中的快照相同K
。但是我们正在强制合并,所以 Git 进行了新的合并提交,我将要求这样M
做merge
:
...--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 --force
or git push --force-with-lease
:
git push --force-with-lease origin master
两者之间的区别在于,--force-with-lease
他们的 Git(GitHub 上的那个)会检查您认为他们拥有master
的master
. 因此,如果其他人也与这个其他存储库一起工作,那么--force-with-lease
与更简单的相比,它会增加一些安全性。--force
正常git push
情况下,您的 Git 会调用另一个 Git(在本例中是 GitHub 上的那个),并确定您是否有新的提交。如果您确实有新的提交,您的 Git 会将它们发送过来。然后,最后,你的 Git 会礼貌地向他们提出请求:如果没问题,请将你的分支名称 ______ 设置为 commit hash _______。Git 将填写两个空白:名称是您使用的分支名称,即master
in git push origin master
,哈希 ID 是名称当前在您的Git 中表示的哈希 ID。
force-push 做同样的事情,除了最后的请求一点也不礼貌:这是一个要求,将你的分支名称 _______ 设置为 ________!
git push
如果它不会丢失任何提交,则参与的另一个 Git将接受礼貌的请求。在这种情况下,您的请求或命令专门设计为使它们失去commit S
。也就是说,他们有:
...--o--o--S <-- master
\
o--o--...--o <-- develop
并且您希望他们推开S
并master
指出之前的提交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
! 他们通常会服从这个命令,你就会得到你想要的。
推荐阅读
- android - 从themes.xml 更改TopAppBar 背景颜色
- gitlab-ci-runner - 如何在管道中自动重启机器后连接回 gitlab -runner?
- javascript - jquery的交叉口观察者
- html - 我的网页内容一直与我的背景图片幻灯片重叠,我已经尽我所能来解决这个问题
- c# - 如何迭代文件夹下的所有文件,但我不想迭代一些特殊的子文件夹?
- git - 如何在 Github 的两个分支中指向同一个提交?
- visual-studio-app-center - appcenter 发送日志 groupName=group_core id=XXX 失败
- java - 将属于特定时区的时间戳转换为另一个时区的时间戳
- qt - 为什么委托项目中心绘制在路径的起始位置?
- snmp - snmpwalk 需要进行一些优化,从 SNMP Manager 获得 5 秒内的响应。需要在 1 秒或毫秒内缩短此时间吗?