14. 合并冲突不是噩梦:Git 合并策略与冲突解决

「Git」系列已发布: 00. 改崩代码不敢回退?Git 版本控制 3 分钟救你狗命 | 01. Git 装完不会配?Windows/macOS/Linux 三端配置一次到位 | 02. Git 配置总踩坑?全局/仓库/SSH 密钥一次搞懂 | 03. add/commit/push 别再死记:Git 工作流核心概念实操 | 04. Git 分支管理:5 步彻底搞懂 branch 与 merge | 05. clone/fetch/pull/push 分不清?Git 远程协作实战 | 06. 给版本打上永久印记:Git 标签管理与发布实战 | 07. 切分支怕丢改动?Git stash 暂存恢复保护现场 | 08. rebase 不是 merge 的替代品:Git 故事线重构进阶 | 09. Git 日志与调试:log/blame/diff 让历史无所遁形 | 10. 提交代码自动检查:Git Hooks 实战,质量不再靠人工 | 11. 多仓库依赖怎么管?Git 子模块统一版本一键更新 | 12. GitFlow vs GitHub Flow vs 主干:团队工作流 3 选 1 | 13. 仓库越来越大?Git LFS 让图片视频不再撑爆仓库 | 14. 合并冲突不是噩梦:Git 合并策略与冲突解决(本文) | 15. 常用命令记不住?Git 别名让操作变成快捷键

merge、rebase、squash 该怎么选?冲突来了怎么解?本文拆解合并策略与冲突解决套路,让多人协作如丝般顺滑。

分支管理(第 04 篇)和变基操作(第 08 篇)要很熟。建议拿个测试仓库刻意制造冲突来练手——冲突这东西,多碰几次就不怕了。

一、合并策略

1.1 快进合并(Fast-Forward)

目标分支没新提交时,Git 直接移指针,不生成合并提交:

快进前,main 落后 feature:

gitGraph commit id: "A" commit id: "B" branch feature commit id: "C" commit id: "D"

git merge feature 后 main 直接快进到 D,形成线性历史(无合并提交):

gitGraph commit id: "A" commit id: "B" commit id: "C" commit id: "D"
git checkout main
git merge feature

禁用快进
——强制生成合并提交:

git merge --no-ff feature

强制生成合并提交 M,保留 feature 分支的完整历史:

gitGraph commit id: "A" commit id: "B" branch feature commit id: "C" commit id: "D" checkout main commit id: "M" merge feature

1.1.1 三方合并

两个分支都有新提交时,Git 用三方合并算法:

三方合并涉及三个点:共同祖先(base)、当前分支(ours)、合入分支(theirs),Git 对两边差异做合并:

gitGraph commit id: "A" commit id: "B" commit id: "E" commit id: "F" branch feature commit id: "C" commit id: "D" checkout main merge feature
git checkout main
git merge feature

Git 做三件事:① 找共同祖先 B;② 算 B→F 的差异(ours);③ 算 B→D 的差异(theirs);④ 合并差异,生成合并提交。

1.1.2 压缩合并(Squash)

把源分支所有提交压成一个,应用到目标分支:

git merge --squash feature
git commit -m "feat: 实现用户认证模块"

feature 分支的历史不会出现在 main 上,但 feature 分支本身还能继续开发。

1.2 冲突怎么来的

Git 自动合并失败就产生冲突。最常见几种情况:

  1. 两个分支改了同一个文件的同一行。
  2. 一个分支删了文件,另一个分支改了同一个文件。
  3. 两个分支都新增了同名文件但内容不同。
# 遇到冲突

git merge feature

# Auto-merging src/config.py

# CONFLICT (content): Merge conflict in src/config.py

# Automatic merge failed; fix conflicts and then commit the result.

# 看哪些文件冲突

git status

# Unmerged paths:

# both modified:   src/config.py

1.2.1 冲突标记

Git 在冲突文件里插标记:

<<<<<<< HEAD (ours / 当前分支)
db_host = "localhost"
db_port = 3306
=======
db_host = os.getenv("DB_HOST", "prod-db.example.com")
db_port = int(os.getenv("DB_PORT", "5432")
>>>>>>> feature (theirs / 合进来的分支)

<<<<<<<到=======是你当前的。=======到>>>>>>>是要合进来的。

1.3 怎么解决

1.3.1 手动解决

# 1. 编辑冲突文件,删标记,留正确代码

vim src/config.py

# 最终效果:

db_host = os.getenv("DB_HOST", "localhost")
db_port = int(os.getenv("DB_PORT", "5432")

# 2. 标记已解决

git add src/config.py

# 3. 完成合并

git commit

# 编辑器弹出,合并提交信息已自动填好

1.3.2 选一方

# 全要当前分支的

git checkout --ours src/config.py
git add src/config.py

# 全要合进来的

git checkout --theirs src/config.py
git add src/config.py

# 注意:rebase 冲突时反过来!

# rebase: ours = 目标分支, theirs = 当前分支

1.3.3 用合并工具

# VS Code

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

# 启动

git mergetool

# 其他

git config --global merge.tool meld
git config --global merge.tool kdiff3
git config --global merge.tool opendiff  # macOS

1.4 合并策略选项

1.4.1 ours vs theirs

# 忽略合进来的分支,完全以当前为准

git merge -s ours feature

# rebase 中用 theirs(冲突时自动选 feature 版本)

git rebase -Xtheirs main

1.4.2 合并算法

某些结构复杂的文件,默认算法可能产生糟糕的冲突标记:

# patience:对重复内容多的文件更好

git merge -Xpatience feature

# histogram:patience 的改进版,更快

git merge -Xhistogram feature

1.4.3 忽略空白

# 忽略所有空白差异

git merge -Xignore-all-space feature

# 只忽略行尾空白

git merge -Xignore-space-at-eol feature

1.5 高级冲突场景

1.5.1 重命名冲突

当前分支把file.py改名为module.py,合并分支修改了file.py。Git 通常能自动处理,检测不到的话:

# 降低重命名检测阈值——30% 相似就视为重命名

git merge -Xfind-renames=30 feature

如果还是出了两个文件:

git status

# added by us:     module.py

# added by them:   file.py

# 手动把合并分支的修改合到重命名后的文件

cat file.py >> module.py
git rm file.py
git add module.py

1.5.2 二进制文件

二进制没法像文本那样手动合并,通常只能选一方:

# 选当前分支的

git checkout --ours logo.png
git add logo.png

# 选合并分支的

git checkout --theirs logo.png
git add logo.png

# 建议:在 .gitattributes 里给二进制配合并驱动

# *.png merge=binary-ours

1.5.3 子模块冲突

# 看子模块差异

git diff main feature -- path/to/submodule

# 切到其中一方的版本

git checkout main -- path/to/submodule   # 或
git checkout feature -- path/to/submodule
git add path/to/submodule

1.6 冲突怎么防

1.6.1 频繁变基

# feature 分支上定期 rebase 到 main

git checkout feature
git rebase main

# 每次冲突都很少,总比攒到最后一次性搞强

1.6.2 小步提交、短命分支

大型 feature 分支开发几周后一次性合并——这是典型的反模式。每天把 main 合并进来,或者把分支拆小。

1.7 Code Review 预判

Merge Request 里如果和正在进行的其他 MR 动了同一文件,提前协调修改顺序。MR 描述里标一下"可能和 MR XX 冲突"。

1.8 解决后检查清单

[ ] 冲突标记(<<<<<<<, =======, >>>>>>>)全清干净了吗?
[ ] 代码编译能过吗?
[ ] 相关测试跑通过了吗?
[ ] 合并后逻辑完整吗(没漏掉两边的功能)?
[ ] 有没有重复的代码块?
[ ] 有没有语法错误(比如重复的闭合括号)?
[ ] .gitignore 有没有误伤需要的文件?

1.9 文章引用

📌 本文首发于微信公众号 【运维知识点滴】——专注运维实战干货,微信搜索“运维知识点滴”或扫下方二维码关注,更新不迷路。

微信公众号【运维知识点滴】 微信小程序【A5单据合并&大试卷拆分转A4打印】
运维实战干货 · 关注追更 Word 转 PDF · 试卷/发票打印神器 · 免费
运维实战干货 · 关注追更 Word 转 PDF · 试卷/发票打印神器 · 免费

💡 小程序【A5单据合并&大试卷拆分转A4打印】是干嘛的? 一个 Word 转 PDF 的打印神器,专治“文档版式和打印机对不上”:

  • A3 试卷打不出来? 学校发的电子版试卷多是 A3,家用打印机只吃 A4——上传 Word 一键转换:可缩到一页 A4(页数不变),也可对半拆成两张 A4(1:1 原大、字更清晰),两种模式自选;
  • 版式不乱:真实渲染引擎原样转换、绝不重排内容,转出的 PDF 和原 Word 排版一致,公式/表格/图片不跑版;
  • 两张 A5 单据拼一张 A4:出租车票、火车票等报销凭证拼版后,一次打印、整齐粘贴;
  • 隐私省心:无需注册,转换结果留存 5 天、最多下载 5 次即自动删除,不留任何历史记录;
  • 微信扫码即用,免费、免安装,支持 doc / docx(≤50MB)。

上一篇:13. 仓库越来越大?Git LFS 让图片视频不再撑爆仓库 | 下一篇:15. 常用命令记不住?Git 别名让操作变成快捷键