news 2026/10/3 22:34:20

merge 为什么有时只是移动指针,有时会多一个 commit

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
merge 为什么有时只是移动指针,有时会多一个 commit

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移到D:

  • A — B — C — D master, feature

这就是 fast-forward。

它的重点是:

  • 当前分支可以直接向前走到目标分支,没有必要额外制造一个合并节点。

从状态变化看,fast-forward 做的是:

  • master: B -> D
  • HEAD: 仍然指向 master
  • index: 恢复成 D 的 tree
  • worktree: 恢复成 D 的 tree

也就是说,它最核心的动作是“移动当前分支指针”。

但为了让你磁盘上的文件跟上这个新提交,index和worktree也要更新到D的快照。

所以 fast-forward 不只是“改一个分支文件”这么简单。用户能看到文件变化,是因为工作区也被恢复到了目标提交。

第二种情况:真正分叉

再看另一种历史:

  • C — D feature
  • /
  • A — B — E master

当前在master,要合并feature。

这时master已经有自己的提交E,feature也有自己的提交C、D。

master不能直接移动到D,否则E就会从当前分支历史里消失。

所以 Git 需要创建一个新的 commit:

  • C — D
  • / \
  • A — B — E — M master

这个M就是 merge commit。

它和普通 commit 最大的区别是:它有两个 parent。

  • parent 1 -> E
  • parent 2 -> D
  • tree -> 合并后的目录快照

这就是为什么普通提交一般只有一条父边,而 merge commit 会把两条历史接在一起。

merge-base 是什么

真正分叉时,Git 不能只看master和feature两个版本。

它还必须知道双方是从哪里分开的。

也就是共同祖先:

  • base -> B
  • ours -> E 当前分支 master
  • theirs -> D 要合并进来的 feature

这个B就是 merge-base。

有了base,Git 才能判断:

  • base -> ours 当前分支改了什么
  • base -> theirs 对方分支改了什么

如果只看ours和theirs,你只能知道两个结果不同,却不知道是谁改了、怎么改的。

这也是三方合并的核心。

为什么叫三方合并

所谓三方,就是:

  • base:共同祖先。
  • ours:当前分支。
  • theirs:要合并进来的分支。

比如同一个文件:

  • base:

  • hello

  • ours:

  • hello master

  • theirs:

  • hello feature

两边都改了同一行,Git 很可能无法自动判断应该保留哪一个,于是产生冲突。

如果双方改的是不同文件,或者同一个文件里的不同位置,Git 就有机会自动合并。

所以 merge 不是简单把两个文件拼起来。

它是在问:

  • 相比共同祖先,双方分别改了什么?这些修改能不能同时保留?

冲突时为什么会有 MERGE_HEAD

如果自动合并失败,Git 不能直接创建 merge commit。

因为合并结果还没有确定。

这时它会进入一个“合并未完成”的状态。

其中一个关键标记就是:

  • .git/MERGE_HEAD

它记录的是“对方那个提交是谁”。

为什么要记录?

因为你解决冲突后再执行提交时,Git 需要知道这个提交不是普通 commit,而是一个 merge commit。

也就是说,新 commit 需要两个 parent:

  • parent 1 -> 当前 HEAD
  • parent 2 -> MERGE_HEAD 里记录的提交

如果没有MERGE_HEAD,冲突解决后的提交就会丢掉“我是在合并谁”这个信息。

mini-git 里怎么落地

mini-git 里 merge 的入口主要在:

  • src/commands/cmd_merge.c
  • src/core/graph.c
  • src/core/linemerge.c
  • src/core/worktree.c

大致流程可以拆成几步。

第一步,解析当前提交和目标提交:

  • ours = 当前 HEAD 指向的 commit
  • theirs = 要 merge 进来的 commit

第二步,先判断能不能 fast-forward。

如果当前提交是目标提交的祖先,就不需要创建 merge commit,只要移动当前分支到目标提交,再恢复index/worktree。

第三步,如果不能 fast-forward,就找 merge-base。

  • base = ours 和 theirs 的共同祖先

第四步,读出三棵 tree:

  • base tree
  • ours tree
  • theirs 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 不是“复制代码”,而是在维护两条历史之间的关系。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 22:23:02

企业AI Agent定制:把万元当元来比,错在哪里?

一家企业的经营分析岗让助手比较两个区域上半年的费用。助手很快给出结论,说A区比B区高出三成。同事看着数字觉得不对,回去翻原始报表才发现,A区的单位是万元,B区是元,两个数被直接放在一起比了。数字都是从系统里取出…

作者头像 李华
网站建设 2026/10/3 22:17:12

ai编程工具接入TaoToken:统一Key与API通道的配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 22:16:24

MetaERP 的 AP 模块在跨法人结算里,不是“A 公司付外部供应商”那种普通应付,而是把“集团内一个法人欠另一个法人”也当成应付对象来处理,并且和 AR、资金、合并报表打通。核心思路是:内部交

MetaERP 的 AP 模块在跨法人结算里,不是“A 公司付外部供应商”那种普通应付,而是把“集团内一个法人欠另一个法人”也当成应付对象来处理,并且和 AR、资金、合并报表打通。核心思路是:内部交易有唯一源头 → 双边自动派生应收/应…

作者头像 李华