解决冲突
当一次合并、变基、拣选(cherry-pick)或还原停下来时,横幅会告诉你是什么停了下来,以及在什么和什么之间——写的是「正在把 feature/x 合并进 main」,而不是干巴巴一句「冲突」。

为什么这里会冲突
标题栏里的为什么这里会冲突,会分别按两边列出:自两个分支分道扬镳以来,动过这个文件的那些提交——也就是 git log --merge,git 早就自带、却没人找得到的东西。

冲突标记告诉你哪些内容打了架。这里告诉你是谁改的、为什么改,而后者通常才是真正决定该怎么解的东西。这里什么都没有,说明两边都没有对这个确切路径提交过改动——这场冲突来自一次重命名或移动。
三个栏
| 栏 | 是什么 |
|---|---|
| 左 | 我们的(Ours)——你原本所在的那一边,标注了它的提交 |
| 右 | 他们的(Theirs)——正在并进来的那一边,标注了它的提交 |
| 中 | 输出:可编辑,带行号,也是真正会被暂存的内容 |
三个栏都能调整宽度,输出栏的标题上还有两个视图开关:
| 开关 | 作用 |
|---|---|
| 换行 | 让 A、B 两栏里的长行折行,而不是横向滚动。输出栏保持一行占一行——旁边的标记依赖这一点——所以它始终可以滚动 |
| 联动 | 让 A、B 和输出一起滚动,纵向和横向都是。三者的行数不同,所以纵向位置按比例对齐 |
换行默认关闭,联动默认开启,两者都会记住状态。
在文件中移动
打开一个文件会落在它的第一个冲突上,而不是文件开头。输出栏标题上的 ⌃ / ⌄ 箭头——或 Alt+↑ / Alt+↓——会逐个跳到其余冲突,并把三个栏一起滚动到那里。
挑选
可以按行、按块,或者一次拿下整整一边——而且当答案是「两边都留」的时候,你可以把一个块的两边都收下。一个逐个冲突的导航器会带你走完还剩下的部分,所以你不可能不小心漏下一个标记。
AI 协助
启用 AI 后,用 AI 解决会往输出栏里提出一份合并方案。它绝不会自己应用任何东西:你来读、你来改、你来暂存。参见 AI 功能。
Xcode 项目文件
project.pbxproj 比 iOS 仓库里任何别的文件都更容易冲突,而且几乎从来不是因为
谁跟谁有分歧。它是一个扁平的对象字典,键是 24 位十六进制 id,所以添加一个文件
会写入四条记录:一个 PBXBuildFile、一个 PBXFileReference、所属分组
children 里的一行,以及目标构建阶段里的一行。两个人各加一个文件,就是落在同样
那几行上的八条记录。Git 看到的是碰撞;其实没有什么需要解决。
当冲突的文件是 project.pbxproj 时,冲突编辑器会把三个版本当作项目而不是文本
来读,并提出按结构合并:按 id 配对对象,取两边的所有新增,合并 children
和 files 数组,然后在真正分歧的地方停下。面板上方的横条会说明每一边加了什么,
以及还有什么——如果有的话——要你来定。
和 AI 提议一样,它只落在输出面板里,不会暂存任何东西。你在保存前先读一遍。

它拒绝做的事
它绝不猜测你们都动过的设置。 如果你把 MARKETING_VERSION 设成 1.1,
对方设成 2.0,那是一个决定,会以设置名、你的值、对方的值出现在横条里,而不是
在你背后被解决掉。它拿不准的对象会原样保留你的版本,好让半途而废的合并永远
不会落到磁盘上。
只要三个版本中有任何一个解析不了,它就整份拒绝。 一个 Xcode 打不开的
project.pbxproj 比手工合并代价更大,所以凡是它无法确凿读出的,都仍旧是普通的
文本冲突,并且会明说。
它不会发现同一个 id 被分给了两个不同的对象。 因为 Xcode 随机取 id,这很 罕见——但一旦发生,选哪一边都会悄悄丢掉某个人的文件,所以它选择报告而不是合并。
不要用 merge=union
针对这件事流传的偏方是在 .gitattributes 里写
*.pbxproj merge=union。别用。只要改动全是互不相干的新增,union 就能凑合;可一旦
两个人编辑同一个构建设置,它会把两行都写出来,产出一个 Xcode 拒绝打开的文件——
而那恰恰是你最不可能仔细读 diff 的时刻。按结构合并给你同样的便利,没有这个故障。
锁文件
Podfile.lock、Package.resolved、yarn.lock 和它们的同类,记录的是某个解析器
已经解好的依赖图。把一份解的一半缝到另一份解的一半上,得到的是一份没人解过的图:
它可能装不上;就算装得上,装的也是两个分支都没测过的东西。
所以当冲突的文件是锁文件时,横条会说明是哪个工具在管它,就地提供保留我们的和 保留他们的,并给出之后重新生成它的命令。在这里选一边不是折中——它就是全部方法, 而让它正确的是重新生成这一步。

三个面板依然在,因为偶尔你确实想读读改了什么:一个你认得的校验和、一个你预期中的 版本。这里想劝你别做的,是动手去编辑它们。
从一开始就避开它们
冲突雷达会在你合并任何分支之前,先告诉你哪些分支将会冲突。
让 git 帮你记住(rerere)
对一个长期存在的分支做变基,你每次都会撞上同一个冲突。rerere——reuse recorded resolution,复用已记录的解决方案——就是 git 的答案:它记住你当初是怎么了结某个冲突的,并在下次出现一模一样的冲突时把那个答案重放一遍。
设置 → 通用 → 记住冲突的解决方案。 它会把 rerere.enabled 写进你的全局 git 配置,所以命令行也会有同样的行为。
当 git 替你作答之后,解决器会明说这件事,而不是给你看一个空空如也的「没有冲突标记」界面,同时提供忘掉这个解决方案——它会丢掉那份记忆并且把冲突原样带回来,好让你换个方式了结它。
有两件事值得知道:
被重放的解决方案不会自动暂存,除非你打开自动暂存被重放的解决方案。让它保持关闭:这次停顿的意义就在于,一个记住的答案在这一次特定的合并里可能是错的,而不看一眼就暂存,正是它混进提交的途径。
这也是为什么被重放的文件仍然留在「有冲突的文件」里:git 写好了内容,但索引里它仍然处于未合并状态,只有暂存才能了结这一点。用解决器里的按现状暂存,或者列表里的全部标记为已解决,才能让它挪窝。
rerere 并不理解每一种冲突。 添加/添加与删除/修改这两类冲突拿不到预映像,所以它们每次都会原样回来。设置里的计数是它实际记住的条数,而全部忘掉会清空它。