被回退的功能,再 merge 一次也回不来
一句话总结
revert 会创建一个把改动反向抵消的新提交。因此,要撤销一个合并提交,必须指明以哪一侧为基准来抵消(-m);而被撤销的功能分支以后再次 merge 时,git 会回答“已经合并过了”。因为历史还在,只是内容消失了。
为什么需要它
发布的第二天出了故障。原因是昨天合并的功能分支,而且已经有很多人拉取了那段历史。不能用 reset——那等于删除已经传到别人手里的提交,所有人的历史都会错位。所以要用 revert。
然而,当你想撤销那个合并提交时,git 会停下来。
$ git revert 685e75f
error: commit 685e75f is a merge but no -m option was given.
fatal: revert failed
合并提交有两个父提交。“撤销”意味着回到某个状态,但有两个父提交时,回到哪个父提交的状态就无法确定。-m 1 表示以第一个父提交(通常是被合并进去的一侧,也就是 main)为基准,去抵消第二个父提交带来的改动。-m 2 则相反。
到这里,大多数人都是知道的。真正的陷阱在后面。
工作原理
用 revert -m 1 撤销合并之后,修好功能想再次合入,执行 git merge feature,会得到这样的输出。
Already up to date.
这看起来像 bug,其实是准确的回答。merge 依据的是历史的可达性。那个合并提交仍在历史中,feature 的所有提交从那里都可达,所以在 merge 看来,已经没有需要合并的东西了。之所以没有内容,是因为后面的 revert 提交把它抹掉了,而不是因为没合并过。
因此,恢复被撤销的功能,办法是对 revert 再做一次 revert。
git revert <되돌림 커밋>
git 2.x 会给这个提交自动加上默认标题 Reapply "..."。历史中同时保留撤销和恢复两条记录,以后想知道“这个功能为什么进来了两次”,只读历史就能明白。Linux 内核文档推荐这种模式,理由也一样——历史应当如实讲述实际发生过的事。
给 cherry-pick 留下出处
在发布分支上做热修复,再把它也合入 main,是很常见的事。直接 cherry-pick 的话,新提交里不会留下原提交来自哪里。加上 -x,提交消息的末尾会多出一行。
(cherry picked from commit fb42e70c24e9bab398dcef64c3b2bd316de8ca4e)
以后调查“这个修复有没有也进入发布分支”时,有没有这一行差别很大。不过从私有仓库迁到公开仓库时,它会指向一个不存在的哈希,那时就不要使用。
相同的改动是否已经合入——patch-id
“这个提交是否已经合入那个分支”,光看哈希是看不出来的,因为 cherry-pick 会创建新的提交。git 改用 patch-id——它是把 diff 中的空白和行号去掉之后计算出的哈希,所以只要是相同的改动,即使提交哈希不同,也会得到相同的值。
$ git show <원본> | git patch-id --stable
37d09d56... fb42e70c...
$ git show <가져온 것> | git patch-id --stable
37d09d56... 075fc52e...
git cherry -v <upstream> <head>(占位符依次为上游与当前分支)会替你完成这个计算。开头的 - 表示该改动已经在 upstream 中,+ 表示还没有。
这里有一个重要的局限。解决冲突后带过来的提交,patch-id 会变。 因为上下文变了,diff 也就不同。所以 git cherry 会把那个提交标为 +(尚未存在)。如果完全相信自动判定而再次带入,同样的修复就会进来两次——这个工具回答的问题是“是否存在相同的 diff”,而不是“是否已经体现了相同的意图”。
在现场相遇的样子
- 把发布分支上的热修复带到
main时,把-x定为规则。 - 撤销了有问题的功能,修好后再次合入时,只看恢复提交的标题,就不会再有人问“为什么进来两次”。
- 向长期支持分支做 backport 时,先用
git cherry -v数出剩余的,对于通过解决冲突处理过的条目,在清单上手动标记。 - 知道冲突中可以用
git cherry-pick --abort干净地退回去,就能减少把解决了一半的状态直接提交的事故。
下一项实验要做什么
先创建一个合并提交,在不带 -m 的情况下撤销它,看它被拒绝。用 -m 1 撤销之后,确认 git merge 输出 “Already up to date”,再通过 revert 的 revert 把它恢复。接着用 -x 带入热修复,其中一个会冲突的提交先用 --abort 退回去,再用 --continue 完成。最后用 git cherry 和 git patch-id 数一数哪些已经合入,就能亲眼看到为什么解决过冲突的提交仍保持 +。