git分支交叉合并排查与收尾

起因是仓库提交历史很乱:来回合并、分支互相吞并、主线缺内容。排查后发现乱象背后是一个清晰的因果链,而收尾方式又恰好印证了 Git 合并机制的两个核心概念。

一、现象

打开 git log --oneline --graph,看到的是这样的画面:

code
*   merge: 合并功能B到功能A|\| *   merge: 合并远程分支| |\| | * ...| * | code review 修复| |/| * ...* |   Merge branch '功能A' of origin into 功能A|\ \| | | * Merge branch '功能A'(master 上的合并)| | | |\| | |_|/| |/| || * | | ...

分叉、汇合、再分叉、再汇合,三个分支之间织了一张网。更麻烦的是:master 上缺了大量本该有的功能,而某个功能分支却"领先 master 三十多个提交"——它明明只是从 master 上分出去的小分支,为什么会领先这么多?

二、背景

  • 两个开发者(下文称 A 和 B)在同一个仓库并行开发。
  • 三条线:master(主线)、分支 X(A 主导)、分支 Y(B 主导,后来两人都在上面提交)。
  • 一天之内,三条线发生了四次合并,其中两次方向"不对"。

三、排查方法:把时间线"钉死"

面对一团乱麻,不能靠猜。Git 提供了足够的线索,按下面四步就能把历史完整还原:

1. 画全局图

sh
git log --oneline --graph --all -n 120

先看全貌,找出所有 merge 提交和分叉点。

2. 拆解每个 merge 提交的父母

merge 提交的价值在于它的 parents 列表——它精确记录了"谁在什么时候把谁并进了谁":

sh
git log --format="%h %ci %p %s" -1 <merge提交># 输出: e576be4 2026-08-12 11:02:17 +0800 e4dcc93 4939e77 Merge ...#       哈希    时间                       第一父    第二父  提交说明
  • 第一父:合并时当前分支的 HEAD(合并发生在谁身上);
  • 第二父:被合并进来的分支头(把谁并了进来)。

一个 %p 字段,直接把"谁合并了谁"钉死,比任何口头回忆都可靠。

3. 补上作者和时间

sh
git show -s --format="%h|%an|%ci|%s" <提交>

给每个关键提交补上作者与精确时间。两个开发者的提交在同一个分支上交叉出现、同一文件被两边平行修改——这些都会在时间线上现形。

4. 验证"包含关系"而不是凭感觉

判断"分支 A 的内容是否在 master 里",用:

sh
git merge-base --is-ancestor <分支A头> master && echo 已合入 || echo 未合入

判断"谁领先谁多少":

sh
git rev-list --count master..分支A   # 分支A 领先 master 多少git rev-list --count 分支A..master   # master 领先分支A 多少

这一步直接推翻了我们最初的记忆:“B 应该把分支 Y 合进 master 了吧?”——答案是没有

四、还原出的时间线

时间 动作 影响
上午 A 在分支 Y 上连续提交 8 个样式修复,推送远程 分支 Y 出现两条平行线
中午 B 自己本地的分支 Y(还停在旧节点)上提交 code review 修复 与 A 的提交分叉
中午 B pull 合并 → 分支 Y 内部完成一次交叉合并 交叉点 1:分支内部合并
傍晚 17:29 A 基于 master 分叉,提交功能提交 P,推送到分支 X
傍晚 17:30~17:47 B 在 master 上连续提交 3 个提交,与 P 平行改同一文件 撞车隐患
傍晚 17:53 B 在分支 X 上提交提交 Q(基于 P),随后 master 合并分支 X 交叉点 2:master 合入"空壳"
晚上 19:00~19:01 A 在分支 X 上提交核心功能(比 B 合并晚了 1 小时) 核心功能错过了 master 的合并
晚上 19:03 A pull,自动合并 Q 交叉点 3
晚上 19:20 A 把分支 Y 整体并入分支 X 交叉点 4:功能分支互合
次日 11:02 B 把 master 反向并入分支 X 局面突然变干净

五、三个关键发现

发现一:master 合并了一个"空壳"分支

master 的合并提交 parents 是 (master头, Q),Q 是分支 X 上仅有的两个提交之一。那一刻(17:53):

  • 核心功能提交还没产生(19:00 才有);
  • 分支 Y 的整套功能还没并进分支 X(19:20 才有)。

所以 master 合了个寂寞——合进来的是分支 X 的中间态,真正的功能全在合并之后才出现。这是"master 缺内容"的直接原因。

发现二:功能分支互相合并,而不是走 master

分支 Y 被直接并进分支 X(而不是各自并 master),导致:

  • master 对两套功能完全不知情;
  • 分支 X “领先 master 三十多个提交”,但其中自己开发的只有 3 个,其余全是吞进来的分支 Y 的内容和合并提交。

发现三:分支职责混在一起

分支 Y 里既有功能 A 的开发,也混着若干无关的优化提交;分支 X 里也混着别的修复。职责不清的分支让"这个提交属于哪个功能"无法回答。

六、原理:为什么"反向合并"反而让正向合并变干净

这是本次排查最有趣的部分。B 次日做了一次反向合并(把 master 并进分支 X),表面看是"又添乱",实际上把收尾变得零成本。原理如下:

1. 合并的本质

git merge B 做的事:

  1. 找两个分支的共同祖先
  2. 对比"祖先→当前分支"和"祖先→B"两份差异;
  3. 合并两份差异,生成一个合并提交(两个父节点);
  4. 两份差异改到同一文件同一区域时 → 冲突,需人工裁决。

2. 正向合并当时的处境

两边各有对方没有的提交,且都改了同一个文件

code
共同祖先 ├── master 独有:3 个提交 └── 分支X 独有:33 个提交

master 直接合并 → 必须建合并提交 + 人工解冲突。

3. 反向合并的效果

B 在分支 X 上执行 git merge master,冲突在这个提交里已经解决(解决结果固化在合并提交中)。合并后,master 的全部内容都并入了分支 X,分支 X 成为 master 的超集

4. 正向合并变成 fast-forward

此时 master 再合并分支 X,Git 发现:master 的 HEAD 是分支 X 头的祖先。这种情况 Git 不走"找祖先 + 合差异"流程,而是直接 fast-forward:

  • 不创建新提交;
  • 不做差异对比,不存在冲突的可能
  • 只是把 master 指针移动到分支 X 的头。

冲突的解决工作被前置到了功能分支上,master 这边只是"接受结果"。这就是"反向合并把提交变干净"的原理。

七、经验教训

  1. 功能分支只从 master 拉、只往 master 合。分支之间互合是交叉网的起点。
  2. 共享分支上的操作要克制。分支 X 是两人共用的,A 推完 B 拉、B 提交完 A 又拉,每拉一次就多一个交叉点。
  3. 合并前先确认要合的内容merge-base --is-ancestor + rev-list --count 两行命令,能避免"合了个空壳"。
  4. 合并提交的 parents 是还原历史的第一手证据。排查乱历史,先 git log --format="%h %ci %p %s" 把所有 merge 提交拆开看。
  5. 反向合并确实能让收尾变干净,但要验证冲突解决没丢东西
sh
git diff <合并前master> <新头> --stat   # 应只含功能分支的改动git diff <合并前分支头> <新头> --stat   # 应只含master独有提交的改动
  1. 分支职责单一。一个分支只做一件事,混入无关提交会让后续所有追溯都变难。

八、最后

这次排查的每一步都只用了 git loggit merge-basegit rev-list 这几个基础命令——Git 的历史是完备的,乱只是表面,把时间、作者、parents 三样信息对齐,任何乱麻都能还原成一条清晰的因果链。而收尾方案的选择,也恰好是一次对合并机制(merge commit vs fast-forward)的实践检验。