处理变更

解决冲突

当一次合并、变基、拣选(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 提议一样,它只落在输出面板里,不会暂存任何东西。你在保存前先读一遍。

Xcode 项目文件上,冲突面板上方的结构合并横条

它拒绝做的事

它绝不猜测你们都动过的设置。 如果你把 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 并不理解每一种冲突。 添加/添加与删除/修改这两类冲突拿不到预映像,所以它们每次都会原样回来。设置里的计数是它实际记住的条数,而全部忘掉会清空它。

另请参阅: 冲突雷达 · 合并与变基

在 GitHub 上改进这份翻译