合并冲突
编辑器里看着已解决,Git 仍可能拒绝继续。Git 用索引状态判断路径是否解决,不是看工作文件有没有冲突标记。标记是文本,索引阶段才是状态。
编辑工作文件只是改草稿,git add 才把该版本交给索引。因此没有标记的文件仍可能未合并,而保留标记的文件也可能被正常暂存。
合并冲突如何工作
分叉合并使用三份不可变快照:base 是共同旧提交,ours 是合并前 HEAD,theirs 是 merge 指定的源末端。
本课模型只有一个文件、三行原位修改、最多一个冲突块。逐行使用四条规则:两侧相同取任一侧;ours 等于 base 取 theirs;theirs 等于 base 取 ours;三者都不同则冲突。Git 按块合并,不按单行:一侧的修改与另一侧的修改重叠或相接、中间没有未改动的行时,两处修改落进同一个冲突块,即使单看某一行有规则可用。
第一行 base 是 title=VizLab,两侧都独立改为 title=VizLearn,所以按两侧相等规则接受 VizLearn,不是因为 HEAD 优先,也不是因为与 base 相同。
冲突不会创建半成品提交。HEAD 和当前分支留在合并前,MERGE_HEAD 保存源末端。未合并路径的索引移除 stage 0,保存 stage 1 的 base、stage 2 的 ours、stage 3 的 theirs。工作文件写入结果及标记:
title=VizLearn
<<<<<<< HEAD
accent=navy
=======
accent=teal
>>>>>>> feature
footer=v1
删除标记还不够。git add app.txt 对当前工作字节计算 blob,移除阶段 1/2/3,写入一个 stage 0。随后 git merge --continue 才能从索引写树、创建有序双父提交、推进当前分支并清除 MERGE_HEAD。
add 只保存字节,不判断内容好坏。若标记尚在,也可被写入 stage 0,随后作为普通文本提交。
合并冲突逐步演示
默认有十个操作:
commit base: title=VizLab | accent=blue | footer=v1
git branch feature
commit main-theme: title=VizLearn | accent=navy | footer=v1
git switch feature
commit feature-theme: title=VizLearn | accent=teal | footer=v1
git switch main
git merge feature
edit app.txt: title=VizLearn | accent=indigo | footer=v1
git add app.txt
git merge --continue
- base 保存
title=VizLab和accent=blue,main、feature 最初都指向它。 - main-theme 把标题改为 VizLearn,第二行改为
accent=navy;feature-theme 做相同标题修改,但选择accent=teal。两个末端共享基点,互不包含。 - 回到 main 合并,固定 ours=main-theme、theirs=feature-theme,唯一基点为 base。
MERGE_HEAD指向 feature,HEAD 不动。 - 第一行两侧同为 VizLearn,接受;第三行都未改;第二行 base=blue、ours=navy、theirs=teal,三值不同,产生一个冲突块。
- app.txt 索引变成阶段 1/2/3,工作文件展开为七行标记形式。新增提交零个,当前引用写入零次。
- 编辑器用
accent=indigo替换标记块,只改工作区。索引仍有阶段 1/2/3,路径仍未合并。 git add app.txt将三行解决结果保存为 stage 0 blob,三个未合并阶段消失。MERGE_HEAD仍在,分支不动。- continue 确认没有未合并路径,从 stage 0 写树,创建合并提交:第一父为合并前 main,第二父为固定的 feature 末端。
- 最后 main 前进,feature 不动,清除
MERGE_HEAD。索引和工作区保留accent=indigo。
两张下方卡片之间的箭头显示数据方向:冲突呈现从三阶段索引写到工作区;add 则把编辑结果从工作区写回 stage 0。
索引为何需要三个阶段
阶段号属于索引键。普通 app.txt 只有 stage 0,内容冲突时同一路径需要同时保存三个 blob 身份:
| 阶段 | 角色 | 本次合并含义 |
|---|---|---|
| 1 | base | 共同祖先中的文件 |
| 2 | ours | 合并前 HEAD 中的文件 |
| 3 | theirs | MERGE_HEAD 中的文件 |
| 0 | resolved | add 选定的单一版本 |
三个 blob 保留合并工具所需证据,比一个红色冲突标志包含更多信息。工作文件只是可编辑提案;stage 0 替换未合并项后,Git 知道下一棵树的精确字节,但不保证其逻辑正确。
本引擎收集每个末端的祖先为可达提交图上的 O(V + E);再从共同祖先中挑最佳者,每对共同祖先各遍历一次图,最坏 O(V²·(V + E)),八提交上限让它很小。逐行处理为 O(L + B),L 为逻辑行数,B 为字节数。结果哈希、树和提交序列化与相应内容大小线性相关。这些是当前引擎的界限,不是生产 ort 的性能声明。
处理冲突的步骤
- 用
git status检查 unmerged paths,确认 Git 认为哪些路径未解决。 - 用
git ls-files -u查看索引三份输入:1 为 base,2 为 ours,3 为 theirs。 - 编辑文件,决定程序应保留的行为,运行相关测试。此时只是候选解决结果。
git add app.txt,再用git diff --cached检查暂存结果,确认阶段 1/2/3 已被 stage 0 替换。- 结果正确就
git merge --continue;若要在本课干净起始状态下退回合并前,则git merge --abort。
边界情况
- 两侧改不同行,例如 main 改第一行、feature 改第三行,未改动的第二行把两处修改隔开,可以同时接受,无阶段 1/2/3,也无标记块。改的是相邻行时,两处修改相接,构成一个冲突块。
- 编辑后未 add 就 continue,索引仍未合并,不创建提交也不移动引用。
- 标记还在就 add,仍可得到有效 stage 0,continue 可能把标记提交进去。
- abort 在本课合并前工作区干净的条件下,从合并起点 HEAD 恢复索引和工作文件,清除临时状态,两分支不动。
- 没有
MERGE_HEAD时,continue 与 abort 失败。本课编辑和 add 控件只在暂停的合并中可用。 - 八提交、三分支、二十四命令是布局限制,非 Git 限制。
常见错误
- 把标记当作冲突本身。权威状态是索引阶段 1/2/3,标记只是工作文本。
- 以为编辑器保存就解决了 Git 状态。保存只改工作区,add 才替换未合并项。
- 以为 add 会验证结果。残留标记、调试输出、错误拼接都会按原字节暂存。
- 期待立即出现冲突提交。结果树、合并提交、引用写入都在解决之后。
- 认为合并移动两分支。只推进当前分支,源末端不动。
- 忘记父顺序。第一父是旧当前末端,第二父为 MERGE_HEAD,顺序参与对象哈希。
模型范围
模型从干净本地仓库、附着 HEAD 开始,只有 app.txt、三条初始逻辑行、原位行修改、唯一基点和最多一个冲突块。省略新增、删除、重命名、目录与文件冲突、二进制、多基点、自定义驱动、属性、rerere、钩子、签名、子模块、稀疏或浅历史、原有工作区修改及远程。真实 ort 还会在冲突合并中写 AUTO_MERGE 树,本课未模拟。
实际行为见 git merge、git ls-files、Git Book 冲突处理示例。