git merge有时直接移动指针、有时创建新提交,取决于分支拓扑关系:若当前分支是目标分支的直接祖先,则执行快进合并,仅移动指针;否则触发三方合并,生成含两个父提交的merge commit。

Git merge 为什么有时直接移动指针,有时却要创建新提交?
取决于两个分支的拓扑关系。Git 先判断能否快进(fast-forward):如果当前分支是目标分支的直接祖先,就只移动当前分支指针,不产生新提交;否则触发三方合并,生成一个带两个父提交的 merge commit。
常见误判点:以为「没冲突」就一定是快进。实际上,只要两个分支都从共同祖先出发各自有新提交,哪怕内容完全不重叠,也会走三方合并流程 —— 因为历史已分叉。
- 快进条件:
git merge-base HEAD target-branch返回的是target-branch的最新提交 - 非快进条件:
git merge-base HEAD target-branch返回一个更早的提交(即共同祖先) - 强制生成 merge commit:
git merge --no-ff,即使满足快进条件也创建新节点
三方合并(Three-way merge)到底比对哪三份内容?
不是「当前文件 vs 目标文件」的简单对比,而是基于三个确定状态做差异计算:
-
Base:两个分支的最近共同祖先提交(由get_merge_base()算出) -
HEAD:你当前所在分支的最新树(即本地修改结果) -
MERGE_HEAD:被合并分支的最新树(即远端修改结果)
Git 分别计算 Base → HEAD 和 Base → MERGE_HEAD 的 diff,再尝试把这两组变更叠加。如果同一行在两边都被修改,或一边修改另一边删除,就标记为冲突 —— 这不是 Git “不会合并”,而是它明确拒绝做无依据的自动决策。
注意:diff 比对的是树对象(tree object),不是工作区文件;所以 .gitattributes 中的 merge=ours 或 merge=theirs 配置,只影响特定文件类型的合并策略,不改变三方比对逻辑本身。
为什么 merge 后某些文件“凭空消失”?
这不是 bug,而是三方合并严格遵循变更溯源的结果。典型场景:A 分支删了 http.js,B 分支在该文件基础上新增了 main.js 并 import 它;当 B 合并入 A 时,Git 发现 Base → A 包含删除操作,Base → B 包含新增和引用,但没有“恢复 http.js”的动作,于是最终状态里 http.js 仍被删除,导致 main.js 报错。
这类问题无法靠“解决冲突”规避,因为根本没触发文本级冲突 —— Git 认为删除和新增是互斥变更,直接采用删除方的结果。
- 检查是否真无冲突:
git status显示 clean ≠ 合并逻辑无风险 - 提前验证影响:
git merge --no-commit target-branch,然后手动检查关键文件是否存在 - 避免跨分支强依赖:不要让一个分支的新增文件,直接依赖另一个分支已删除的文件
merge-recursive.c 里真正决定冲突的关键函数是什么?
核心是 traverse_trees_recursive(),它逐文件遍历三棵树(ancestor / head / merge)。对每个路径,它调用 is_conflict() 判断是否需用户介入 —— 这个判断基于三元状态组合(例如:ancestor 有、head 删、merge 改 → 冲突)。
容易忽略的一点:Git 默认只做内容合并,不处理重命名。如果 Base 有 a.js,HEAD 把它改名为 b.js,MERGE_HEAD 修改了 a.js,Git 会把 a.js 和 b.js 当作两个独立文件处理,最终可能同时存在,而非智能重命名后合并内容。
所以真实项目中,git config merge.renormalize true 或使用 git merge -s recursive -X rename-threshold=50% 才能激活重命名检测,但这需要双方变更足够相似(默认阈值 50%),且不能保证 100% 正确。











