解决冲突
冲突意味着 Git 发现同一个地方有两个更改,并且拒绝猜测。解决冲突是一项关于意图的决定,安全的默认做法是保留双方的意图。通过删除一方来解决冲突只会让标记消失,但问题变得更糟:构建仍然通过,而同事的更改却消失了。
1. 了解你处于哪个操作中 - ours 和 theirs 取决于它
git status # "rebase in progress" / "You have unmerged paths" / "cherry-pick in progress"
| 操作 | --ours / HEAD 方 | --theirs 方 |
|---|---|---|
merge | 你当前所在的分支 | 被合并进来的分支 |
rebase | 你变基到的分支(上游) | 你正在被重放的提交 |
cherry-pick,revert,stash pop | 你当前所在的分支 | 正在被应用的提交 |
变基行是一个陷阱。在执行 git rebase main 时,“ours”是 main,而“theirs”是用户自己的工作,这与合并相反。在变基中运行 git checkout --theirs file 会保留你的更改并丢弃上游的更改。在使用这两个标志之前,请重新阅读此表。
2. 查看整个冲突,而不仅仅是标记
git diff --name-only --diff-filter=U # 未合并的文件
git status # 类型:both modified / deleted by us / both added
git config merge.conflictStyle zdiff3 # 也显示共同祖先(Git 2.35+;旧版本使用 diff3)
git checkout --conflict=zdiff3 -- <file> # 重新显示一个文件的冲突,带基版本(会丢弃已对该文件所做的编辑)
当基版本可见时,标记有三个部分:你的方、原始版本、他们的方。当你能看到每一方从什么更改而来时,大多数表面上的冲突就会变得显而易见。
要了解每一方更改的原因:
git log --merge -p -- <file> # 来自双方修改该文件的提交(合并或变基进行中)
git show REBASE_HEAD # 在变基期间:正在被重放的提交
git show :1:<file> :2:<file> :3:<file> # 基版本、"ours"、"theirs" 作为完整文件
阅读提交信息。一个你无法用一句话解释每一方意图的解决方案就是猜测。
3. 通过合并来解决
- 双方因不同原因编辑了相同的行 - 编写同时执行两者的版本。例如,一方面添加了
tax_rate参数,另一方面添加了discount参数:已解决的函数同时接受两者并应用两者。 - 一方移动或重命名了,另一方编辑了 - 在当前位置应用编辑。不要同时保留旧文件和新文件。
- 一方删除了,另一方修改了 - 这是一个真正的决定,而不是机械的决定。找出被删除的原因;如果修改仍然重要,必须保留,否则删除占上风。如果不清楚,询问用户。
- 生成文件和锁文件(
package-lock.json、poetry.lock、go.sum、快照、编译输出)- 不要手动合并。选择一方,然后用工具重新生成(npm install、poetry lock、go mod tidy、快照命令)。 - 二进制文件 - 无法合并;有目的地选择一方(注意上表)并说明。
文本可以干净地合并但仍然错误:如果一方重命名了一个函数,而另一方添加了对旧名称的调用,Git 不会检测到冲突。这就是为什么第 4 步包含构建。
全局标志隐藏决定。 git checkout --ours .、-X ours / -X theirs(将每个冲突区块朝一方解决)和 -s ours(完全丢弃另一个分支)都会在没有检查的情况下丢弃工作。仅当用户明确要求这些操作并知道正在丢弃什么时才使用它们。
4. 验证,然后继续
git diff --check # 检查残留冲突标记和空白错误
grep -rnE '^(<<<<<<<|=======|>>>>>>>)' . # 双重检查,在 git diff-check 不检查的文件中
# 构建并运行最快的相关测试
git add <已解决的文件>
使用匹配的命令继续。添加 GIT_EDITOR=true 以便在编辑器会打开的地方使用,因为编辑器会在非交互式 shell 中挂起:
git merge --continue # 或者:git commit --no-edit
GIT_EDITOR=true git rebase --continue
GIT_EDITOR=true git cherry-pick --continue
变基会逐个重放提交,因此它可能会在下一个提交处再次停止。重复此循环。在最早出现冲突的提交处解决;后面的提交可能是同一个冲突的重放。
如果在解决后测试失败,失败信息是关于解决结果的信息。不要越过它继续,也不要在知道测试保护的是哪一方的行为之前通过编辑测试来“修复”它。
5. 何时停止并询问
在以下情况下停止而非猜测:
- 双方以不兼容的方式更改了业务逻辑(两种不同的税收规则,两种不同的重试策略);
- 你无法用一句话说出每一方做出更改的原因;
- 解决方案导致测试失败且原因不明确。
说明双方的意图,说出你会选择什么以及原因,然后让用户决定。在 --continue 之前的所有操作都是可逆的:git merge --abort、git rebase --abort、git cherry-pick --abort 会将树恢复到起始状态。
重复冲突
对于同一冲突反复出现的长时间变基,或反复合并和放弃的分支:
git config rerere.enabled true # 记录每个解决方案,下次重放
git rerere diff # 它记录了什么
git rerere forget-path <file> # 丢弃一个错误的已记录解决方案
rerere 会不询问地重新应用解决方案,因此错误的解决方案会传播。在 rerere 解决的冲突之后,仍然要检查结果。
当分支寿命很长且远远落后时,一次性合并主干通常意味着一次冲突回合;变基会按提交重放冲突。这个选择由用户决定。
安全护栏
- 永远不要通过单方面选择一方来让冲突消失。 另一方是某人的工作。
- 不要仅仅因为无聊就中止用户要求你完成的变基。 只有当出现错误时才中止,并说明原因。
- 除非被要求,否则不要在解决后推送。 变基需要强制推送;请遵循
git-safe-history-rewrite。 - 报告每个冲突文件:双方做了什么,你保留了什么,以及任何你无法验证的内容。