简体   繁体   English

Git - 如何恢复空的合并提交?

[英]Git - How to revert empty merge commit?

Someone created an empty merge commit( develop to broken-branch ).有人创建了一个空的合并提交( develop to broken-branch )。

When I try to run git revert <commit> -m 1 I receive a message:当我尝试运行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

I need to remove this commit since merging this branch to develop simply reverts all changes from develop that were ignored during the empty merge.我需要删除这个提交,因为将这个分支合并到develop只是恢复了develop中在空合并期间被忽略的所有更改。

Also, the commit is already published:(此外,提交已经发布:(

UPDATE: The commit is shared across the team.更新:提交在整个团队中共享。 And there are plenty of commits after that之后有很多提交

I need to remove this commit...我需要删除这个提交...

Actually, you don't need to remove it.实际上,您不需要删除它。 Removing it would solve one problem but can create others.删除它可以解决一个问题,但会产生其他问题。

... since merging this branch to develop simply reverts all changes from develop that were ignored during the empty merge. ...因为将这个分支合并到开发只是简单地恢复在空合并期间被忽略的所有来自开发的更改。

I believe I know what you mean to say here.我相信我知道你在这里要说什么。 That's not quite correct technically, but captures the essence of the problem.这在技术上并不完全正确,但抓住了问题的本质。

Also, the commit is already published:(此外,提交已经发布:(

UPDATE: The commit is shared across the team.更新:提交在整个团队中共享。 And there are plenty of commits after that之后有很多提交

This is where the can create other [problems] part I mentioned in my first sentence comes in. It may be too hard to remove the senseless merge, so you might want a different approach.这就是我在第一句话中提到的can create other [problems]部分的用武之地。删除无意义的合并可能太难了,因此您可能需要不同的方法。 First, let's describe the problem properly , though.首先,让我们正确描述问题。

We start with two lines of development, with a series of commits on each, and two branch names to identify the last commit in each of these lines of development:我们从两条开发线开始,每条线都有一系列提交,还有两个分支名称来标识每条开发线中的最后一次提交:

       o--o--o   <-- develop
      /
...--*
      \
       o--o--o   <-- br2

I've used one of the two names you used ( develop ) and a more neutral name for the second branch ( br2 ).我使用了您使用的两个名称之一 ( develop ),并为第二个分支 ( br2 ) 使用了一个更中性的名称。 Note that both branches descend from some common starting point, which I have marked here as commit "star" * .请注意,两个分支都从某个共同的起点下降,我在此处将其标记为 commit "star" * I've arbitrarily chosen to represent the branches as having three commits since that point on each branch, though any number of commits would work.我任意选择将分支表示为自每个分支上的该点以来具有三个提交,尽管任何数量的提交都可以工作。 The key is that this starred commit is the best common ancestor of the two branches.关键是这个带星号的提交是两个分支的最佳共同祖先

If we were to do a normal merge operation now, we could pick one of the two branches with git checkout or git switch and run git merge on the other branch.如果我们现在要进行正常的合并操作,我们可以选择带有git checkoutgit switch的两个分支之一,然后在另一个分支上运行git merge To represent our mistake, though, I'm going to first create a new name , mistake , and point it at the last commit that is on branch br2 as of this point:不过,为了表示我们的mistake ,我将首先创建一个新名称error ,并将其指向此时在分支br2上的最后一次提交:

       o--o--o   <-- develop
      /
...--*
      \
       o--o--o   <-- br2, mistake (HEAD)

We now have three branches, including the branch on which we'll make a mistake (on purpose): we will run:我们现在有三个分支,包括我们会(故意)犯错误的分支:我们将运行:

git merge -s ours develop

This will create a new merge commit , which I will represent using the letter M .这将创建一个新的合并提交,我将使用字母M来表示。 A merge commit is like any other commit in that it has a source code snapshot of all files and a first parent, but unlike ordinary commits, it then has a second parent too.合并提交与任何其他提交一样,它具有所有文件的源代码快照和第一个父级,但与普通提交不同,它还有第二个父级。 The first parent of new merge M will be the commit that is at the tip of br2 (and currently at the tip of mistake ) and the second parent will be the commit that is at the tip of develop . new merge M的第一个父级将是br2尖端的提交(当前处于mistake的尖端),第二个父级将是develop尖端的提交。 For easier reference later, let me call these two commits A and B :为了以后更容易参考,让我将这两个提交称为AB

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

Because of the -s ours option, the snapshot in commit M exactly matches the snapshot in the last commit on br2 .由于-s ours选项,提交M中的快照br2上的最后一次提交中的快照完全匹配。 That is, if we get the actual hash IDs of M and B and run:也就是说,如果我们得到MB的实际 hash ID 并运行:

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

the difference will be completely empty .差异将是完全空的。 (If it's not completely empty, we can still have a problem later, it's just that the problem might be a little smaller.) If we had left out the -s ours option, we would have a normal merge and we would not have set up a trap for ourselves in the future. (如果它不是完全为空,我们以后仍然会遇到问题,只是问题可能会小一点。)如果我们省略了-s ours选项,我们将有一个正常的合并,我们不会设置为自己的未来设下陷阱。 But we are trying to reproduce your issue.但我们正在尝试重现您的问题。

Now let's make more commits on both develop and mistake , in the usual way:现在让我们以通常的方式对developmistake进行更多的提交:

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

If we now check out one of these two—for instance, develop —and run git merge on the other, Git does the same sort of thing we should have let it do last time: it finds the merge base between commits E and H .如果我们现在检查这两个中的一个(例如, develop )并在另一个上运行git merge ,Git 会做我们上次应该让它做的同样的事情:它找到提交EH之间的合并基础 The problem here is that this is commit A , not commit * .这里的问题是这是提交A ,而不是提交*

What Git does now, then, is compare what's in the merge base—commit A —with what's in the two branch-tip commits.那么,Git 现在所做的是将合并基础(提交A )中的内容与两个分支提示提交中的内容进行比较。 That is, it runs, in effect:也就是说,它实际上运行:

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

Now, what exactly is in commit H ?现在,提交H到底什么? Well, commit H is built by taking what's in M —which matches what's in B —and making some change in F , then making some change in G , then making some change in H .好吧,提交H是通过获取M中的内容(与B中的内容匹配)并在F中进行一些更改,然后在G中进行一些更改,然后在H中进行一些更改来构建的。 So the "what they changed" part starts with removing whatever is in the two o commits along the top line , then adding some more changes.因此,“他们更改了什么”部分首先删除了沿顶行的两个o提交中的任何内容,然后添加了更多更改。

Meanwhile, what's in A -vs- E is whatever got changed in C , D , and E .同时, A -vs- E中的内容是CDE中发生的任何变化。 Git combines these two changes: add C+D+E, but also add M+F+G+H . Git 结合了这两个变化:增加了 C+D+E ,也增加了 M+F+G+H The add M step means take away what's in the two top-line o commits . add M步骤意味着删除两个顶行o commits 中的内容

Note that the merge base between commits A and B is commit * .请注意,提交AB之间的合并基础是 commit * If we had let Git combine the work in the three commits leading to A with the work in the three commits leading to B —if we had not made a mistake , in other words—we'd be in great shape now.如果我们让 Git 将导致A的三个提交中的工作与导致B的三个提交中的工作结合起来——换句话说,如果我们没有犯错的话——我们现在状态会很好。 The snapshot in M would combine these bits of work. M中的快照将结合这些工作。 It would not take away changes from the two unnamed o commits along the top.它不会顶部的两个未命名的o提交中删除更改。 But we deliberately didn't use commit * .但是我们故意使用 commit * We made the snapshot in M match the snapshot in B .我们使M中的快照与B中的快照相匹配。 We set up a time bomb, and our later merge explodes the time bomb.我们设置了一个定时炸弹,我们后来的合并引爆了定时炸弹。

Note that this problem is independent of the branch names .请注意,此问题与分支名称无关。 If we didn't use the name mistake , we would now have:如果我们不使用名称mistake ,我们现在将拥有:

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

This is the same commit graph and therefore contains the same time bomb.这是相同的提交图,因此包含相同的定时炸弹。 The bomb is embedded in the commits.炸弹嵌入在提交中。 The names we use to find the commits are quite irrelevant.我们用来查找提交的名称是完全不相关的。

How to fix the problem如何解决问题

There are several different ways around this problem.有几种不同的方法可以解决这个问题。 None of them is the Single One Right Way.它们都不是单一的正道。 All of them need to take the mistake into account.他们都需要考虑到错误。 The mistake was to set up a time bomb, so that this future merge will drop several commits—the unnamed ones along the top row.错误是设置了一个定时炸弹,这样未来的合并将丢弃几个提交——最上面一行的未命名的提交。

All ways to fix this involve creating new commits.解决此问题的所有方法都涉及创建新提交。 (That's pretty much the only thing you ever do in Git, after all.) The question is which new commits to create. (毕竟,这几乎是您在 Git 中所做的唯一事情。)问题是要创建哪些新提交。 We can choose any of several actions:我们可以选择几个动作中的任何一个:

  • Copy a whole bunch of commits, fixing our earlier mistake as we go.复制一大堆提交,修复我们之前的错误 go。
  • Go ahead and merge now, then copy the individual commits that got dropped. Go 提前并立即合并,然后复制已删除的各个提交。
  • Temporarily "fake out" the lack of a merge, using git replace .暂时“假装”缺少合并,使用git replace . (I'm not going to show this method, as it's complex and has repeatability issues.) (我不打算展示这种方法,因为它很复杂并且存在可重复性问题。)
  • Others that you can imagine and try out for yourself.其他你可以想象并自己尝试的。

Suppose we start with this:假设我们从这个开始:

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

That is, even if we used the name br2 all along, let's create a new name mistake pointing to commit H , then use git branch or git reset --hard to force the name br2 to point back to commit B :也就是说,即使我们一直使用名称br2 ,让我们创建一个新的名称mistake指向提交H ,然后使用git branchgit reset --hard强制名称br2指向提交B

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

Now we'll git checkout commit B under the name br2 :现在我们将git checkout commit B名称为br2

git checkout br2

to get:要得到:

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

Method 1: copy commits, omitting the mistake方法一:复制提交,省略错误

We can now simply copy every commit after M , without bothering with commit M , using an en-masse git cherry-pick :我们现在可以简单地复制M之后的每个提交,而不用担心提交M ,使用git cherry-pick

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

This might have a bunch of merge conflicts.这可能有一堆合并冲突。 If so, we'll have to solve each one.如果是这样,我们将不得不解决每一个问题。

Having done this, we can now erase the name mistake entirely, if we wish, and pretend commits MFGH never even existed:完成此操作后,如果我们愿意,我们现在可以完全消除名称mistake ,并假装提交MFGH甚至不存在:

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

This amounts to removing the merge via git rebase .这相当于通过git rebase删除合并。 The rebase command is in essence just an en-masse cherry-pick with some branch name juggling, which is exactly what we just did. rebase 命令本质上只是一个带有一些分支名称杂耍的整体樱桃选择,这正是我们刚刚所做的。 Now we can merge commits E and H' in the normal way, using commit * as their merge base.现在我们可以以正常方式合并提交EH' ,使用 commit *作为它们的合并基础。

Note that we can leave the name mistake around, and maybe create a new name br3 , so that we get:请注意,我们可以留下名称mistake ,并可能创建一个新名称br3 ,这样我们得到:

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

or use develop as the branch that acquires the merge:或使用develop作为获取合并的分支:

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

The advantage to creating a new name is that this way, branch names only move forward.创建新名称的好处是这样,分支名称只会向前移动。 Any time we use git branch -f or git rebase to move a branch name "backwards" first, then forwards, that means that every clone of our repository must adjust any work they have done that depends on the names moving forward.任何时候我们使用git branch -fgit rebase来首先“向后”移动分支名称,然后向前移动,这意味着我们存储库的每个克隆都必须调整他们所做的任何工作,这取决于向前移动的名称。

Method two: merge, then copy commits to fix the mistake方法二:合并,然后复制提交修复错误

Alternatively, let's look at starting with this:或者,让我们从这个开始:

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

and just doing the merge now.现在就进行合并。 We run git checkout develop and then git merge br2 and get:我们运行git checkout develop然后git merge br2得到:

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

where commit M2 drops commits oo from the top, just as we noted.正如我们所指出的,commit M2从顶部删除提交oo So now let's just cherry-pick them in:所以现在让我们挑选它们:

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

The merge itself and the two cherry-picks might have merge conflicts.合并本身和两个樱桃选择可能有合并冲突。 If so, that's OK;如果是这样,那没关系; we just solve them.我们只是解决它们。 Now we have:现在我们有:

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

and the snapshot in the last o' commit has what we want.最后o'提交中的快照有我们想要的。

Method 3: copy the develop branch to bypass the mistake方法三:复制develop分支绕过错误

Last, we can use one other mass copy trick.最后,我们可以使用另一种批量复制技巧。 Instead of mass-copying FGH onto the end of B (whether by git cherry-pick or by git rebase ), let's mass-copy the entire top line .与其将FGH大量复制到B的末尾(无论是通过git cherry-pick还是通过git rebase ),让我们大量复制整个 top line For sanity sake, let's use a new name, keeping develop as the old one, for the moment.为了理智起见,让我们使用一个新名称,暂时保持旧名称的develop We'll call the new branch fixup and we'll point it to commit * , which is before the screwup starts:我们将调用新的分支fixup并将其指向 commit * ,这是在搞砸开始之前:

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

Now, with fixup checked out (as indicated by HEAD above), we force Git to copy commits ooCDE using git cherry-pick :现在,通过检查修复(如上面的HEAD所示),我们强制 Git 使用fixup git cherry-pick复制提交ooCDE

git cherry-pick HEAD..develop

This has no merge conflicts (ever) because these commits easily apply here, so now we have:这没有合并冲突(永远),因为这些提交很容易在这里应用,所以现在我们有:

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

We can now do the merge with git merge br2 .我们现在可以使用git merge br2 The merge base here is commit * , not commit A —commit A isn't on fixup —so the merge has the desired source snapshot:这里的合并基础是 commit * ,而不是 commit A fixup A不在修复中 - 所以合并具有所需的源快照:

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

We're now ready to do one last merge, with:我们现在准备进行最后一次合并,其中:

git checkout develop
git merge fixup

The merge base of these two commits is commit * again, so Git compares * vs E to see what we changed, and * vs M2 to see what "they" changed.这两个提交的合并基础是再次提交* ,因此 Git 比较*E以查看我们更改了什么,并比较*M2以查看“他们”更改了什么。 What "they" changed includes everything we changed, so with luck, this merge goes well, and makes a new commit automatically: “他们”更改的内容包括我们更改的所有内容,因此幸运的是,此合并进展顺利,并自动进行了新提交:

       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

We can now delete the name fixup entirely.我们现在可以完全删除名称fixup All we have done is add new commits: one to develop , and a whole new chain that we called fixup while we were doing it.我们所做的只是添加新的提交:一个 to develop ,以及一个我们在执行此操作时称为fixup的全新链。

If the commit is published, but not really shared (for example: if no one else on your team has its work depending on broken-branch ), you are perhaps in a situation where you can force push:如果提交已发布,但没有真正共享(例如:如果您的团队中没有其他人根据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

Otherwise: you can run git restore to create a new commit on top of the merge, with the exact same content as what was on broken-branch before the merge:否则:您可以运行git restore以在合并之上创建一个新提交,其内容与合并前broken-branch上的内容完全相同:

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

note : git restore was added fairly recently to git (v2.27), if you are stuck with an older git and can't upgrade, replace it with:注意git restore是最近添加到 git (v2.27) 中的,如果您坚持使用较旧的 git 并且无法升级,请将其替换为:

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

Is this the last commit in your branch?这是您分支中的最后一次提交吗? If yes - you can do the following: git reset --hard HEAD~1 and then git push -f如果是 - 您可以执行以下操作: git reset --hard HEAD~1然后git push -f

If not - you can try with: git rebase -i HEAD~{X}如果没有 - 您可以尝试: git rebase -i HEAD~{X}

X is the number of commits from the last commit in the branch to the commit you want to revert. X是从分支中的最后一次提交到要恢复的提交的提交数。

Then you can try and drop the commit.然后你可以尝试放弃提交。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM