Git: Git合并与冲突解决详解

合并是将不同分支的修改整合到一起的操作。通过合并,团队可以并行开发不同功能,最后统一整合。理解合并原理和冲突解决方法对团队协作至关重要。

1. 合并基础概念

(1) 什么是合并

合并是将一个分支的修改应用到另一个分支的操作。Git会尝试自动合并修改,当两个分支修改同一位置时可能产生冲突。

100%
graph TB
    A[main分支] --> B[合并feature分支]
    B --> C{是否冲突?}
    C -->|无冲突| D[自动合并成功]
    C -->|有冲突| E[手动解决冲突]
    E --> F[标记冲突已解决]
    F --> G[完成合并]
    
    style D fill:#d4edda
    style E fill:#fff3cd

(2) 合并的类型

Git支持两种合并方式:

(3) 合并的方向

TEXT 📖 仅展示
git merge feature  # 将feature合并到当前分支

合并总是将指定分支合并到当前分支,所以合并前要确认当前分支。


2. 快进合并

(1) 快进合并原理

当目标分支(如main)没有新提交,而要合并的分支(如feature)有新提交时,Git可以直接将main指针向前移动,这称为快进合并。

100%
graph LR
    subgraph 合并前
        C1[提交C1] --> C2[提交C2]
        C2 --> C3[提交C3]
        main1[main] --> C2
        feature1[feature] --> C3
    end
    
    subgraph 合并后
        C4[提交C1] --> C5[提交C2]
        C5 --> C6[提交C3]
        main2[main] --> C6
        feature2[feature] --> C6
    end
    
    style main1 fill:#fff3cd
    style main2 fill:#d4edda

(2) 执行快进合并

▶ 示例:快进合并

BASH
# 查看当前状态
git log --oneline --graph --all

# 输出:
# * d4e5f6g (feature) Add feature
# * a1b2c3d (HEAD -> main) Initial commit

# 切换到main分支
git switch main

# 合并feature分支
git merge feature

# 输出:
# Updating a1b2c3d..d4e5f6g
# Fast-forward
#  feature.js | 1 +
#  1 file changed, 1 insertion(+)
#  create mode 100644 feature.js

# 查看合并结果
git log --oneline --graph --all

# 输出:
# * d4e5f6g (HEAD -> main, feature) Add feature
# * a1b2c3d Initial commit

(3) 禁用快进合并

有时希望即使可以快进合并,也创建合并提交,保留分支历史。

▶ 示例:禁用快进合并

BASH
# 使用--no-ff禁用快进合并
git merge --no-ff feature

# 输出:
# Merge made by the 'ort' recursive strategy.
#  feature.js | 1 +
#  1 file changed, 1 insertion(+)
#  create mode 100644 feature.js

# 查看历史
git log --oneline --graph --all

# 输出:
# *   a1b2c3d (HEAD -> main) Merge branch 'feature'
# |\
# | * d4e5f6g (feature) Add feature
# |/
# * h7i8j9k Initial commit

3. 三方合并

(1) 三方合并原理

当两个分支都有新提交时,Git需要找到共同祖先(合并基),然后进行三方合并。

100%
graph TB
    A[共同祖先<br/>合并基] --> B[main分支提交]
    A --> C[feature分支提交]
    B --> D[三方合并]
    C --> D
    D --> E[合并提交]
    
    style A fill:#fff3cd
    style E fill:#d4edda

(2) 执行三方合并

▶ 示例:三方合并

BASH
# 查看分支状态
git log --oneline --graph --all

# 输出:
# * d4e5f6g (feature) Feature work
# | * a1b2c3d (HEAD -> main) Main work
# |/
# * h7i8j9k Initial commit

# 合并feature分支
git merge feature

# 输出:
# Merge made by the 'ort' recursive strategy.
#  feature.js | 1 +
#  1 file changed, 1 insertion(+)

# 查看合并结果
git log --oneline --graph --all

# 输出:
# *   k9l0m1n (HEAD -> main) Merge branch 'feature'
# |\
# | * d4e5f6g (feature) Feature work
# * | a1b2c3d Main work
# |/
# * h7i8j9k Initial commit

(3) 查看合并基

▶ 示例:查找共同祖先

BASH
# 查找两个分支的共同祖先
git merge-base main feature

# 输出:
# h7i8j9k

# 查看将要合并的内容
git diff $(git merge-base main feature) feature

# 或者使用三点语法
git diff main...feature

4. 合并冲突解决

(1) 冲突的产生

当两个分支修改同一文件的同一位置时,Git无法自动合并,会产生冲突。

▶ 示例:产生冲突

BASH
# 在main分支修改文件
echo "main content" > file.js
git add file.js
git commit -m "Main modification"

# 切换到feature分支
git switch feature

# 修改同一文件的同一位置
echo "feature content" > file.js
git add file.js
git commit -m "Feature modification"

# 切换回main并合并
git switch main
git merge feature

# 输出:
# Auto-merging file.js
# CONFLICT (content): Merge conflict in file.js
# Automatic merge failed; fix conflicts and then commit the result.

(2) 查看冲突状态

▶ 示例:分析冲突

BASH
# 查看状态
git status

# 输出:
# On branch main
# You have unmerged paths.
#   (fix conflicts and run "git commit")
# 
# Unmerged paths:
#   (use "git add <file>..." to mark resolution)
#   both modified:   file.js

# 查看冲突文件
cat file.js

# 输出:
# <<<<<<< HEAD
# main content
# =======
# feature content
# >>>>>>> feature

# 查看冲突详情
git diff

# 输出:
# <<<<<<< HEAD
# -main content
# =======
# -feature content
# >>>>>>> feature

(3) 解决冲突

▶ 示例:手动解决冲突

BASH
# 编辑冲突文件,选择保留的内容
cat file.js

# 方式1:保留main的修改
# main content

# 方式2:保留feature的修改
# feature content

# 方式3:合并两者的修改
# main content
# feature content

# 标记冲突已解决
git add file.js

# 完成合并
git commit

# 输出:
# [main d4e5f6g] Merge branch 'feature'

(4) 使用工具解决冲突

▶ 示例:使用合并工具

BASH
# 使用图形化合并工具
git mergetool

# 使用特定工具
git mergetool --tool=vimdiff

# 查看可用的合并工具
git mergetool --tool-help

# 使用checkout选择版本
git checkout --ours file.js      # 选择当前分支版本
git checkout --theirs file.js    # 选择合并分支版本

(5) 冲突标记详解

TEXT 📖 仅展示
<<<<<<< HEAD
当前分支的内容(HEAD)
=======
要合并分支的内容
>>>>>>> feature

5. 合并策略

(1) 合并策略选项

Git支持多种合并策略:

策略 说明 使用场景
recursive(默认) 递归三方合并 大多数情况
octopus 合并多个分支 合并多个分支时自动使用
ours 忽略其他分支 保留当前分支
theirs 忽略当前分支 保留要合并的分支
resolve 简单三方合并 特殊情况

▶ 示例:使用合并策略

BASH
# 使用recursive策略(默认)
git merge feature

# 使用ours策略(忽略feature的修改)
git merge -s ours feature

# 使用recursive策略的选项
git merge -X ours feature    # 冲突时优先选择当前分支
git merge -X theirs feature  # 冲突时优先选择要合并的分支
git merge -X ignore-space-change feature  # 忽略空白变化

(2) 压缩合并

将多个提交压缩为一个提交再合并。

▶ 示例:压缩合并

BASH
# 压缩合并
git merge --squash feature

# 查看状态
git status

# 输出:
# Changes to be committed:
#   new file:   feature1.js
#   new file:   feature2.js

# 提交压缩后的修改
git commit -m "Merge feature branch (squashed)"

# 优点:保持主分支历史简洁
# 缺点:丢失feature分支的详细历史

(3) 仅合并但不提交

▶ 示例:预览合并

BASH
# 合并但不自动提交
git merge --no-commit feature

# 查看合并结果
git status
git diff --staged

# 如果满意,手动提交
git commit -m "Merge feature branch"

# 如果不满意,取消合并
git merge --abort

6. 合并相关操作

(1) 中止合并

▶ 示例:取消正在进行的合并

BASH
# 合并产生冲突
git merge feature
# CONFLICT (content): Merge conflict in file.js

# 取消合并,恢复到合并前状态
git merge --abort

# 或者使用reset
git reset --merge

(2) 查看合并历史

▶ 示例:查看合并提交

BASH
# 查看所有合并提交
git log --merges --oneline

# 输出:
# d4e5f6g Merge branch 'feature'
# a1b2c3d Merge branch 'develop'

# 查看非合并提交
git log --no-merges --oneline

# 查看合并的详细信息
git show --stat d4e5f6g

(3) 撤销合并

▶ 示例:撤销已完成的合并

BASH
# 撤销最后一次合并(合并提交还在)
git reset --hard HEAD~1

# 使用revert撤销合并(创建新提交)
git revert -m 1 HEAD

# -m 1 表示保留第一个父提交(main分支)

# 查看结果
git log --oneline --graph

❓ 常见问题

Q 快进合并和三方合并有什么区别?
A 快进合并是目标分支没有新提交时,直接移动指针,不创建合并提交。三方合并是两个分支都有新提交时,找到共同祖先进行合并,创建合并提交。
Q 如何解决合并冲突?
A 编辑冲突文件,删除冲突标记(<<<<<<<、=======、>>>>>>>),保留需要的内容,然后 git add 标记冲突已解决,最后 git commit 完成合并。
Q 什么时候使用--no-ff选项?
A 当希望保留分支历史,明确显示这是一次合并操作时使用。通常在合并功能分支到主分支时使用,以便在历史中看到分支的合并点。
Q 如何取消正在进行的合并?
A 使用 git merge --abort 取消合并并恢复到合并前的状态。如果已经解决了部分冲突,也可以使用 git reset --merge 重置。
Q squash合并和普通合并有什么区别?
A squash合并将分支的所有提交压缩为一个提交,不保留分支的详细历史。优点是主分支历史简洁,缺点是丢失了分支的提交历史和上下文信息。

📖 小节


📝 作业

  1. 基础题:创建两个分支,在不同分支上修改不同文件,然后合并,体验快进合并和三方合并的区别。

  2. 进阶题:模拟冲突场景:在两个分支上修改同一文件的同一位置,合并产生冲突,手动解决冲突并完成合并。

  3. 挑战题:使用不同的合并策略(--no-ff、--squash、-X ours等)合并分支,比较不同策略的效果和适用场景。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏