为什么 feature-a 合入 master 无冲突,feature-b 同步 master 却发生冲突
这两次 merge 比较的不是同一组提交。feature-a 合入 master 时,Git 比较 A 与当时 master 的修改;feature-b 随后同步 master 时,Git 比较 B 与 master 在另一个共同祖先之后的修改。因此,第一次能够自动合并,不能证明第二次也不会冲突。
本文假设 feature-a 通过普通 merge 进入 master,从而保留 A 的祖先关系。若仓库使用 squash merge、rebase merge 或 cherry-pick,提交关系会不同,文末会单独说明。
一、先建立完整的提交关系
假设 feature-a 和 master 从初始提交 O 分开开发,feature-b 又从 A 的最新提交 A2 创建:
B1---B2 feature-b
/
A1---A2 feature-a
/
O----+
\
M1---M2 master各提交代表的修改如下:
| 提交 | 所属分支 | 含义 |
|---|---|---|
A1、A2 | feature-a | 第一阶段功能修改 |
B1、B2 | feature-b | 基于 A2 继续开发的第二阶段修改 |
M1、M2 | master | master 上并行进入的其他修改 |
在 master 上合并 feature-a:
git switch master
git merge feature-a合并完成后产生 merge commit MA:
B1---B2 feature-b
/
A1---A2 feature-a
/ \
O----+ MA master
\ /
M1---M2MA 的两个父提交是 A2 和 M2。因此,A2 此时既是 feature-b 的祖先,也是 master 的祖先。
二、Git 在 merge 时比较什么
典型的双分支 merge 使用三个输入:
| 输入 | 含义 |
|---|---|
| merge base | 两个分支 tip 的共同祖先 |
| ours | 当前分支的 tip,即 HEAD |
| theirs | 待合入分支的 tip |
Git 分别计算以下两组修改:
merge base -> ours
merge base -> theirs当两组修改能够组合时,Git 自动生成合并结果;当修改涉及相同内容且无法自动协调时,Git 将相关路径标记为冲突。
“同一行被修改成不同内容”是最常见的文本冲突,但不是唯一情况。Git 还可能报告 rename/delete、add/add、文件与目录冲突等。另一方面,即使 Git 没有报告文本冲突,合并结果仍可能存在业务语义不兼容。
三、第一次合并为什么没有冲突
第一次操作是在 master 上执行:
git merge feature-a这次三方合并的输入是:
| 项目 | 对应提交或修改 |
|---|---|
| merge base | O |
| ours | master 的 M1、M2 |
| theirs | feature-a 的 A1、A2 |
Git 实际比较:
O -> M2
O -> A2如果 M1、M2 与 A1、A2 修改了不同文件或可自动组合的代码区域,这次 merge 就不会产生冲突。
该结果只说明 A 与当时 master 的修改兼容,未涉及 B1、B2。
四、第二次合并为什么可能冲突
feature-b 完成开发后,同步最新 master:
git switch feature-b
git merge master由于 A2 同时是 feature-b 和 master 的祖先,这次 merge 的共同祖先通常是 A2:
| 项目 | 对应提交或修改 |
|---|---|
| merge base | A2 |
| ours | feature-b 的 B1、B2 |
| theirs | master 相对 A2 引入的修改 |
Git 这次比较:
A2 -> B2
A2 -> MA第一次 merge 比较 A 与 master,第二次 merge 比较 B 与 master。只要 B1、B2 修改了 master 相对 A2 也修改过的内容,第二次 merge 就可能冲突。
五、用一个文件修改说明差异
初始提交 O 中的配置为:
gateway=legacy
timeout=30feature-a 修改支付网关:
gateway=payment-v2
timeout=30master 上的并行提交修改超时时间:
gateway=legacy
timeout=60两边修改的是不同配置项,因此 feature-a 合入 master 后可以得到:
gateway=payment-v2
timeout=60但 feature-b 是从尚未包含 master 修改的 A2 创建的。它看到的 timeout 仍是 30,随后将其修改为 45:
gateway=payment-v2
timeout=45feature-b 同步 master 时,三方比较为:
| 版本 | timeout |
|---|---|
merge base A2 | 30 |
| ours:feature-b | 45 |
| theirs:master | 60 |
Git 无法判断最终应该使用 45 还是 60,因此需要人工解决冲突。
这个例子同时说明:第一次 merge 没有冲突,是因为 A 没有修改 timeout;第二次发生冲突,是因为 B 和 master 都从 timeout=30 出发做了不同修改。
六、合并方向本身不是根因
如果参与合并的是同一对分支 tip,仅交换 ours 和 theirs,merge base 不会因此改变。冲突标记中的 ours/theirs 会对调,但不能仅凭“合并方向相反”解释冲突数量发生变化。
本例两次操作结果不同,是因为参与比较的提交已经不同:
第一次:master 与 feature-a
第二次:feature-b 与包含 A 的 master分析类似问题时,应先确认两次操作的分支 tip、祖先关系和合并策略,而不是只比较命令方向。
七、如何检查实际 merge base 和修改范围
查看提交图:
git log --graph --oneline --decorate --all计算 feature-b 与 master 的共同祖先:
git merge-base feature-b master假设返回值为 <base>,分别检查两侧修改:
git diff <base>..feature-b
git diff <base>..master只查看文件范围:
git diff --name-status <base>..feature-b
git diff --name-status <base>..master检查提交的父节点:
git show -s --format=%P <commit>普通提交通常有一个父提交,merge commit 通常有两个或更多父提交。这些命令可以确认文章中的提交图是否与仓库实际历史一致。
八、使用 merge 同步 master
对于已经推送或由多人维护的 feature-b,merge 通常更稳妥:
git switch feature-b
git status
git fetch origin
git merge origin/master发生冲突后,先检查冲突路径:
git status修改文件并标记为已解决:
git add <resolved-files>
git commit随后运行项目测试和构建,再推送分支:
git push origin feature-bmerge 会保留 feature-b 原有提交,并新增 merge commit。若 master 在此后没有新增提交,后续将 feature-b 合回 master 时可能可以快进;实际行为还取决于仓库保护规则和平台合并策略。
九、使用 rebase 同步 master
如果 feature-b 尚未共享,或者确认只有当前开发者使用,可以将其提交重新应用到最新 master:
git switch feature-b
git status
git fetch origin
git branch backup/feature-b-before-rebase
git rebase origin/masterrebase 会以最新 master 为新基点,依次重新创建 B1、B2。新提交的父节点发生变化,因此提交哈希也会变化。
遇到冲突时:
git add <resolved-files>
git rebase --continue取消整个 rebase:
git rebase --abort跳过当前提交:
git rebase --skip--skip 会丢弃当前正在重放的提交。只有确认该修改已经存在或确实不再需要时才能使用。
如果 feature-b 从未推送,可以正常首次推送:
git push -u origin feature-b如果远程已经保存了 rebase 前的旧历史,更新远程通常需要:
git push --force-with-lease origin feature-b--force-with-lease 会检查远程分支是否仍指向本地预期的提交。它比 --force 多一层并发保护,但仍然会改写远程历史。
十、merge 与 rebase 的工程取舍
| 对比项 | git merge origin/master | git rebase origin/master |
|---|---|---|
| 原提交哈希 | 保留 | 重新生成 |
| 历史结构 | 保留分叉并新增 merge commit | 形成线性历史 |
| 冲突处理 | 集中在一次 merge 中 | 按提交逐个处理 |
| 共享分支 | 适合 | 通常不适合 |
| 是否需要强制推送 | 不需要 | 远程存在旧历史时通常需要 |
| 并行开发过程 | 保留 | 被重新组织 |
可以按分支共享状态判断:
本地且未共享的功能分支:可以 rebase
已经推送但确认只有自己使用:协调后可以 rebase
多人共同维护的功能分支:优先 merge
master、release 等共享分支:不要随意 rebase十一、需要单独处理的边界情况
Squash merge
如果 feature-a 通过 squash merge 进入 master,master 会得到一个新的压缩提交,而不是保留 A1、A2 的祖先关系。此时 A2 通常不是 master 的祖先,feature-b 与 master 的 merge base 可能仍是更早的提交。
因此,普通 merge 场景中的 merge-base 推导不能直接用于 squash merge。
Rebase merge 和 cherry-pick
rebase merge 和 cherry-pick 都会重新创建提交。即使提交标题和代码修改相似,新的提交哈希和父节点也可能不同。分析历史时应查看祖先关系,而不是只看提交说明。
多个 merge base
复杂的 criss-cross 历史可能存在多个最佳共同祖先。Git 的合并策略可能先将这些祖先合成为虚拟 merge base。本文的提交图属于单一 merge base 的常见场景。
语义不兼容
Git 自动合并成功只说明文本或树结构可以组合。例如,两边分别修改接口参数和调用方式时,最终代码仍可能无法编译。合并后必须运行测试、构建和必要的业务验证。
十二、降低长期分支的冲突成本
缩短同步间隔
定期同步 master 可以更早暴露冲突,并缩小单次需要处理的上下文范围:
git fetch origin
git merge origin/master个人未共享分支也可以根据团队约定使用 rebase。频繁同步不能保证消除冲突,但可以避免在功能结束时集中处理大量历史差异。
启用 rerere
Git 可以记录已解决冲突的处理结果:
git config --global rerere.enabled true查看 rerere 状态和记录差异:
git rerere status
git rerere diff自动复用的结果仍需通过测试确认。
明确团队策略
建议在 CONTRIBUTING.md 中约定:
哪些分支允许 rebase
哪些分支禁止 force push
功能分支如何同步主干
PR 使用 merge、squash merge 还是 rebase merge
冲突由谁解决并验证总结
feature-a 合入 master 没有冲突,只能说明 A 与当时 master 的修改能够自动组合。feature-b 随后同步 master 时,参与合并的分支 tip 和 merge base 已经不同,Git 比较的是 B 与 master 在 A2 之后的修改。
判断此类问题时,应依次确认:
参与合并的是哪两个分支 tip
它们的 merge base 是哪个提交
两侧分别修改了 merge base 之后的哪些内容
feature-a 通过哪种策略进入 master
feature-b 是否已经共享在此基础上,再根据分支共享状态选择 merge 或 rebase。共享分支优先保留历史,本地未共享分支可以通过 rebase 整理提交;无论采用哪种方式,冲突解决后都需要运行测试和构建。
