有人创建了一个空的合并提交(develop
到broken-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 checkout
或git 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
顶端的提交。 为了稍后更容易参考,让我将这两个提交称为A
和B
:
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
选项,我们将有一个正常的合并,我们将来不会为自己设置陷阱。但是我们正在努力重现您的问题。
现在让我们以通常的方式在develop
和mistake
上进行更多提交:
o--o--A--C--D--E <-- develop
/
...--* M--F--G--H <-- mistake
/
o--o--B <-- br2
如果我们现在检查出这两个中的一个(例如,develop
- 并在另一个上运行git merge
,Git 会执行我们上次应该让它做的相同的事情:它找到提交E
和H
之间的合并基础。 这里的问题是这是提交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
中的内容是C
、D
和E
中更改的内容。 Git 结合了这两个更改:添加C+D+E,但也添加 M+F+G+H。添加 M步骤意味着删除两个顶行o
提交中的内容。
请注意,提交A
和B
之间的合并基是提交*
。 如果我们让 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
,让我们创建一个指向提交H
mistake
的新名称,然后使用git branch
或git reset --hard
强制名称br2
指向回提交B
:
git checkout -b mistake br2; git branch -f br2 <hash-of-B>
现在我们将git checkout
以br2
的名称提交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 命令本质上只是一个带有一些分支名称杂耍的集体樱桃选择,这正是我们刚刚所做的。 现在我们可以以正常方式合并提交E
和H'
,使用 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 -f
或git 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是从分支中的最后一次提交到要还原的提交的提交数。
然后,您可以尝试删除提交。