diff --git a/base.md b/base.md
index f547db6..a1a53b5 100644
--- a/base.md
+++ b/base.md
@@ -1,3 +1,3 @@
\ No newline at end of file
(END)
指令集为:
第一行新增字母 a,然后回车(有换行符)
保持第二行不变
删除第三行(由于第三行是空行,实际上是鼠标移到第四行首,敲击删除键以删除回车符即可)
删除第三行的字母 b(无换行符)
第三行新增字母 b,然后回车(有换行符)
到这里为止就能解释之前为何 vscode 出现了删除标记,因为 diff 后的指令集中确实有删除操作,就是删除字母 b 所在行及其上方一共两行。前面提到,git 借助这一组指令集来完成文件的变更,因此,就可能存在两组或多组不同的指令集,但是最后的变更是一样的。在这个例子中,显然 git 生成的这组指令集和我们人为操作时感受到的”不太一样“。
原生 diff 和编辑器展示的差异
一般情况下,编辑器展示的 diff 更有助于我们阅读,但是对于理解 git 内部的操作并无太大帮助,而且有时候还会出现不一致的情况,这也是我偶然发现的。
拿 vscode 举例,diff 时如果在一个文件中查看,总共有三个颜色标识,蓝色代表编辑,红色代表删除,绿色代表新增。而如果用双列模式查看,则没有蓝色标识,蓝色的编辑此时用”左侧一行显示红色+右侧同一行显示绿色“来表示。双列展示新增时,左侧用 /////// 表示错位行,右侧对应的是绿色的新增行,删除则是相反。
这样的标识对于人的理解来说很方便,也完全够用。但是其忽视了”换行符“这个细节,前面提到,换行符是归属于当前行的,而我们新增一行时,实际上也更改了当前行,而这个更改在 vscode 的 diff 中就没有表现出来。
除此之外,再来看看我偶然发现的一个不一致的情况,还是回到分支 base 的最初状态,文件 base.md 内容如下:
当变更为:
使用上述两种变更时,vscode 展示的 diff 和 git diff 中对字母 a 这一行的操作是一致的(前者是编辑,后者是新增)。
但是如果我变更如下:
vscode 中显示的对字母 a 这一行的操作是编辑,而 git diff 显示的操作是仅新增:
这里算是一个很小的细节,甚至说是隐藏的。而且我也不知道这是否算是 vscode 编辑器的一个 bug,如果大家有了解的,可以评论区留言。
相同的操作,不同的 git diff
关于 git diff 生成的指令集,我还发现在看似”相同“的操作下,生成的指令集却不”相同“的情况。在上面部分,我的变更从仅新增 a,到 b 这一行后新增一行,再到 b 这一行后新增两行。这里还是遵循这样的规律,由仅新增 a,到 b 末尾新增一行,直到 b 末尾新增 4 行,分别看看他们的 git diff:
不难看出,仅增加 a 时,是对 a 所在行的编辑,然后是我们分支 base 变更的情况。当文件末尾增加到 2 行时,变成了先增加 b 再删除原来的 b。增加到 3 行时,会在增加的 a 和 b 两行间加 1 行。直到最后才变得规律,在增加的 b 所在行后面分别增加所需的空行。
看起来有点无规律可循。由于本文并没有深入研究到 git 的 diff 引擎算法层面,提到这个,只是想进一步说明,git diff 生成的指令集,确实和我们常规思考的不一样。认识到这一点以后,我们回到主题,来看看冲突产生的根源。
从 git diff 再到合并冲突
git 背后生成了一套与我们所想不同的操作指令集,这和合并产生的冲突有什么关系?实际上,git merge 时,git 会找到两个分支的最近公共 commit,基于这个 commit,git 对两个分支分别执行 diff 得到两套 diff 指令集,git 会尝试合并这两套指令集来完成 merge,一旦有指令发生操作上的重叠,git 便会提示冲突。因此,合并冲突,实际上是指令集的冲突,产生什么样的冲突,和 diff 生成什么样的指令集是密切相关的。
回到上面的冲突,为了更好地说明冲突是如何产生的,下面我使用 merge.conflictstyle diff3 来展示冲突的文件内容,你可以通过 git checkout --conflict=diff3 来使用,也可以通过 git config --global merge.conflictstyle diff3 进行全局配置。这种形式下,会多出一个 merge base,可以理解为这两个变更最近的一次共同 commit 内容。
采用 diff3 来展示冲突:
01: a
03: <<<<<<< ours
04: b
05: ||||||| base
07: b
08: =======
10: b
12: c
13: >>>>>>> theirs
这样一看,就清晰多了。在 git 看来,两者冲突,是因为分支 base 是想删除 b 上方一行,而分支 feat 则是想保留这一行并新增下面两行。那为什么分支 base 这里会解析出删除操作?就是因为前面的指令集。下面我们再对比看看这两组指令集:
分支 base 的 diff 指令集为:
第一行新增字母 a,然后回车(有换行符)
保持第二行不变
删除第三行(由于第三行是空行,实际上是鼠标移到第四行首,敲击删除键以删除回车符即可)
删除第三行的字母 b(无换行符)
第三行新增字母 b,然后回车(有换行符)
分支 feat 的 diff 指令集为:
保持第一行不变
保持第二行不变
删除第三行的字母 b(无换行符)
第三行新增字母 b,然后回车(有换行符)
第四行由于是空行,直接回车(有换行符)
第五行新增字母 c,无需回车(无换行符)
注意看,前者的第 4-5 条指令和后者的 3-4 条指令完全重合。对于前两行,并无冲突(只要一方为保持不变,则应用另外一方的变更),因此直接应用即可。这里的冲突在于前者的第 3-5 条指令和后者的 3-6 条指令的冲突,尤其是前者的第 3 条指令指出需要先删除第三行(也就是字母 b 上方的空行),这也解决了为什么这里有删除操作,以及删除的是什么这两个问题。
我的疑问和看法
回顾一下冲突展示结果:
01: a
03: <<<<<<< ours
04: b
05: ||||||| base
07: b
08: =======
10: b
12: c
13: >>>>>>> theirs
对于上面的冲突结果,不知道大家有没有和我一样,持有一个疑问:注意末尾最后一行是在冲突之外的,而我认为更合适的是它位于第 5 行:
01: a
03: <<<<<<< ours
04: b
06: ||||||| base
08: b
09: =======
11: b
13: c
14: >>>>>>> theirs
这样更符合指令的操作情况,而且当接收 theirs 也就是分支 feat 的变更时,无需再手动去删除末尾这一行,这样更接近分支 feat 本来的变更。关于这个疑问,如果有对 merge 更了解的,也欢迎评论区告诉我。
除了这个疑问,关于 diff 生成这样的指令,而不是更接近我们思考的一组。我猜想这也许是 git 的 diff 引擎算法所做的一些权衡,只不过这个权衡恰好在我这个特殊的例子中没有发挥出它应有的价值。例如,假设 git diff 对于分支 base 的变更生成的是如下一组指令:
删除第一行(由于第一行是空行,实际上是鼠标移到第二行首,敲击删除键以删除回车符即可)
第一行新增字母 a,然后回车(有换行符)
保持第二行不变
删除第三行的字母 b(无换行符)
第三行新增字母 b,然后回车(有换行符)
此时再对比分支 feat 的 diff 指令:
保持第一行不变
保持第二行不变
删除第三行的字母 b(无换行符)
第三行新增字母 b,然后回车(有换行符)
第四行由于是空行,直接回车(有换行符)
第五行新增字母 c,无需回车(无换行符)
此时产生的冲突则是前者的第 4-5 条指令和后者的第 3-6 条指令,展示结果如下:
01: a
03: <<<<<<< ours
04: b
05: ||||||| base
06: b
07: =======
08: b
10: c
11: >>>>>>> theirs
这样的冲突,除了仍然有我上面疑问部分提出的问题,看起来显然更符合我们的思考过程(也是我在上面的场景还原部分提出来的直观感受)。
除了上面我的一个疑问,这个问题就算解决了。回忆这次的解决过程,还是挺艰难的,我找不到相关的材料,而且由于问题太细,可能没有人注意和关注过。为此我在 StackOverflow 上提出了两个问题,分别是 Why git diff have different behaviors? 和 Why git changes the line order when merge conflicts occurs?。从原始问题的标题就能知道,我当时并不了解 git diff 和 merge 的本质,提的问题标题都是用很表面的文字陈述,幸运的是,马上就有热心的歪果仁解答了我的疑惑,且最终收获满满。例如文中的 git diff 产生的不同指令集,以及更直观展示冲突的 diff3 样式,甚至最后我提出的假设性看法,都源于这两个提问。
这里要感谢 torek 的耐心解答,他在回答中也提出了 git 的 diff 引擎基于一些算法来生成最终的变更指令集。我们甚至可以通过一些参数,例如 indent-heuristic 来调整这一行为,只不过刚好对于我这个例子来说,调整貌似没有什么效果。
关于这两个提问,感兴趣的可以过去看看,其中大部分结论我都写在了这篇文章之中。希望大家对 git diff 和 merge 有了比以往更深入的见解。