[英]Merged and pushed feature branch to the wrong remote branch. How to fix?
[英]Merged Master into branch then committed and pushed changes to branch. How can this be undone without a force push?
我犯了一个错误。
我有一个分支( A
),它是从Master
分支出来的。 Master
领先A
不少。 前几天不小心把Master
合并到A
里面推了。 那天晚些时候我注意到了我的错误,但不知道如何修复它,所以我尝试添加一些功能标志来关闭不应在A
中启用的功能并推送它们。 后来,我决定尝试恢复A
分支以摆脱所有Master
提交。 我完成了所有更改(大约 100 个文件),现在A
看起来就像在Master
之前所做的那样。 但是,我现在的问题是,如果没有合并尝试删除Master
中存在的所有更改,我就无法将A
合并到Master
中。 (即,在Master
中创建的新文件在A
的恢复中被删除,所以现在 git 想要从Master
中删除文件,如果我尝试将A
合并到Master
。)
我怎样才能修复我的巨大错误,然后回到我可以在A
上进行维护补丁并相应地与Master
合并的地方,以便将来的版本不会丢失补丁?
我 C。 你大发雷霆。 最好的方法是删除 master 到 A.. 的合并,但它的价格很高。 不太优雅的方法是取消恢复从 A 恢复的与 master 相关的更改,以便它们不再消失。 然后,当您将 A 合并到 master 时,您将不会看到那么大的逆转。 我不喜欢它,但是......
如何在没有强制推送的情况下撤消合并的简短答案? 是:你不能。
更长的答案是你不能,但你也不需要,只要你知道你在做什么以及合并是如何工作的; 如果您可以说服所有其他用户以这种方式强制使用任何存储库,那么强制推送有时会更方便。
请参阅如何恢复已推送到远程分支的合并提交? 另请参阅MK446 对同一问题的回答,这几乎是 Linus Torvald 关于恢复合并恢复的描述的复制粘贴。
理解为什么会出现这种情况以及如何处理的关键是要认识到任何一组提交的“合并性”是提交本身所固有的。 分支名称仅用作查找提交的方法。 强制推送的行为是一种更改名称指向的方法,以便人们(和 Gits)无法再找到某些提交。
拿到手之后就很容易看出来,但是我还是不知道怎么解释,只能说服人画图。 Linus Torvalds 以这种方式总结了它——这是准确的,但很棘手:
[虽然] 恢复合并提交...撤消提交更改的数据,...它对合并对历史的影响绝对没有任何作用。 所以合并仍然存在,它仍将被视为将两个分支连接在一起,未来的合并将把合并视为最后一个共享的 state - 恢复引入的合并的恢复根本不会影响它。 因此,“还原”撤消了数据更改,但从某种意义上说,它不是“撤消”,因为它不会撤消提交对存储库历史的影响。 因此,如果您将“还原”视为“撤消”,那么您将永远错过这部分还原。 是的,它会撤消数据,但不,它不会撤消历史记录。
“历史”是提交图。 该图由提交确定,但我们通过分支名称找到提交。 因此,我们可以通过更改存储在名称中的 hash ID 来更改我们可以看到的内容。 但是,除非您知道并在自己的脑海中看到它是如何工作的,否则这并没有真正的帮助。
您可能会花一些时间查看Think Like (a) Git上的教程,但为了快速查看,请考虑以下事实:
一个 Git 提交由两部分组成:它的主要数据,它是你所有文件的快照——我们将在这里多说一点——以及它的元数据,它包含关于提交本身的信息。 大多数元数据都是供您稍后自己了解的信息:谁进行了提交,何时以及他们的日志消息告诉您他们为什么进行该提交。 但是元数据中的一项是针对 Git 本身的,即父提交 hash ID的列表。
存储在任何 Git 提交中的所有内容——实际上,在任何 Git object中,但大多数情况下你直接处理提交对象——都是完全只读的。 原因是 Git 通过 hash ID找到object。 Git 有一个存储这些对象的大键值数据库; 键是 hash ID,值是对象的内容。 每个key唯一标识一个object,并且每个commit都是不同的, 1所以每个commit都有一个唯一的hash ID。 2
因此,提交的 hash ID 实际上是该提交的“真实名称”。 每当我们将 hash ID 存储在某处时,例如,在文件中,或电子表格中的一行,或其他任何地方,我们说这个条目指向提交。
因此,存储在每个提交中的父 hash ID指向先前的提交。 大多数提交只有一个父 hash ID; 使提交成为合并提交的原因是它具有两个或多个父 hash ID。 Git 确保每当有人进行新提交时,该提交中列出的父 hash ID 是现有提交的 ID。 3
所有这一切的结果是,大多数普通提交以简单的线性方式向后指向。 如果我们绘制一系列提交,用单个大写字母替换真实的 hash ID,将较新的提交放在右侧,我们得到:
... <-F <-G <-H
其中H
代表链中最后一次提交的 hash ID。 提交H
指向(包含原始 hash ID)其父提交G
; 提交G
指向更早的提交F
; 等等。
因为 hash ID 看起来非常随机, 4我们需要一些方法来找到链中的最后一个提交。 另一种方法是查看存储库中的每个提交,构建所有链,并使用它来确定哪些提交是“最后的”。 5这太慢了:所以 Git 给了我们分支名称。 像master
或dev
这样的分支名称只是指向一个提交。 无论名称指向什么提交,我们都认定这是分支的尖端提交。 所以给出:
...--F--G--H <-- master
我们说 commit H
是分支master
的提示提交。 6我们说所有这些提交都包含在分支master
中。
多个名称可以指向任何一个特定的提交。 如果我们有:
...--G--H <-- dev, master
然后两个名称dev
和master
将提交H
标识为其分支提示提交。 通过并包括H
的提交在两个分支上。 我们将git checkout
出这些名称之一以开始使用提交H
; 如果我们随后添加一个新提交,则新提交将以提交H
作为其父级。 例如,如果我们在“on”分支master
时添加一个新的提交,新的提交将是提交I
,我们可以这样绘制:
I <-- master (HEAD)
/
...--G--H <-- dev
特殊名称HEAD
可以附加到一个分支名称上——一次只能附加一个; 它指示哪个分支名称新提交更新,并向我们显示哪个提交是我们当前的提交,哪个分支名称是我们的当前分支。
添加另一个提交到master
,然后签出dev
,得到我们这个:
I--J <-- master
/
...--G--H <-- dev (HEAD)
当前提交现在倒回到H
,当前分支是dev
。
1这就是提交具有日期和时间戳的原因之一。 即使两个提交在其他方面相同,但如果它们是在不同时间进行的,它们具有不同的时间戳,因此是不同的提交。 如果您在完全相同的时间进行两次完全相同的提交,那么您只进行了一次提交......但是如果您在完全相同的时间多次执行完全相同的事情,您实际上做了很多事情,还是只做一件事?
2根据鸽巢原理,如果“所有提交”的空间大于“提交 hash IDs”的空间——并且确实如此——必须有多个不同的提交解析到相同的 hash ID。 Git 对此的回答部分是“你不能使用那些其他提交”,但也是“那又怎样,它在实践中从未发生过”。 另请参阅新发现的 SHA-1 冲突如何影响 Git?
3不这样做可能会导致 Git 存储库损坏,“连接性”不正确。 每当您看到有关“检查连接性”的 Git 消息时,Git 就会进行此类检查。 一些新的 Git 工作故意削弱这些连接检查,但即使 Git 有时不检查,至少原则上仍然存在规则。
4当然,它们完全是确定性的——它们目前是 SHA-1 哈希——但它们具有足够的不可预测性,看起来是随机的。
5 git fsck
和git gc
都这样做,以便确定是否有一些提交可以被丢弃。 git fsck
命令会告诉你它们——它们是悬空的和/或无法访问的提交。 如果其他条件合适, git gc
命令将删除它们。 特别是,它们需要老化超过过期时间。 这避免了git gc
删除仍在构建的提交。 提交和其他对象可能无法访问,因为创建它们的 Git 命令尚未完成。
6这给我们留下了各种各样的难题:Git 中的分支一词是模棱两可的。 它是表示分支名称,还是表示提示提交,还是表示某些以指定提交结尾的提交? 如果是后者,规范是否必须是分支名称? 这个问题的答案通常是肯定的:分支这个词可以表示所有这些,甚至更多。 另请参阅“分支”到底是什么意思? 因此,最好尽可能使用更具体的术语。
现在我们在dev
和 commit H
上,我们可以再添加两个提交来生成:
I--J <-- master
/
...--G--H
\
K--L <-- dev (HEAD)
此时,我们可以git checkout master
然后git merge dev
。 如果提交是 Git 存在的理由,那么 Git 的自动合并是我们都使用Git 而不是其他一些 VCS 的重要原因。 7 git merge
所做的是执行三向合并,将合并基础快照与两个提示提交快照相结合。
合并基础完全由提交图决定。 在这个特定的图表中很容易看到,因为合并基础是两个分支上的最佳提交。 8所以git merge
会做的是:
H
中的快照与我们当前分支提示提交中的快照进行比较,以查看我们更改了什么; 和H
中的快照与其分支提示提交中的快照进行比较,以查看它们更改了什么, 然后简单地(或复杂地,如果需要)组合这两组更改。 现在可以将组合更改应用于基本快照,即在提交H
中始终保存的文件。
组合这两个变更集的结果要么是成功——一个新的快照准备好 go 进入一个新的提交——要么是一个合并冲突。 每当 Git 无法自行组合我们的更改及其更改时,就会发生冲突情况。 If that happens, Git stops in the middle of the merge, leaving a mess behind, and our job becomes clean up the mess and provide the correct final snapshot and then tell Git to continue: git merge --continue
or git commit
(both do同样的事情)。
成功组合更改后——也许在我们的帮助下——Git 现在进行了新的提交。 这个新提交就像任何其他提交一样,因为它有一个数据快照,并且有一些元数据给出我们的名字和 email 地址、当前日期和时间等等。 但它的特殊之处仅在于一种方式:作为其父母(复数),它具有两个提示提交的 hash ID。
与任何提交一样,提交的行为会更新当前分支名称,因此我们可以绘制如下结果:
I--J
/ \
...--G--H M <-- master (HEAD)
\ /
K--L <-- dev
请记住,我们从git checkout master
开始该过程,因此当前提交是J
并且当前分支名称是,并且仍然是master
。 当前提交现在是合并提交M
,它的两个父级依次是J
(如果您愿意,可以稍后使用J
的第一个父级)和L
。
7许多前 Git VCS 具有内置的合并功能,但没有那么多具有如此聪明和自动的合并功能。 过去和现在都有其他很好的版本控制系统,但Git也增加了分布式版本控制,并与GitHub等站点一起赢得了网络效应。 所以现在我们被 Git 困住了。 Mercurial 在用户友好性方面明显优于 Git,而且 Bitbucket 曾经是 Mercurial 专用站点,但现在......不是了。
8在这里,我们使用分支一词来表示可从当前分支提示到达的提交集。 我们知道分支名称稍后会移动:在将来的某个时候, master
不会命名 commit J
和/或dev
不会命名 commit L
,但现在他们会这样做。 所以我们发现提交可以从J
到达并向后工作,并且提交可从L
到达并向后工作,当我们这样做时,两个分支上明显的最佳提交是提交H
git merge
并不总是合并在一种特定(但常见)的情况下, git merge
不会进行合并提交,除非您强制它这样做。 特别是,假设两个分支上的最佳共享提交是“后面”分支上的最后一个提交。 也就是说,假设我们有:
...--o--B <-- br1 (HEAD)
\
C--D <-- br2
其中D
的父级是C
, C 的父C
是B
,依此类推。 我们已经检查了br1
,如HEAD
所示。 如果我们运行git merge br2
B
Git 将像往常一样找到提交B
和D
,从D
向后工作到C
,发现最好的提交也是在当前共享分支上提交B
犯罪。
如果我们此时进行真正的合并,Git 将比较B
中的快照与B
中的快照:base vs HEAD
是B
vs B
。 显然这里没有变化。 然后 Git 将比较B
中的快照与D
中的快照。 无论这些更改是什么,Git 都会将这些更改应用于B
中的快照。 结果是...... D
中的快照。
因此,如果 Git 在这一点上进行真正的合并,它将产生:
...--o--B------M <-- br1 (HEAD)
\ /
C--D <-- br2
其中M
中的快照与D
中的快照完全匹配。
您可以强制 Git使用git merge --no-ff
进行真正的合并,但默认情况下,Git 将“作弊”。 它会对自己说:合并快照将匹配D
,因此我们可以直接将名称br1
指向提交D
。 所以git merge
将简单地git checkout D
,但也滑动名称br1
"forward" 指向提交D
:
...--o--B
\
C--D <-- br1 (HEAD), br2
如果您使用 GitHub 进行合并,请注意GitHub 始终强制进行真正的合并,因此您永远不会快进。 9
9最接近的方法是使用 GitHub 的变基和合并模式,但这会复制原本可以快速向前合并的提交。 它为他们提供了新的提交者名称和电子邮件以及时间戳,并且生成的提交具有新的 hash ID。 所以它从来都不是真正的快进。 这有时很烦人,我希望他们有一个真正的快进选项。
假设我们已经完成了这个模式一段时间,并且有:
...--o--o--o------A-----M <-- master
\ / /
o--o--o--o--B--C--D <-- dev
哪个提交是master
和dev
的合并基础? 这里有一个很大的提示:它是一个有字母的提交,而不是更无聊的历史o
提交。
棘手的部分是要找到合并基础,当我们从分支提示提交向后走时,我们应该同时访问两个父节点。 所以合并提交M
有两个父母, A
和B
。 同时,从D
开始并向后工作,我们也到达了提交B
(经过两跳)。 所以提交B
是合并基础。
B
是合并基础的原因是合并提交M
的存在。 名称master
指向M
和M
指向两个提交, A
和B
。 提交B
是“on”(包含在)分支master
,并且显然是on/contained-in branch dev
,只要master
指向提交M
或到达(通过某些提交链)合并M
。
Git 通常只会向分支添加提交,有时通过提交一次一个,有时通过合并或快进一次添加多个。 一旦提交B
通过提交M
变为“on”(包含在)分支master
,它将继续是 on/contained-in master
。 未来的合并可能会找到比提交B
更好的提交,但只要这些提交继续在master
和dev
上,提交B
将始终是基于合并的候选者。
所以这就是为什么你不能在没有强制推送的情况下“撤消合并”。 您可以更改新提交中的快照(例如,这就是git revert
的内容),但您无法更改现有提交的历史记录。 历史是通过遍历图找到的一组提交,所有现有的提交都将一直冻结,只要可以找到它们就会保留在图中:
...--o--o--o------A-----M <-- master
\ / /
o--o--o--o--B--C--D <-- dev
master
的历史是提交M
,然后都提交A
和B
,然后是他们的父母等等。 dev
的历史是提交D
,然后提交C
,然后提交B
,依此类推。
要更改从master
看到的历史记录,您必须说服 Git 停止遍历提交M
如果您使用 force-push 从master
中删除M
——它仍然存在,只是不再通过master
找到它——你会得到:
------M ???
/ /
...--o--o--o------A <-- master
\ / /
o--o--o--o--B--C--D <-- dev
(请注意,在此图中没有找到M
的名称,因此最终git gc
将完全丢弃提交M
另请参见脚注 5。)
Force-push 是我们告诉 Git 的方式:是的,此操作将导致某些提交无法访问,并且可能永远丢失它们。 我们的意思是让这发生! 通过完全删除合并提交M
,我们返回到 state ,其中从未发生合并,并且下次提交B
不会成为合并基础。
(练习:找到合并基。)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.