假如有一个 commit ,你在刚把它写完的时候并没有觉得它不好,可是在之后又写了几个提交以后,你突然灵光一现:「哎呀,那个 commit 不该写,我要撤销!」
想丢弃的也不是最新的提交?
不是最新的提交,就不能用 reset --hard 来撤销了。这种情况的撤销,就要用之前介绍过的一个指令:交互式 rebase ——rebase -i。
用交互式 rebase 撤销提交
之前介绍过,交互式 rebase 可以用来修改某些旧的 commits。其实除了修改提交,它还可以用于撤销提交。比如下面这种情况:


你想撤销倒数第二条 commit,那么可以使用 rebase -i:
|
|

然后就会跳到 commit 序列的编辑界面,这个在之前的第 10 节里已经讲过了。和第 10 节一样,你需要修改这个序列来进行操作。不过不同的是,之前讲的修正 commit 的方法是把要修改的 commit 左边的 pick 改成 edit,而如果你要撤销某个 commit ,做法就更加简单粗暴一点:直接删掉这一行就好。

pick 的直接意思是「选取」,在这个界面的意思就是应用这个 commit。而如果把这一行删掉,那就相当于在 rebase 的过程中跳过了这个 commit,从而也就把这个 commit 撤销掉了。

现在再看 log,就会发现之前的倒数第二条 commit 已经不在了。
|
|

用 rebase –onto 撤销提交
除了用交互式 rebase ,你还可以用 rebase --onto 来更简便地撤销提交。
rebase 加上 --onto 选项之后,可以指定 rebase 的「起点」。一般的 rebase,告诉 Git 的是「我要把当前 commit 以及它之前的 commits 重新提交到目标 commit 上去,这其中,rebase 的「起点」是自动判定的:选取当前 commit 和目标 commit 在历史上的交叉点作为起点。
例如下面这种情况:

如果在这里执行:
|
|
那么 Git 会自动选取 3 和 5 的历史交叉点 2 作为 rebase 的起点,依次将 4 和 5 重新提交到 3 的路径上去。
而 --onto 参数,就可以额外给 rebase 指定它的起点。例如同样以上图为例,如果我只想把 5 提交到 3 上,不想附带上 4,那么我可以执行:
|
|
--onto 参数后面有三个附加参数:目标 commit、起点 commit(注意:rebase 的时候会把起点排除在外)、终点 commit。所以上面这行指令就会从 4 往下数,拿到 branch1 所指向的 5,然后把 5 重新提交到 3 上去。

同样的,你也可以用 rebase --onto 来撤销提交:
|
|
上面这行代码的意思是:以倒数第二个 commit 为起点(起点不包含在 rebase 序列里哟),branch1 为终点,rebase 到倒数第三个 commit 上。
也就是这样:

小结
这节的内容是「撤销过往的提交」。方法有两种:
- 用
git rebase -i在编辑界面中删除想撤销的commits - 用
git rebase --onto在rebase命令中直接剔除想撤销的commits
方法有两种,理念是一样的:在 rebase 的过程中去掉想撤销的 commit,让他它消失在历史中。
代码已经 push 上去了才发现写错?
有的时候,代码 push 到了中央仓库,才发现有个 commit 写错了。这种问题的处理分两种情况:
1. 出错的内容在你自己的 branch
假如是某个你自己独立开发的 branch 出错了,不会影响到其他人,那没关系用前面几节讲的方法把写错的 commit 修改或者删除掉,然后再 push 上去就好了。不过……

由于你在本地对已有的 commit 做了修改,这时你再 push 就会失败,因为中央仓库包含本地没有的 commits。但这个和前面讲过的情况不同,这次的冲突不是因为同事 push 了新的提交,而是因为你刻意修改了一些内容,这个冲突是你预料到的,你本来就希望用本地的内容覆盖掉中央仓库的内容。那么这时就不要乖乖听话,按照提示去先 pull 一下再 push 了,而是要选择「强行」push:
|
|
-f 是 --force 的缩写,意为「忽略冲突,强制 push」。

这样,在本地修改了错误的 commits,然后强制 push 上去,问题就解决了。
出错的内容已经合并到 master
这就不能用上面那招了。同事的工作都在 master 上,你永远不知道你的一次强制 push 会不会洗掉同事刚发上去的新提交。所以除非你是人员数量和行为都完全可控的超小团队,可以和同事做到无死角的完美沟通,不然一定别在 master 上强制 push。
在这种时候,你只能退一步,选用另一种策略:增加一个新的提交,把之前提交的内容抹掉。例如之前你增加了一行代码,你希望撤销它,那么你就做一个删掉这行代码的提交;如果你删掉了一行代码,你希望撤销它,那么你就做一个把这行代码还原回来的提交。这种事做起来也不算麻烦,因为 Git 有一个对应的指令:revert。
它的用法很简单,你希望撤销哪个 commit,就把它填在后面:
|
|
上面这行代码就会增加一条新的 commit,它的内容和倒数第二个 commit 是相反的,从而和倒数第二个 commit 相互抵消,达到撤销的效果。
在 revert 完成之后,把新的 commit 再 push 上去,这个 commit 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit。
小结
这节的内容是讲当错误的 commit 已经被 push 上去时的解决方案。具体的方案有两类:
- 如果出错内容在私有
branch:在本地把内容修正后,强制push (push -f)一次就可以解决; - 如果出错内容在
master:不要强制push,而要用revert把写错的commit撤销。