git revert merge commit

前言

本文将结合一个标准的git工作流,以及具体的例子,来引出何种条件,会触发 git revert 的失败。通过本文,你将学到

巩固 git 的使用和理解;

出现问题能够放心大胆并以正确的姿势来 revert。

准备

为了更好地描述本文,我们将从头开始进行一些准备工作。

首先,创建一个repo:

mkdir git-revert-merge-commit
cd git-revert-merge-commit
git init

首先创建两个 commit 来模拟 master 上现有的 commit 记录:

echo 'file1' > file1 
git add . && git commit -m 'commit 1'
echo 'file2' > file2
git add . && git commit -m 'commit 2'

在项目不断的演进过程中,我们会不断地做新功能。假设现在我们要做一个新 feature,为了不 block master 的 CI pipeline,所有的 feature 应该基于新的 branch 来开发。于是,我们创建一个 branch, 并切换到该 branch:

git branch feature
git checkout feature # 或者使用 git checkout -b feature 一键完成上述两步

首先创建两个 commit 来模拟我们完成了这个 feature:

echo 'file3' > file3
git add . && git commit -m 'feature - commit 1'
echo 'file4' > file4
git add . && git commit -m 'feature - commit 2'

在开发 feature 的阶段中,master 上通常会有其他人新进入的 commit,于是我们回到 master,来模拟一下这些 commit:

git checkout master
echo 'file5' > file5
git add . && git commit -m 'commit 3'
echo 'file6' > file6
git add . && git commit -m 'commit 4'

此时,QA们测完了你的 feature,给了你 thumbup (测试通过) 的 tag,模拟一下这个tag:

git co -
git tag thumbup-feature

然后,你就可以用这个 tag merge 了:

git checkout master
git pull origin master # 拉取远程 master 合并到本地 master
git merge thumbup-feature --no-ff # --no-ff 是为了避免 fast-forward
Merge tag 'thumbup-feature'
  
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

查看一下 git log 图:

g@g /o/w/f/git-revert-merge-commit> git lg
*   805486f - (HEAD -> master) Merge tag 'thumbup-feature' (68 seconds ago) <Your Name>
|\  
| * 562ca76 - (tag: thumbup-feature, feature) feature - commit 2 (2 minutes ago) <Your Name>
| * af29e4d - feature - commit 1 (2 minutes ago) <Your Name>
* | ec0ce14 - commit 4 (2 minutes ago) <Your Name>
* | 0d10636 - commit 3 (2 minutes ago) <Your Name>
|/  
* 3ed548c - commit 2 (2 minutes ago) <Your Name>
* 31309e6 - commit 1 (2 minutes ago) <Your Name>

后来,master 上又多了一些 commit:

echo 'file7' > file7
git add . && git commit -m 'commit 5'

好了,到这里,很多开发者觉得自己的事已经做完了,不,此时生产环境的 UT 由于你的 merge 挂了,由于不能 quick fix,也不能 block release,所以只好暂时决定 revert 你的 change,好吧,那就来 revert 吧。

首先,看一下 git log:

g@g /o/w/f/git-revert-merge-commit> git lg
* 0979cf2 - (HEAD -> master) commit 5 (24 seconds ago) <Your Name>
*   805486f - Merge tag 'thumbup-feature' (2 minutes ago) <Your Name>
|\  
| * 562ca76 - (tag: thumbup-feature, feature) feature - commit 2 (3 minutes ago) <Your Name>
| * af29e4d - feature - commit 1 (3 minutes ago) <Your Name>
* | ec0ce14 - commit 4 (3 minutes ago) <Your Name>
* | 0d10636 - commit 3 (3 minutes ago) <Your Name>
|/  
* 3ed548c - commit 2 (3 minutes ago) <Your Name>
* 31309e6 - commit 1 (3 minutes ago) <Your Name>

找到 merge 的 commit id, 来 revert 吧:

git revert 805486f

哈哈,问题就来了:

g@g /o/w/f/git-revert-merge-commit> git revert 805486f
error: commit 805486fd85f7bd6269b0bae5f3f1210edb62af40 is a merge but no -m option was given.
fatal: revert failed

终于出现了该问题,nice。

当然,由于程序员的最强本性是懒,所以我也为想动手的同学准备好了一键运行脚本:

git init 后 copy 到 iTerm 中运行以下脚本,最后 找到你的 merge commit id 尝试 git revert

echo 'file1' > file1; \
git add . && git commit -m 'commit 1'; \
echo 'file2' > file2; \
git add . && git commit -m 'commit 2'; \
git branch feature; \
git checkout feature; \
echo 'file3' > file3; \
git add . && git commit -m 'feature - commit 1'; \
echo 'file4' > file4; \
git add . && git commit -m 'feature - commit 2'; \
git checkout master; \
echo 'file5' > file5; \
git add . && git commit -m 'commit 3'; \
echo 'file6' > file6; \
git add . && git commit -m 'commit 4'; \ 
git co -; \
git tag thumbup-feature; \
git checkout master; \
git pull origin master; \
git merge thumbup-feature --no-ff; \
echo 'file7' > file7; \
git add . && git commit -m 'commit 5';

分析

再次给出错误信息:

error: commit 805486fd85f7bd6269b0bae5f3f1210edb62af40 is a merge but no -m option was given.

我们来看看 -m 到底指的是什么, 援引 官方文档, 可以看到:

Usually you cannot revert a merge because you do not know which side of the merge should be considered the mainline. This option specifies the parent number (starting from 1) of the mainline and allows revert to reverse the change relative to the specified parent.

Reverting a merge commit declares that you will never want the tree changes brought in by the merge. As a result, later merges will only bring in tree changes introduced by commits that are not ancestors of the previously reverted merge. This may or may not be what you want.

翻译过来就是:

通常情况下,你无法 revert 一个 merge,因为你不知道 merge 的哪一条线应该被视为主线。这个选项(-m)指定了主线的 parent 的代号(从1开始),并允许以相对于指定的 parent 的进行 revert。

revert 一个 merge commit 意味着你将完全不想要来自 merge commit 带来的 tree change。 因此,之后的 merge 只会引入那些不是之前被 revert 的那个 merge 的祖先引入的 tree change,这可能是也可能不是你想要的。

听起来很绕口,简单解释一下,由于 merge commit 是将两条线合并到一条线上,因此,合并时的那个commit,将具有两个祖先。所以 git 不知道 base 是选择哪个 parent 进行 diff,所以就抱怨了,所以你要用 -m 属性显示地告诉 git 用哪一个 parent。

那么,如何查看当前的commit有几个祖先呢?

g@g /o/w/f/git-revert-merge-commit> git show 805486f
commit 805486fd85f7bd6269b0bae5f3f1210edb62af40
Merge: ec0ce14 562ca76
Author: Your Name <you@example.com>
Date:   Sat Oct 19 16:36:34 2019 +0800

    Merge tag 'thumbup-feature'

复制代码可以看到,Merge 这个字段便标明了当前的parent,分别是 ec0ce14562ca76

接下来,我们用 git log 图来描述一下这两个 parent:

g@g /o/w/f/git-revert-merge-commit> git lg3
* 0979cf2 - (7 minutes ago) commit 5 - Your Name (HEAD -> master)
*   805486f - (9 minutes ago) Merge tag 'thumbup-feature' - Your Name
|\  
| * 562ca76 - (9 minutes ago) feature - commit 2 - Your Name (tag: thumbup-feature, feature)
| * af29e4d - (9 minutes ago) feature - commit 1 - Your Name
* | ec0ce14 - (9 minutes ago) commit 4 - Your Name
* | 0d10636 - (9 minutes ago) commit 3 - Your Name
|/  
* 3ed548c - (9 minutes ago) commit 2 - Your Name
* 31309e6 - (10 minutes ago) commit 1 - Your Name

是不是很直观?又有人会问了,为什么 master 是 parent1,而 branch 的最后一个 commit 是 parent2。是这样的,当你在 B 分支上把 A merge 到 B 中,那么 B 就是merge commit 的 parent1,而 A 是 parent2。

解决

有了上一节的分析,我们可以很直接地给出以下可用的代码:

git revert cae5381 -m 1

复制代码输出以下log:

Revert "Merge tag 'thumbup-feature'"
  
This reverts commit 805486fd85f7bd6269b0bae5f3f1210edb62af40, reversing
changes made to ec0ce14e91f9a232dddd200c2fcaa8efd7672945.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
#       deleted:    file3
#       deleted:    file4

复制代码:wq 退出看到:

[master 5611368] Revert "Merge tag 'thumbup-feature'"
 2 files changed, 2 deletions(-)
 delete mode 100644 file3
 delete mode 100644 file4

复制代码file3 和 file4 是 feature branch 上的 commit 引入的文件,被正确地删掉了,cool。 那么 git revert cae5381 -m 2 到底会发生什么呢? 或许你已经有答案,还是来试试看: 首先 reset hard 到 revert 前的最后一个commit,让我们可以再次尝试另一种 revert,这里,你可以用更可爱的 git reflog。

git reset --hard 0979cf2 
git revert 805486f -m 2

复制代码[master 2c5a0ee] Revert "Merge tag 'thumbup-feature'"

[master 5241785] Revert "Merge tag 'thumbup-feature'"
 2 files changed, 2 deletions(-)
 delete mode 100644 file5
 delete mode 100644 file6

果然,这种 revert 把 master 在 feature branch 期间进行的 commit 都给干掉了。哈哈,很正常,因为此时 diff 是基于 feature branch 的。

说到这里,你是不是豁然开朗了呢?是不是也知道再遇到这种问题,心里有一股底气去revert呢?

本文代码均已放到这个仓库上 git-revert-merge-commit

结论

结论如下:

  • 对于单一 parent 的 commit,直接使用 git revert commit_id ;
  • 对于具有多个 parent 的 commit,需要结合 -m 属性:git revert commit_id -m parent_id` ;
  • 对于从 branch 合并到 master 的 merge commit,master 的 parent_id 是1,branch 的 parent_id 是2, 反之亦然;

最后,附上我自定义的可爱的 git log 的配置(copy到~/.gitconfig中的 [alias] 下即可):

lo = log --color --pretty=format:' %C(white bold dim) %C(cyan bold dim)[%h] %Creset %Cgreen%cn %C(white bold dim)| %C(yellow dim)%cr %n%Creset  %C(white dim italic)%s%n%n%Creset'

话说起来,一年前我有一个如何用 shell 来 enhance git 工作流的库 git-shell,当前,目前没有任何文档,有兴趣的同学可以看一下思路,后面我会写好文档并另开博文。

本文就到这里,谢谢阅读。

当你决定去 revert 一个merge commit

  • qq_43638135
    妲己再美究为妃: 博主没有想过自己接一些私活干吗?我现在还没毕业,但是我也确实听说外挂市场自动化游戏脚本市场挺火热的,并且报酬也很丰厚,但是具体的我也不是很清楚,求解答。 (1个月前 #47楼) 查看回复(2) 举报 回复
    22