在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

合并冲突

编辑器里看着已解决,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
  1. base 保存 title=VizLab 和 accent=blue,main、feature 最初都指向它。
  2. main-theme 把标题改为 VizLearn,第二行改为 accent=navy;feature-theme 做相同标题修改,但选择 accent=teal。两个末端共享基点,互不包含。
  3. 回到 main 合并,固定 ours=main-theme、theirs=feature-theme,唯一基点为 base。MERGE_HEAD 指向 feature,HEAD 不动。
  4. 第一行两侧同为 VizLearn,接受;第三行都未改;第二行 base=blue、ours=navy、theirs=teal,三值不同,产生一个冲突块。
  5. app.txt 索引变成阶段 1/2/3,工作文件展开为七行标记形式。新增提交零个,当前引用写入零次。
  6. 编辑器用 accent=indigo 替换标记块,只改工作区。索引仍有阶段 1/2/3,路径仍未合并。
  7. git add app.txt 将三行解决结果保存为 stage 0 blob,三个未合并阶段消失。MERGE_HEAD 仍在,分支不动。
  8. continue 确认没有未合并路径,从 stage 0 写树,创建合并提交:第一父为合并前 main,第二父为固定的 feature 末端。
  9. 最后 main 前进,feature 不动,清除 MERGE_HEAD。索引和工作区保留 accent=indigo。

两张下方卡片之间的箭头显示数据方向:冲突呈现从三阶段索引写到工作区;add 则把编辑结果从工作区写回 stage 0。

索引为何需要三个阶段

阶段号属于索引键。普通 app.txt 只有 stage 0,内容冲突时同一路径需要同时保存三个 blob 身份:

阶段角色本次合并含义
1base共同祖先中的文件
2ours合并前 HEAD 中的文件
3theirsMERGE_HEAD 中的文件
0resolvedadd 选定的单一版本

三个 blob 保留合并工具所需证据,比一个红色冲突标志包含更多信息。工作文件只是可编辑提案;stage 0 替换未合并项后,Git 知道下一棵树的精确字节,但不保证其逻辑正确。

本引擎收集每个末端的祖先为可达提交图上的 O(V + E);再从共同祖先中挑最佳者,每对共同祖先各遍历一次图,最坏 O(V²·(V + E)),八提交上限让它很小。逐行处理为 O(L + B),L 为逻辑行数,B 为字节数。结果哈希、树和提交序列化与相应内容大小线性相关。这些是当前引擎的界限,不是生产 ort 的性能声明。

处理冲突的步骤

  1. 用 git status 检查 unmerged paths,确认 Git 认为哪些路径未解决。
  2. 用 git ls-files -u 查看索引三份输入:1 为 base,2 为 ours,3 为 theirs。
  3. 编辑文件,决定程序应保留的行为,运行相关测试。此时只是候选解决结果。
  4. git add app.txt,再用 git diff --cached 检查暂存结果,确认阶段 1/2/3 已被 stage 0 替换。
  5. 结果正确就 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 冲突处理示例。