合并冲突解决

4 min中级

Git合并冲突的产生机制、解决策略与预防方法。

1. 冲突概述

1.1 什么是合并冲突

当两个分支修改了同一文件的同一位置时,Git 无法自动决定采用哪个版本,就会产生合并冲突。

1.2 冲突标记

<<<<<<< HEAD
当前分支的内容
=======
合并分支的内容
>>>>>>> feature
标记含义
<<<<<<< HEAD当前分支内容开始
=======分隔线
>>>>>>> feature合并分支内容结束

1.3 会不会冲突的判定

  • 修改不同文件 → 自动合并
  • 修改同一文件的不同位置(互不相邻)→ 自动合并
  • 同一位置双方都改 → 冲突(内容冲突)
  • 一方修改、另一方删除同一文件 → 冲突(modify/delete 冲突,Git 不敢替你决定留哪个)

共同祖先是判定基准:Git 对「你的版本 / 对方版本 / 共同祖先」做三方比较,只有同一位置相对祖先各改各的才无法仲裁。

2. 冲突解决流程

2.1 标准流程

# 1. 尝试合并
git merge feature
# CONFLICT (content): Merge conflict in src/index.js

# 2. 查看冲突文件
git status
# Unmerged paths:
#   both modified:   src/index.js

# 3. 打开冲突文件,手动解决
vim src/index.js

# 4. 标记为已解决
git add src/index.js

# 5. 完成合并
git commit

2.2 查看冲突详情

# 列出冲突文件
git diff --name-only --diff-filter=U

# 查看冲突内容
git diff

# 查看三方视图
git mergetool

3. 解决策略

3.1 手动解决

编辑冲突文件,删除冲突标记,保留正确内容:

<!-- 冲突内容 -->
<<<<<<< HEAD
const API_URL = "https://api.example.com/v2";
=======
const API_URL = "https://api.staging.com/v2";
>>>>>>> feature

<!-- 解决后 -->
const API_URL = "https://api.example.com/v2";

3.2 选择一方

# 采用当前分支版本
git checkout --ours file.txt

# 采用合并分支版本
git checkout --theirs file.txt

# 对特定文件选择
git checkout --ours src/config.js
git checkout --theirs src/styles.css

3.3 策略化批量取舍

# 冲突处一律采用当前分支(谨慎:会静默丢弃对方改动)
git merge -X ours feature

# 冲突处一律采用对方分支
git merge -X theirs feature

注意 -X ours/-X theirs 只在冲突行上偏向一侧,非冲突改动仍会正常合并;与 git merge -s ours(整体忽略对方全部内容)是两回事。

「双方修改都保留」(如变更日志类逐行追加的文件)要用 union 合并驱动——通过 .gitattributes 声明,而不是 -X 选项:

echo "CHANGELOG.md merge=union" >> .gitattributes   # 声明该文件用 union 驱动
git merge feature

union 驱动把双方的行都保留下来,不做语义判断,仅适合「只增不改」的纯追加型文本文件。

3.4 放弃合并

# 放弃当前合并,回到合并前状态
git merge --abort

# 如果已经部分解决
git reset --hard HEAD

4. 复杂冲突场景

4.1 多文件冲突

# 批量选择 ours/theirs
git checkout --ours .
git checkout --theirs .

# 逐文件处理
for file in $(git diff --name-only --diff-filter=U); do
    echo "Conflict in: $file"
    # 手动处理每个文件
done

4.2 重命名冲突

# 一方重命名、一方修改内容
# CONFLICT (modify/delete): ...

# 查看重命名情况
git diff --name-status --diff-filter=R

4.3 子模块冲突

# 子模块指向不同提交
git ls-tree HEAD path/to/submodule
# 选择正确的提交
cd path/to/submodule
git checkout correct-commit
cd ..
git add path/to/submodule

5. 预防冲突

5.1 工作流策略

策略说明
频繁同步经常从主分支拉取更新
小步提交每次提交只做一件事
短生命周期分支功能分支尽快合并
模块化代码减少多人修改同一文件
代码所有者CODEOWNERS 指定负责人

5.2 减少冲突的编码习惯

  • 避免大范围格式化修改
  • 将公共配置与业务逻辑分离
  • 使用接口/抽象减少直接依赖
  • 新增代码而非修改共享代码

5.3 预合并检查

# 合并前检查是否有冲突
git merge --no-commit --no-ff feature
git diff --check     # 检查冲突标记
git merge --abort    # 放弃测试合并

冲突标记格式

基本写法:冲突标记结构 <<<<<<< HEAD ... ======= ... >>>>>>> <分支名>

# 冲突标记格式
<<<<<<< HEAD
当前分支的内容
=======
合并分支的内容
>>>>>>> feature

冲突解决标准流程

基本写法:尝试合并 git merge <分支名>

# 合并 feature 分支到当前分支
git merge feature;

基本写法:查看冲突文件 git status

# 查看冲突状态
git status;

基本写法:标记冲突已解决 git add <file>

# 将解决冲突后的文件加入暂存区
git add src/index.js;

基本写法:完成合并提交 git commit

# 提交合并结果
git commit;

查看冲突详情

基本写法:列出冲突文件 git diff --name-only --diff-filter=U

# 列出所有冲突文件
git diff --name-only --diff-filter=U;

基本写法:查看冲突内容 git diff

# 查看冲突内容
git diff;

基本写法:使用合并工具 git mergetool

# 启动配置的合并工具
git mergetool;

选择一方版本

基本写法:采用当前分支版本 git checkout --ours <file>

# 采用当前分支版本的 src/config.js
git checkout --ours src/config.js;

基本写法:采用合并分支版本 git checkout --theirs <file>

# 采用合并分支版本的 src/styles.css
git checkout --theirs src/styles.css;

合并策略选项

基本写法:冲突时采用当前分支 git merge -X ours <分支名>

# 冲突行偏向当前分支,非冲突改动仍正常合并
git merge -X ours feature

基本写法:冲突时采用合并分支 git merge -X theirs <分支名>

# 冲突行偏向对方分支
git merge -X theirs feature

基本写法:双方修改都保留(union 合并驱动) echo "<文件> merge=union" >> .gitattributes

# 适合纯追加型文件(如变更日志):双方新增行都保留
echo "CHANGELOG.md merge=union" >> .gitattributes

放弃合并

基本写法:放弃当前合并 git merge --abort

# 放弃当前合并操作
git merge --abort;

基本写法:硬重置放弃合并 git reset --hard HEAD

# 强制回到合并前的 HEAD 状态
git reset --hard HEAD;

多文件冲突处理

基本写法:批量采用 ours git checkout --ours .

# 批量采用当前分支版本
git checkout --ours .;

基本写法:批量采用 theirs git checkout --theirs .

# 批量采用合并分支版本
git checkout --theirs .;

基本写法:逐文件处理冲突 for file in $(git diff --name-only --diff-filter=U)

# 遍历所有冲突文件逐个处理
for file in $(git diff --name-only --diff-filter=U); do
    echo "Conflict in: $file"
done

重命名冲突

基本写法:查看重命名情况 git diff --name-status --diff-filter=R

# 查看重命名的文件
git diff --name-status --diff-filter=R;

子模块冲突

基本写法:查看子模块指向的提交 git ls-tree HEAD <子模块路径>

# 查看子模块指向的提交
git ls-tree HEAD path/to/submodule;

基本写法:进入子模块目录 cd <子模块路径>

# 进入子模块目录
cd path/to/submodule;

基本写法:切换到正确的提交 git checkout <提交哈希>

# 切换到正确的提交
git checkout correct-commit;

基本写法:返回主仓库 cd ..

# 返回主仓库
cd ..;

基本写法:添加子模块 git add <子模块路径>

# 添加子模块
git add path/to/submodule;

预合并检查

基本写法:测试合并(不提交) git merge --no-commit --no-ff <分支名>

# 测试合并但不提交
git merge --no-commit --no-ff feature;

基本写法:检查冲突标记 git diff --check

# 检查空白错误和冲突标记
git diff --check;

基本写法:放弃测试合并 git merge --abort

# 放弃测试合并
git merge --abort;