
merge 为什么有时只是移动指针有时会多一个 commit前面讲checkout和reset时我们一直围绕几样东西看HEAD当前站在哪里。分支引用refs/heads/master这种名字指向哪个 commit。index下一次提交准备包含什么。worktree磁盘上的文件内容。理解merge也一样不要先背命令结果而是先问当前分支和要合并进来的分支历史有没有真正分叉如果没有分叉merge 可能只是 fast-forward。如果已经分叉merge 就要做三方合并并产生一个新的 merge commit。这两个结果看起来都叫merge但本质很不一样。第一种情况fast-forward先看最简单的历史A — B master\C — D feature如果当前在master执行git merge feature这时master没有自己的新提交。它只是落后于feature。所以 Git 不需要创建新 commit只要把master从B移到DA — B — C — D master, feature这就是 fast-forward。它的重点是当前分支可以直接向前走到目标分支没有必要额外制造一个合并节点。从状态变化看fast-forward 做的是master: B - DHEAD: 仍然指向 masterindex: 恢复成 D 的 treeworktree: 恢复成 D 的 tree也就是说它最核心的动作是“移动当前分支指针”。但为了让你磁盘上的文件跟上这个新提交index和worktree也要更新到D的快照。所以 fast-forward 不只是“改一个分支文件”这么简单。用户能看到文件变化是因为工作区也被恢复到了目标提交。第二种情况真正分叉再看另一种历史C — D feature/A — B — E master当前在master要合并feature。这时master已经有自己的提交Efeature也有自己的提交C、D。master不能直接移动到D否则E就会从当前分支历史里消失。所以 Git 需要创建一个新的 commitC — D/ \A — B — E — M master这个M就是 merge commit。它和普通 commit 最大的区别是它有两个 parent。parent 1 - Eparent 2 - Dtree - 合并后的目录快照这就是为什么普通提交一般只有一条父边而 merge commit 会把两条历史接在一起。merge-base 是什么真正分叉时Git 不能只看master和feature两个版本。它还必须知道双方是从哪里分开的。也就是共同祖先base - Bours - E 当前分支 mastertheirs - D 要合并进来的 feature这个B就是 merge-base。有了baseGit 才能判断base - ours 当前分支改了什么base - theirs 对方分支改了什么如果只看ours和theirs你只能知道两个结果不同却不知道是谁改了、怎么改的。这也是三方合并的核心。为什么叫三方合并所谓三方就是base共同祖先。ours当前分支。theirs要合并进来的分支。比如同一个文件base:helloours:hello mastertheirs:hello feature两边都改了同一行Git 很可能无法自动判断应该保留哪一个于是产生冲突。如果双方改的是不同文件或者同一个文件里的不同位置Git 就有机会自动合并。所以 merge 不是简单把两个文件拼起来。它是在问相比共同祖先双方分别改了什么这些修改能不能同时保留冲突时为什么会有 MERGE_HEAD如果自动合并失败Git 不能直接创建 merge commit。因为合并结果还没有确定。这时它会进入一个“合并未完成”的状态。其中一个关键标记就是.git/MERGE_HEAD它记录的是“对方那个提交是谁”。为什么要记录因为你解决冲突后再执行提交时Git 需要知道这个提交不是普通 commit而是一个 merge commit。也就是说新 commit 需要两个 parentparent 1 - 当前 HEADparent 2 - MERGE_HEAD 里记录的提交如果没有MERGE_HEAD冲突解决后的提交就会丢掉“我是在合并谁”这个信息。mini-git 里怎么落地mini-git 里 merge 的入口主要在src/commands/cmd_merge.csrc/core/graph.csrc/core/linemerge.csrc/core/worktree.c大致流程可以拆成几步。第一步解析当前提交和目标提交ours 当前 HEAD 指向的 committheirs 要 merge 进来的 commit第二步先判断能不能 fast-forward。如果当前提交是目标提交的祖先就不需要创建 merge commit只要移动当前分支到目标提交再恢复index/worktree。第三步如果不能 fast-forward就找 merge-base。base ours 和 theirs 的共同祖先第四步读出三棵 treebase treeours treetheirs tree第五步做三方合并。如果能自动合并就把合并结果写成新的 tree再创建一个有两个 parent 的 merge commit。如果有冲突就把冲突内容写进工作区同时写入MERGE_HEAD等用户解决冲突后再提交。一个容易误解的点很多人会把 merge 理解成把 feature 的代码复制到 master。这个说法太粗了。更准确的理解是把当前分支和目标分支从共同祖先之后的变化合到一起。如果当前分支没有自己的变化那就是 fast-forward。如果双方都有变化就需要三方合并。如果三方合并能自动完成就生成 merge commit。如果不能自动完成就进入冲突状态等你人工决定最终内容。面试里怎么说可以这样回答merge 先判断当前分支能否 fast-forward 到目标分支。如果可以就只移动当前分支引用并同步 index 和 worktree 到目标提交。如果不能 fast-forward说明两边历史已经分叉需要找到 merge-base然后基于 base、ours、theirs 做三方合并。自动合并成功后会创建一个有两个 parent 的 merge commit如果发生冲突会写入冲突文件和 MERGE_HEAD等待用户解决后再提交。如果对方继续问 merge commit 为什么有两个 parent可以补一句因为 merge commit 同时接住了当前分支和被合并分支两条历史。第一个 parent 通常是当前分支原来的 HEAD第二个 parent 是被合并进来的提交。总结理解 merge抓住这几句话就够了没有分叉时merge 可以 fast-forward。fast-forward 本质是移动当前分支指针同时同步index/worktree。分叉后merge 要找 merge-base。真正的合并是三方合并base、ours、theirs。自动合并成功会生成一个双 parent 的 merge commit。冲突时会留下MERGE_HEAD表示这次合并还没完成。所以 merge 不是“复制代码”而是在维护两条历史之间的关系。