因为最近帮别人解决了一次git merge丢代码的情况,当时处理地简单粗暴,reset到merge前一次commit后重来一遍。后来又设想了一下,如果merge后又commit很多次,这样岂不会很麻烦?如果这个分支不允许force push,那这样做也不行。于是就去查了一些资料,自己尝试并总结了一下几个方法。


用git reset重新来过

这种方法很简单,就是git reset到merge之前的一次commit,然后重新merge,然后如果之前在merge之后还有commit的话利用git cherry-pick <commit start>^...<commit end>apply到当前分支。最后git push --force到远端分支(如果之前已经push了的话)

这种方法适合你在push之前就发现了错误的情况,因为用了git reset后当前分支就必须要force push了。

force push有什么问题?
一般来说不应该在非私人分支上force push,因为这有可能会覆盖掉其他人在该分支上的commit。而且远端的protected branch是不能直接force push的。

更好的方法?

首先解决第一个问题,如何不用force push?

如果我们不修改要push分支的历史的话是不需要force push的,那么很简单了,只要把需要修改历史的reset操作在另外一个不需要push的分支上操作就行了。

实际操作就是从当前分支拉一条新分支出来,然后进行上面reset的方法,最后再rebase或merge回当前分支。

既然这里也提到了rebase,其实我们还可以简化一下上面的方法。

# 假设我们这里merge feature到master
# 使用第一种方法
git checkout -b fix-merge
git reset <commit before merge> --hard
git merge feature # fix conflict
git cherry-pick <commit start>^...<commit end> # maybe need fix conflict
git checkout master
git merge fix-merge # fix conflict

# 用rebase简化一下,因为rebase也是一个会修改历史的操作,所以还是需要新开分支
git checkout -b fix-merge
# 用rebase代替了reset > merge > cherry-pick的操作
git rebase -i <commit before merge> # fix conflict
git checkout master
git merge fix-merge # fix conflict

这里在rebase时添加了-i的参数,可以清楚地展示和操作每次会应用的commit。如果不懂rebase可以看看官方文档

用上面的方法要处理的冲突太多了怎么办?

无论是cherry-pick还是rebase都是将commit一次次给apply,所以merge之后的commit如果有冲突,那也得一次次解决,有时候一直要改同一个文件的话,那解决冲突就是很痛苦的一件事了。

这里有两个方案可以考虑一下:

  1. 其实在第一种方法中完全没必要用cherry-pick,新分支在重新merge后可以直接merge到当前分支的。当然这样还是一样要处理很多冲突,只不过可以一次处理完了。

    git checkout -b fix-merge
    git reset <commit before merge> --hard
    git merge feature # fix conflict
    git checkout master
    git merge fix-merge # fix large conflict
    
  2. 如果是丢了代码并知道丢的是哪次提交的代码,那么可以直接git cherry-pick <commit id>

  3. 直接改代码。

git revert可以解决问题吗?

我在网上也看到很多用git revert来解决错误merge的方法,但是这样做只能撤销merge而无法去修复merge,而且就算提交了revert commit,当前分支也不能重新merge,因为revert只是普通的commit而不能改变已经存在的merge commit。