Git - 如何还原空合并提交



有人创建了一个空的合并提交(developbroken-branch)。

当我尝试运行git revert <commit> -m 1时,我收到一条消息:

$ git revert <commit> -m 1
Already up to date!
On branch my-brach
Your branch is up to date with 'origin/broken-brach'.
nothing to commit, working tree clean

我需要删除此提交,因为将此分支合并到develop只会还原在空合并期间忽略的develop中的所有更改。

此外,提交已发布:(

更新:提交在整个团队中共享。之后有很多提交

我需要删除此提交...

实际上,您不需要删除它。 删除它将解决一个问题,但可以创建其他问题。

。由于将此分支合并到开发只是还原在空合并期间忽略的所有更改。

我相信我知道你在这里想说什么。 这在技术上并不完全正确,但抓住了问题的本质。

此外,提交已发布:(

更新:提交在整个团队中共享。之后有很多提交

这就是我在第一句话中提到的可能产生其他[问题]部分的地方。 删除无意义的合并可能太难了,因此您可能需要不同的方法。 不过,首先,让我们正确描述问题。

我们从两条开发线开始,每条开发线都有一系列提交,以及两个分支名称来标识每条开发线中的最后一次提交:

o--o--o   <-- develop
/
...--*

o--o--o   <-- br2

我使用了您使用的两个名称之一(develop)和第二个分支的更中性的名称(br2)。 请注意,这两个分支都是从某个共同的起点下降的,我在这里将其标记为提交"star"*。 自那时以来,我任意选择将分支表示为每个分支上有三个提交,尽管任何数量的提交都可以工作。 关键是这个星号提交是两个分支的最佳共同祖先

如果我们现在要执行正常的合并操作,我们可以选择具有git checkoutgit switch的两个分支中的一个,并在另一个分支上运行git merge。 但是,为了表示我们的错误,我将首先创建一个新名称mistake,并将其指向截至此时分支br2上的最后一个提交:

o--o--o   <-- develop
/
...--*

o--o--o   <-- br2, mistake (HEAD)

我们现在有三个分支,包括我们会犯错误的分支(故意):我们将运行:

git merge -s ours develop

这将创建一个新的合并提交,我将使用字母M表示。 合并提交与任何其他提交一样,因为它具有所有文件的源代码快照和第一个父级,但与普通提交不同,它也有第二个父级。 新合并M的第一个父级将是位于br2顶端(当前位于mistake顶端)的提交,第二个父项将是位于develop顶端的提交。 为了稍后更容易参考,让我将这两个提交称为AB

o--o--A   <-- develop
/       
...--*         M   <-- mistake (HEAD)
       /
o--o--B   <-- br2

由于-s ours选项,提交M中的快照br2上最后一次提交中的快照完全匹配。 也就是说,如果我们得到M的实际哈希 ID 并B并运行:

git diff <hash-of-M> <hash-of-B>

差异完全为空。 (如果它不是完全空的,我们以后仍然可能遇到问题,只是问题可能小一点。如果我们省略了-s ours选项,我们将有一个正常的合并,我们将来不会为自己设置陷阱。但是我们正在努力重现您的问题。

现在让我们以通常的方式在developmistake上进行更多提交:

o--o--A--C--D--E   <-- develop
/       
...--*         M--F--G--H   <-- mistake
       /
o--o--B   <-- br2

如果我们现在检查出这两个中的一个(例如,develop- 并在另一个上运行git merge,Git 会执行我们上次应该让它做的相同的事情:它找到提交EH之间的合并基础。 这里的问题是这是提交A,而不是提交*

然后,Git 现在所做的是将合并库中的内容(提交A)与两个分支提示提交中的内容进行比较。 也就是说,它实际上运行:

git diff --find-renames <hash-of-A> <hash-of-E>   # what we changed
git diff --find-renames <hash-of-A> <hash-of-H>   # what they changed

现在,提交H中到底是什么? 好吧,提交H是通过获取M中的内容(与B中的内容相匹配)并在F中进行一些更改,然后在G中进行一些更改,然后在H中进行一些更改来构建的。 因此,"他们更改了什么"部分首先删除沿顶行的两个o提交中的任何内容,然后添加更多更改。

同时,A-vs-E中的内容是CDE中更改的内容。 Git 结合了这两个更改:添加C+D+E,但也添加 M+F+G+H添加 M步骤意味着删除两个顶行o提交中的内容

请注意,提交AB之间的合并基是提交*。 如果我们让 Git 将导致A的三个提交中的工作与导致B的三个提交中的工作结合起来——换句话说,如果我们没有犯错误——我们现在的状态会很好。M中的快照将合并这些工作。 它不会带走顶部两个未命名o提交的更改。 但是我们故意不使用提交*。 我们在M中制作了快照,以匹配B中的快照。我们设置了一个定时炸弹,后来的合并引爆了定时炸弹。

请注意,此问题与分支名称无关。 如果我们不使用名称mistake,我们现在将拥有:

o--o--A--C--D--E   <-- develop
/       
...--*         M--F--G--H   <-- br2
       /
o--o--B

这是相同的提交图,因此包含相同的定时炸弹。 炸弹嵌入在提交中。 我们用来查找提交的名称完全无关紧要。

如何解决问题

有几种不同的方法可以解决此问题。 它们都不是唯一的正确方式。他们所有人都需要考虑这个错误。 错误是设置了一个定时炸弹,以便将来的合并将丢弃几个提交 - 顶行的未命名提交。

解决此问题的所有方法都涉及创建新的提交。 (毕竟,这几乎是你在 Git 中过的唯一一件事。 问题是要创建哪些新提交。 我们可以选择以下几种操作中的任何一种:

  • 复制一大堆提交,随时修复我们之前的错误。
  • 继续并立即合并,然后复制丢弃的各个提交。
  • 暂时"假装"缺少合并,使用git replace. (我不打算展示这种方法,因为它很复杂并且存在可重复性问题。
  • 您可以想象并亲自尝试的其他内容。

假设我们从这个开始:

o--o--A--C--D--E   <-- develop
/       
...--*         M--F--G--H   <-- mistake
       /
o--o--B   <-- br2

也就是说,即使我们一直使用名称br2,让我们创建一个指向提交Hmistake的新名称,然后使用git branchgit reset --hard强制名称br2指向回提交B

git checkout -b mistake br2; git branch -f br2 <hash-of-B>

现在我们将git checkoutbr2的名称提交B

git checkout br2

获得:

o--o--A--C--D--E   <-- develop
/       
...--*         M--F--G--H   <-- mistake
       /
o--o--B   <-- br2 (HEAD)

方法1:复制提交,省略错误

我们现在可以使用集体git cherry-pick简单地复制M的每个提交,而无需为提交M而烦恼:

o--o--A--C--D--E   <-- develop
/       
...--*         M--F--G--H   <-- mistake
       /
o--o--B--F'-G'-H'   <-- br2 (HEAD)

这可能会有一堆合并冲突。 如果是这样,我们将不得不解决每一个问题。

完成此操作后,如果我们愿意,我们现在可以完全删除mistake名称,并假装提交M-F-G-H甚至不存在:

o--o--A--C--D--E   <-- develop
/
...--*

o--o--B--F'-G'-H'   <-- br2 (HEAD)

这相当于通过git rebase删除合并。 rebase 命令本质上只是一个带有一些分支名称杂耍的集体樱桃选择,这正是我们刚刚所做的。 现在我们可以以正常方式合并提交EH',使用 commit*作为它们的合并基础。

请注意,我们可以将名称保留在mistake,也许可以创建一个新名称br3,以便我们得到:

o--o--A--C--D----E   <-- develop
/                 
...--*         M--F--G--H  <-- mistake
       /            
o--o--B--F'-G'-H'----M2   <-- br3 (HEAD)

或者使用develop作为获取合并的分支:

o--o--A--C--D--E-----M2   <-- develop (HEAD)
/                   /
...--*         M--F--G--H / <-- mistake
       /          /
o--o--B--F'-G'---H'   <-- br3

创建新名称的优点是,这样分支名称只会向前移动。 每当我们使用git branch -fgit rebase先"向后"移动分支名称,然后再向前移动时,这意味着我们存储库的每个克隆都必须调整他们所做的任何工作,这取决于名称向前移动。

方法二:合并,然后复制提交以修复错误

或者,让我们从这个开始看:

o--o--A--C--D--E   <-- develop
/       
...--*         M--F--G--H   <-- br2 (HEAD)
       /
o--o--B

现在就进行合并。 我们运行git checkout develop,然后git merge br2并得到:

o--o--A--C--D--E---M2   <-- develop (HEAD)
/                 /
...--*         M--F--G--H   <-- br2
       /
o--o--B

正如我们所指出的,提交M2丢弃的地方从顶部o-o提交。 所以现在让我们挑选它们:

git cherry-pick <hash1>
git cherry-pick <hash2>

合并本身和两个樱桃选择可能存在合并冲突。 如果是这样,那没关系;我们只是解决它们。 现在我们有:

o--o--A--C--D--E---M2-o'-o'  <-- develop (HEAD)
/                 /
...--*         M--F--G--H   <-- br2
       /
o--o--B

上一次o'提交的快照有我们想要的。

方法3:复制develop分支以绕过错误

最后,我们可以使用另一个大规模复制技巧。 与其将F-G-H大量复制到B末尾(无论是按git cherry-pick还是按git rebase),不如将整个顶行大量复制。 为了理智起见,让我们暂时使用一个新名称,将develop保留为旧名称。 我们将新分支称为fixup,并将其指向提交*,这是在搞砸开始之前:

..................   <-- fixup (HEAD)
.
. o--o--A--C--D--E   <-- develop
./       
...--*         M--F--G--H   <-- br2
       /
o--o--B

现在,在签出fixup(如上面的HEAD所示),我们强制 Git 使用git cherry-pick复制提交o-o-C-D-E

git cherry-pick HEAD..develop

这(从来没有)合并冲突,因为这些提交很容易在这里应用,所以现在我们有:

o'-o'-A'-C'-D'-E'  <-- fixup (HEAD)
/
| o--o--A--C--D--E   <-- develop
|/       
...--*         M--F--G--H   <-- br2
       /
o--o--B

我们现在可以与git merge br2. 这里的合并基础是提交*,而不是提交A- 提交A不在fixup- 因此合并具有所需的源快照:

o'-o'-A'-C'-D'-E'----M2  <-- fixup (HEAD)
/                    /
| o--o--A--C--D--E   /  <-- develop
|/                 /
...--*         M--F--G--H   <-- br2
       /
o--o--B

现在,我们已准备好执行最后一次合并,包括:

git checkout develop
git merge fixup

这两个提交的合并基础再次是 commit*,因此 Git 比较*E以查看我们更改的内容,*M2进行比较以查看"它们"更改了什么。 "他们"更改的内容包括我们更改的所有内容,因此幸运的是,此合并进展顺利,并自动进行新的提交:

o'-o'-A'-C'-D'-E'----M2  <-- fixup
/                    /  
| o--o--A--C--D--E---/----M3  <-- develop (HEAD)
|/                 /
...--*         M--F--G--H   <-- br2
       /
o--o--B

我们现在可以完全删除该名称fixup。 我们所做的只是添加新的提交:一个到develop,以及一个全新的链,我们在这样做时称之为fixup

如果提交已发布,但并未真正共享(例如:如果您的团队中没有其他人根据broken-branch进行工作),则可能处于可以强制推送的情况:

# spot <good sha> in the history of 'broken-branch' :
git push origin --force-with-lease <good sha>:broken-branch
# additionally, fix your local repo and your colleague's local repo

否则:您可以运行git restore在合并之上创建一个新的提交,其内容与合并前broken-branch的内容完全相同:

# again : spot <good sha> in the history of 'broken-branch' :
git restore <good sha>
git commit
git push

注意git restore最近添加到 git (v2.27) 中,如果您坚持使用较旧的 git 并且无法升级,请将其替换为:

git read-tree <good sha>
git commit
...

这是分支中的最后一次提交吗? 如果是 - 您可以执行以下操作:git reset --hard HEAD~1,然后git push -f

如果没有 - 您可以尝试:git rebase -i HEAD~{X}

X是从分支中的最后一次提交到要还原的提交的提交数。

然后,您可以尝试删除提交。

最新更新