Skip to content

为什么 A 合入 master 无冲突,B 同步 master 却冲突:从 merge-base 理解三方合并

feature-a 合入 master 没有冲突,不代表基于 A 开发的 feature-b 再合并 master 时也不会冲突。

两次 merge 使用的共同祖先不同,比较的提交范围也不同。第一次比较的是 A 与 master 的并行修改,第二次比较的是 B 与 master 的并行修改。只要 B 和 master 修改了相同代码区域,第二次合并就可能产生冲突。

一、还原提交关系

假设存在三个分支:

  • master:主分支。
  • feature-a:从 master 创建的第一阶段功能。
  • feature-b:从 feature-a 创建的第二阶段功能。

在 A 合入 master 前,历史如下:

text
             B1---B2  feature-b
            /
       A1---A2        feature-a
      /
O----+
      \
       M1---M2        master

其中:

  • A1、A2 是 feature-a 的修改。
  • B1、B2 是 feature-b 在 A2 之上的修改。
  • M1、M2 是其他开发者在 master 上提交的修改。
  • feature-b 开发期间没有同步 M1、M2

在 master 上执行:

bash
git switch master
git merge feature-a

如果合并成功,历史变为:

text
             B1---B2  feature-b
            /
       A1---A2        feature-a
      /       \
O----+         MA     master
      \       /
       M1---M2

MA 是 merge commit,它有两个父提交:

text
A2
M2

此时 master 同时包含 A1、A2、M1、M2

二、第一次合并为什么没有冲突

在 master 上执行:

bash
git merge feature-a

Git 会进行三方合并,参与比较的是:

text
共同祖先:O
ours:    master,也就是 M1、M2 一侧
theirs:  feature-a,也就是 A1、A2 一侧

可以把它理解为比较两组差异:

text
O -> M2
O -> A2

如果 M1、M2A1、A2 没有对相同代码区域做不兼容修改,Git 就能自动合并。

因此,第一次无冲突只能证明:

text
A 的修改与当时 master 的修改可以自动合并

它不能证明 feature-b 也能与 master 自动合并。

三、第二次合并的基准已经改变

现在切换到 feature-b,同步最新 master:

bash
git switch feature-b
git merge master

Git 会重新计算 feature-b 和 master 的共同祖先。

由于 feature-b 基于 A2 开发,而 A2 已经通过 merge commit MA 进入 master,因此新的共同祖先通常是:

text
A2

这一次参与三方合并的是:

text
共同祖先:A2
ours:    feature-b,也就是 B1、B2
theirs:  master 在 A2 之后引入的修改

Git 比较的差异变成:

text
A2 -> B2
A2 -> MA

两次 merge 的比较范围完全不同:

操作当前分支merge-baseours 的修改theirs 的修改
master 合并 feature-amasterOM1、M2A1、A2
feature-b 合并 masterfeature-bA2B1、B2master 相对 A2 的修改

第一次比较的是:

text
A 与 master

第二次比较的是:

text
B 与 master

如果 B1、B2 修改了 M1、M2 也修改过的代码区域,第二次合并就可能产生冲突。

四、Git 如何判断冲突

Git merge 不是简单比较两个分支的最终文件,而是进行三方合并:

text
共同祖先
当前分支 ours
被合入分支 theirs

对于每个文件,Git会计算:

text
merge-base -> ours
merge-base -> theirs

如果两边修改互不干扰,Git可以自动组合;如果两边对相同区域进行了无法自动协调的修改,就会要求人工处理。

冲突不只包括“同一行修改不同内容”,还可能包括:

text
一边修改文件,另一边删除文件
两边新增同名文件
文件重命名与内容修改冲突
目录与文件名称冲突
多个相邻修改块无法安全拼接

还需要注意:没有文本冲突,不代表没有业务冲突。Git 只能判断文件内容能否合并,不能判断两个修改组合后业务逻辑是否正确。

五、如何确认实际的 merge-base

查看 feature-b 与 master 的共同祖先:

bash
git merge-base feature-b master

查看完整提交图:

bash
git log --graph --oneline --decorate --all

查看共同祖先到 feature-b 的修改:

bash
BASE=$(git merge-base feature-b master)
git diff "$BASE..feature-b"
powershell
$base = git merge-base feature-b master
git diff "$base..feature-b"

查看共同祖先到 master 的修改:

bash
BASE=$(git merge-base feature-b master)
git diff "$BASE..master"
powershell
$base = git merge-base feature-b master
git diff "$base..master"

如果两个 diff 修改了相同文件和相邻代码区域,就能解释冲突来源。

还可以提前模拟合并结果,但不生成提交:

bash
git switch feature-b
git merge --no-commit --no-ff master

检查完成后取消:

bash
git merge --abort

执行前应确保工作区干净。

六、使用 Merge 同步 master

对于已推送或多人协作的 feature-b,merge 通常更稳妥:

bash
git switch feature-b
git status
git fetch origin
git merge origin/master

发生冲突后:

bash
git status

逐个处理冲突文件,再执行:

bash
git add <resolved-files>
git commit

完成后运行测试:

bash
npm test

根据项目实际情况,也可能需要执行:

bash
npm install
npm run build
npm run lint

确认无误后推送:

bash
git push origin feature-b

merge 会保留 feature-b 原提交,并增加一个 merge commit:

text
             B1---B2------MB  feature-b
            /            /
       A1---A2----------MA   master
      /                 /
O----+-----------------+
      \
       M1---M2

如果 master 在此后没有新增提交,将 feature-b 合回 master 时可能快进;是否实际 fast-forward,还取决于平台合并策略和分支保护规则。

七、使用 Rebase 同步 master

如果 feature-b 只有自己使用,并且团队允许重写历史,可以选择:

bash
git switch feature-b
git status
git fetch origin
git branch backup/feature-b-before-rebase
git rebase origin/master

执行前:

text
             B1---B2  feature-b
            /
       A1---A2
              \
               MA  master
              /
         M1---M2

执行后:

text
A1---A2---MA---B1'---B2'  feature-b

        master

B1'、B2' 是重新生成的提交。它们的代码修改可能与 B1、B2 相同,但父提交发生了变化,因此哈希不同。

处理 Rebase 冲突

rebase 会逐个重放提交,冲突也会按提交分批出现。

解决当前提交的冲突:

bash
git status
git add <resolved-files>
git rebase --continue

取消整个 rebase:

bash
git rebase --abort

跳过当前提交:

bash
git rebase --skip

--skip 会丢弃当前正在重放的整个提交,除非确认修改已经存在或确实不再需要,否则不要使用。

Rebase 后如何推送

如果 feature-b 从未推送过,可以正常推送:

bash
git push -u origin feature-b

如果远程已经存在 rebase 前的 feature-b,需要改写远程历史:

bash
git push --force-with-lease origin feature-b

--force-with-lease 会检查远程分支是否仍指向本地预期的提交。如果其他人已经更新远程分支,推送会被拒绝。

它比下面的命令安全:

bash
git push --force

--force-with-lease 仍然会改写远程历史,不能把它理解成普通 push。

八、Merge 与 Rebase 如何选择

对比项git merge origin/mastergit rebase origin/master
原提交哈希保留重新生成
历史形态保留分叉和 merge commit线性历史
冲突处理通常集中在一次 merge 中按提交逐个处理
共享分支适合通常不适合
是否需要强推不需要已推送时通常需要
并行开发过程保留被重新组织
适合场景团队协作分支个人未共享分支

可以使用以下原则:

text
个人本地分支、尚未共享:可以 rebase
已经推送但确认只有自己使用:协调后可 rebase
多人共同维护的分支:优先 merge
release、master 等共享分支:不要随意 rebase
合入主干:遵循 PR/MR 和仓库保护策略

九、为什么不建议 Rebase 共享分支

假设同事已经基于旧的 B1、B2 开发:

text
B1---B2---C1  colleague

此时将 feature-b rebase 并强推:

text
MA---B1'---B2'  origin/feature-b

Git 不会认为 B1B1' 是同一个提交。其他开发者再同步时,可能出现:

text
本地与远程分叉
重复提交
额外冲突
需要重新 rebase 或 cherry-pick

rebase 前至少确认:

text
分支是否已经推送
是否有人基于该分支继续开发
仓库是否允许 force push
CI、PR 或发布流程是否引用旧提交

任一条件不明确,都应优先选择 merge。

十、降低长期分支的冲突成本

1. 缩短同步间隔

不要等到功能全部完成后才同步 master。可以定期执行:

bash
git fetch origin
git merge origin/master

个人分支也可以根据团队约定执行:

bash
git fetch origin
git rebase origin/master

频繁同步不会消除所有逻辑冲突,但能缩小单次冲突范围,并让开发者在仍熟悉上下文时处理问题。

2. 启用 rerere

Git 可以记录人工解决冲突的方式:

bash
git config --global rerere.enabled true

当后续遇到结构相同的冲突时,Git 会尝试复用之前的解决结果。

查看记录:

bash
git rerere status
git rerere diff

rerere 只复用历史解决方案,仍然需要运行测试确认结果符合当前业务。

3. 明确团队历史策略

建议在 CONTRIBUTING.md 中约定:

text
哪些分支允许 rebase
哪些分支禁止 force push
功能分支如何同步主干
PR 使用 merge、squash 还是 rebase merge
冲突由谁解决

工具配置无法替代团队约定。

十一、需要特别区分的合并方式

前面的分析建立在 feature-a 通过普通 merge 进入 master 的前提下。

如果平台使用的是 squash merge:

text
A1、A2 被压缩成一个新提交 S

那么 A2 通常不会成为 master 的祖先。此时 feature-b 与 master 的 merge-base 可能仍然是更早的提交,Git 可能再次看到 A 的修改,从而产生重复提交或额外冲突。

同样,以下操作也会改变祖先关系:

text
rebase
cherry-pick
reset
force push
删除并重建分支

因此,分析冲突前不要只看提交标题,应检查提交哈希、父节点和 merge-base:

bash
git log --graph --oneline --decorate --all
git merge-base feature-b master
git show -s --format=%P <commit>

十二、关于 Pull --rebase

明确执行:

bash
git pull --rebase

大致相当于:

text
git fetch
git rebase 对应的远程跟踪分支

可以配置 pull 默认使用 rebase:

bash
git config --global pull.rebase true

关闭默认 rebase:

bash
git config --global pull.rebase false

需要注意,普通 git pull 的具体行为取决于 Git 版本、分支关系以及 pull.rebasepull.ff 等配置:

  • 可以直接 fast-forward;
  • 可以使用 merge;
  • 可以使用 rebase;
  • 分支分叉且未配置策略时,也可能要求用户明确选择。

因此,不应简单认为所有 git pull 都会产生 merge commit。

十三、决策清单

遇到类似情况时,可以按以下顺序判断:

text
1. 当前分支和目标分支的 merge-base 是什么?
2. 两边分别修改了 merge-base 之后的哪些内容?
3. feature-a 是普通 merge、squash merge,还是 rebase merge?
4. feature-b 是否已经推送或被他人使用?
5. 团队是否允许重写远程历史?
6. 应该使用 merge 还是 rebase?
7. 冲突解决后是否完成测试和构建?

总结

A 合入 master 没有冲突,B 同步 master 产生冲突,是完全正常的 Git 行为。

根本原因是两次操作使用了不同的共同祖先:

text
第一次比较:A 与 master 的修改
第二次比较:B 与 master 的修改

第一次无冲突只能说明 A 与当时的 master 可以自动合并,不能推导出 B 与 master 也能自动合并。

处理方式取决于分支是否共享:

text
共享分支优先 merge
个人未共享分支可以 rebase
已推送后 rebase 需要谨慎使用 --force-with-lease

真正需要掌握的不是如何消灭所有冲突,而是如何通过 merge-base 理解 Git 正在比较什么,并选择符合团队协作方式的历史策略。

参考资料