git merge 在底层做的就是三方合并:基础、ours、theirs。这篇用小例子把它的行为一项项对出来:什么时候自动合并、什么时候冲突、冲突时 index 和 .git 里留下什么、改名会不会被识别、-X 和 -s 到底差在哪里。
$ git merge theirs 2>&1 | head -3; git diff HEAD~1 --stat | tail -1 Auto-merging f.txt Merge made by the 'ort' strategy. f.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) git merge-file -p ours base theirs 的输出与合并结果一致: True
对每个文件,git 取 merge-base 里的版本为基础,与 ours、theirs 各做一次比较,不重叠的改动两边都收。底层命令是 git merge-file,实测它的输出与合并结果完全一致。
$ git merge theirs 2>&1 | head -3; git status --short; sed -n 4,8p f.txt
Auto-merging f.txt
CONFLICT (content): Merge conflict in f.txt
Automatic merge failed; fix conflicts and then commit the result.
UU f.txt
第 4 行
<<<<<<< HEAD
第 5 行 ours 改
=======
第 5 行 theirs 改
$ git ls-files -u | sed -E 's/^([0-9]+) [0-9a-f]{40} ([123])/\1 <sha> stage\2/'
100644 <sha> stage1 f.txt
100644 <sha> stage2 f.txt
100644 <sha> stage3 f.txt
$ for s in 1 2 3; do printf "stage $s: "; git show :$s:f.txt | sed -n 5p; done
stage 1: 第 5 行
stage 2: 第 5 行 ours 改
stage 3: 第 5 行 theirs 改
$ cat .git/MERGE_HEAD | cut -c1-7; cat .git/MERGE_MSG | head -3; ls .git | grep -E "^MERGE_"
72fdde5
Merge branch 'theirs'
# Conflicts:
MERGE_HEAD
MERGE_MODE
MERGE_MSG工作区文件里有冲突标记;index 里同一路径保留三个阶段(1 基础、2 ours、3 theirs),可以用 git show :1:文件 取出;.git 里出现 MERGE_HEAD、MERGE_MODE 和 MERGE_MSG。
$ git merge theirs 2>&1 | head -2 Merge made by the 'ort' strategy. $ git merge theirs 2>&1 | head -2 Auto-merging f.txt CONFLICT (content): Merge conflict in f.txt $ git merge theirs 2>&1 | head -2 Auto-merging f.txt Merge made by the 'ort' strategy.
相同的改动不冲突;改动块相邻就算冲突(即使改的是不同的行);中间隔一行以上则能自动合并。
$ git merge theirs 2>&1 | head -3; git status --short CONFLICT (modify/delete): f.txt deleted in theirs and modified in HEAD. Version HEAD of f.txt left in tree. Automatic merge failed; fix conflicts and then commit the result. UD f.txt $ git merge theirs 2>&1 | head -3; git status --short; cat new.txt Auto-merging new.txt CONFLICT (add/add): Merge conflict in new.txt Automatic merge failed; fix conflicts and then commit the result. AA new.txt <<<<<<< HEAD ours 新增的 new ======= theirs 新增的 new >>>>>>> theirs
一边删除一边修改是 modify/delete(状态 UD),两边各自新增同名文件内容不同是 add/add(状态 AA)。
$ git merge theirs 2>&1 | head -4; ls; sed -n 5p renamed.txt Merge made by the 'ort' strategy. old.txt => renamed.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename old.txt => renamed.txt (100%) renamed.txt 第 5 行 ours 改
一边改名、一边修改内容时,ort 检测到改名,把修改应用到新名字上。检测依据见下一篇。
git merge -X ours:第 2 行='第 2 行 ours 改' 第 5 行='第 5 行 ours 改' 第 9 行='第 9 行 theirs 改' x.txt='x theirs 改' git merge -X theirs:第 2 行='第 2 行 ours 改' 第 5 行='第 5 行 theirs 改' 第 9 行='第 9 行 theirs 改' x.txt='x theirs 改' git merge -s ours:第 2 行='第 2 行 ours 改' 第 5 行='第 5 行 ours 改' 第 9 行='第 9 行' x.txt='x' (-X 只在冲突块选边,不冲突的块两边都收;-s ours 不收对方的任何改动,但仍产生一个有两个 parent 的合并提交) $ git cat-file -p HEAD | grep -c "^parent" 2
-X ours/-X theirs 只在冲突块上选边,其余改动都收;-s ours 不收对方任何改动,但仍记录一次有两个 parent 的合并。
$ git merge --abort; git status --short; ls .git | grep -E "^MERGE_" || echo "(MERGE_HEAD 等文件都没了)"; sed -n 5p f.txt (MERGE_HEAD 等文件都没了) 第 5 行 ours 改
git merge --abort 回到合并之前,清除 MERGE_HEAD 等文件。