跳转到内容
关系图谱

Git merge 详解

5,497 字 21 分钟

属性 1
status
学习中

Git merge 详解

git merge 是 Git 中用于将一个分支的修改合并到另一个分支的核心命令。它是团队协作中最基本的操作之一,理解不同的合并策略和冲突解决方法是高效使用 Git 的关键。

一句话理解

merge 就像「汇流」——将两条支流(分支)的修改汇合到一起,形成完整的代码流。

基本语法

bash
git merge <branch>
git merge --no-ff <branch>
git merge --ff-only <branch>
git merge --squash <branch>
git merge --abort
git merge --continue
git merge --no-commit <branch>

核心概念

合并的本质

合并是将两个分支的修改历史整合在一起。Git 会找到两个分支的共同祖先(merge base),然后将两个分支相对于祖先的修改合并起来。

三种合并方式

方式命令特点历史结构
Fast-forwardgit merge <branch>直接移动 HEAD 指针,不创建合并提交线性
No fast-forwardgit merge --no-ff <branch>强制创建合并提交,保留分支结构分叉合并
Squash mergegit merge --squash <branch>将所有提交压缩为一个,需手动提交线性

Fast-forward 合并

当目标分支没有新的提交时(即 feature 分支是 main 的直接后代),Git 只需移动 HEAD 指针即可完成合并。

bash
git checkout main
git merge feature
# Updating abc1234..def5678
# Fast-forward
#  src/main.ts | 10 ++++++++++
#  1 file changed, 10 insertions(+)
Fast-forward 不产生合并提交

Fast-forward 的结果是线性历史,看不出分支曾经存在。如果你想保留分支合并的痕迹,使用 --no-ff

禁止 fast-forward

bash
# 只允许 fast-forward(如果不能 ff 则失败)
git merge --ff-only feature
# fatal: Not possible to fast-forward, aborting.

# 适用场景:确保 main 是 feature 的祖先(如 rebase 后合并)

No fast-forward 合并

即使可以 fast-forward,也强制创建一个合并提交

bash
git checkout main
git merge --no-ff feature
# 打开编辑器编写合并提交信息
# Merge branch 'feature' into main
何时使用 --no-ff
  • 团队约定所有合并都保留分支结构(便于追溯功能开发历程)
  • 需要在 git log --graph 中清晰看到功能分支的起止
  • 需要方便地 git revert -m 1 <merge-commit> 回滚整个功能

很多团队在 .gitconfig 中设置:

bash
[merge]
    ff = false

Squash merge

将 feature 分支的所有提交压缩为一个修改,放入暂存区但不自动创建提交:

bash
git checkout main
git merge --squash feature
# Squash commit -- not updating HEAD
# 所有修改已暂存,但未提交

git commit -m "feat: 完整的支付模块 (#456)"

Squash merge vs 普通 merge vs Rebase merge

对比项普通 merge--no-ff mergeSquash mergeRebase + merge
历史结构线性或分叉分叉线性线性
保留分支提交❌(压缩为一个)✅(但 SHA 变了)
合并提交可能有一定有
回滚整个功能revert -m 1revert -m 1revert <sha>需要多次 revert
适用场景默认方式保留完整历史PR 提交太碎/太乱追求线性历史
Squash merge 后不要继续在 feature 分支上开发

Squash merge 后,feature 分支的提交并没有真正合入 main 的历史。如果继续在 feature 上开发并再次合并,可能产生冲突或重复代码。

bash
# ❌ squash merge 后继续在同一分支上开发
git merge --squash feature
git commit -m "feat: 功能 A"
# ... 在 feature 上继续开发 ...
git merge --squash feature    # 可能冲突!

# ✅ squash merge 后删除原分支,从 main 重新拉
git merge --squash feature
git commit -m "feat: 功能 A"
git branch -D feature
git checkout -b feature-v2    # 新分支继续开发

处理合并冲突

冲突产生的原因

当两个分支修改了同一文件的同一区域时,Git 无法自动决定保留哪个版本,需要手动解决。

冲突标记格式

<<<<<<< HEAD
// 当前分支(main)的修改
const value = 2;
=======
// 被合并分支(feature)的修改
const value = 3;
>>>>>>> feature

冲突解决流程

bash
# 1. 执行 merge 遇到冲突
git merge feature
# CONFLICT (content): Merge conflict in src/config.ts
# Automatic merge failed; fix conflicts and then commit the result.

# 2. 查看冲突文件
git status
# both modified: src/config.ts

# 3. 编辑冲突文件,删除冲突标记,选择或合并代码
vim src/config.ts

# 4. 标记已解决
git add src/config.ts

# 5. 完成合并
git commit
# 或
git merge --continue

# 如果想放弃合并
git merge --abort

使用工具解决冲突

bash
# 使用 VS Code 解决冲突(推荐)
# VS Code 会显示 Accept Current / Accept Incoming / Accept Both 按钮

# 使用 Git 内置的 mergetool
git mergetool
# 会打开配置的合并工具(vimdiff、meld、kdiff3 等)

# 配置默认合并工具
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

合并策略选项

bash
# 冲突时自动采用当前分支(ours)的版本
git merge -X ours feature

# 冲突时自动采用被合并分支(theirs)的版本
git merge -X theirs feature

# 完全使用 ours 策略(忽略 feature 的所有修改)
git merge -s ours feature
# ⚠️ 注意:-s ours 和 -X ours 不同!
# -s ours:完全忽略 feature 的修改,只保留 main
# -X ours:只在冲突时用 main 的版本,非冲突部分正常合并
-s ours vs -X ours 的区别
  • -s ours(策略级别):完全丢弃被合并分支的所有修改,只记录合并关系
  • -X ours(选项级别):正常合并,只在冲突时优先使用当前分支的版本

大多数情况下你想用的是 -X ours,而不是 -s ours

合并策略(Merge Strategies)

Git 支持多种合并算法,大多数时候你不需要手动指定:

策略命令说明
recursive默认(两个分支合并)最常用的三路合并策略
ortGit 2.33+ 默认recursive 的替代品,更快更准确
resolve-s resolve简单的三路合并,不处理重命名
ours-s ours完全忽略被合并分支的修改
octopus默认(多分支合并)同时合并多个分支
subtree-s subtree将子项目合并到子目录

Octopus 合并(同时合并多个分支)

bash
# 同时合并多个分支
git merge feature-a feature-b feature-c
# 创建一个有 4 个父提交的合并提交

# 注意:octopus 合并不处理冲突
# 如果有冲突,需要逐个合并

实际项目中的 merge 场景

场景 1:Feature 分支合入 main

最基础的工作流——功能开发完成后合入主分支:

bash
# 1. 确保 main 是最新的
git checkout main
git pull origin main

# 2. 合并 feature 分支
git merge --no-ff feature/payment -m "Merge feature/payment: 支付模块 (#456)"

# 3. 推送
git push origin main

# 4. 删除已合并的 feature 分支
git branch -d feature/payment
git push origin --delete feature/payment

场景 2:使用 Pull Request 合并

大多数团队通过 PR/MR 合并,平台提供多种合并选项:

团队应统一合并策略
  • 小型团队/个人项目:Squash merge(保持 main 历史整洁)
  • 大型项目/需要追溯:No-ff merge(保留完整的功能开发脉络)
  • 追求线性历史:Rebase and merge(需要团队熟练掌握 rebase)

在 GitHub 仓库设置中可以限制允许的合并方式: Settings → General → Pull Requests → Allow merge commits / squash / rebase

场景 3:定期同步上游分支

长期开发的 feature 分支需要定期同步 main 的更新:

bash
# 方式 1:merge main 到 feature(推荐给新手)
git checkout feature/long-running
git merge main
# 如果有冲突就解决

# 方式 2:rebase feature 到 main(更干净但需要 force push)
git checkout feature/long-running
git rebase main
git push --force-with-lease origin feature/long-running
merge vs rebase 同步的对比
对比项merge main 到 featurerebase feature 到 main
操作复杂度
历史整洁度有很多合并提交线性,干净
是否需要 force--force-with-lease
冲突解决一次性解决所有可能每个提交都要解决
推荐场景多人协作分支个人 feature 分支

场景 4:紧急 hotfix 合并

线上 bug 需要紧急修复并合并到 main 和 develop:

bash
# 1. 从 main 创建 hotfix 分支
git checkout -b hotfix/critical-bug main

# 2. 修复 bug
git commit -m "fix: 修复订单金额计算错误 (#789)"

# 3. 合并到 main
git checkout main
git merge --no-ff hotfix/critical-bug -m "Merge hotfix: 修复订单金额计算错误"
git tag -a v2.0.1 -m "v2.0.1: hotfix"

# 4. 同时合并到 develop(保持 develop 也有这个修复)
git checkout develop
git merge --no-ff hotfix/critical-bug -m "Merge hotfix into develop"

# 5. 清理
git branch -d hotfix/critical-bug

场景 5:Release 分支合并

发布流程中 release 分支的合并:

bash
# 1. 从 develop 创建 release 分支
git checkout -b release/v3.0 develop

# 2. 在 release 上做发布前的修改(版本号、CHANGELOG)
git commit -m "chore: bump version to 3.0.0"

# 3. 合并到 main
git checkout main
git merge --no-ff release/v3.0 -m "Merge release/v3.0"
git tag -a v3.0.0 -m "Release v3.0.0"

# 4. 合并回 develop
git checkout develop
git merge --no-ff release/v3.0 -m "Merge release/v3.0 back to develop"

# 5. 清理
git branch -d release/v3.0

场景 6:解决「criss-cross merge」

当两个分支互相合并过,历史变得复杂时:

bash
# 场景:A merge B,然后 B merge A,再合并时可能出现问题
# 解决方案:使用 recursive 策略(默认)
git merge -s recursive feature

# 如果递归策略也有问题,尝试指定更多选项
git merge -s recursive -X patience feature

场景 7:合并不相关的仓库

将两个没有共同历史的仓库合并(如合并 monorepo):

bash
# 1. 添加另一个仓库作为 remote
git remote add other-repo https://github.com/org/other-repo.git
git fetch other-repo

# 2. 合并不相关的历史
git merge other-repo/main --allow-unrelated-histories

# 3. 解决可能的冲突
# 4. 提交
--allow-unrelated-histories

从 Git 2.9 开始,默认不允许合并没有共同祖先的分支。使用此选项可以强制合并。常见场景:

  • 合并两个独立的项目
  • 重新初始化的仓库与远程同步
  • Monorepo 整合

高级用法

--no-commit:合并但不自动提交

bash
# 合并但不提交,允许你在提交前做修改
git merge --no-commit feature

# 检查合并结果
git diff --cached

# 做额外修改
vim src/config.ts

# 手动提交
git commit -m "merge: 合并 feature 并调整配置"

合并特定文件(从其他分支获取文件)

bash
# 不是真正的 merge,但经常被误认为是 merge 的需求
# 从 feature 分支获取特定文件
git checkout feature -- src/utils.ts
git commit -m "feat: 从 feature 分支引入 utils"

# 真正的 merge 无法只合并特定文件
# 如果需要部分合并,考虑使用 cherry-pick

预览合并结果

bash
# 模拟合并,查看是否有冲突
git merge --no-commit --no-ff feature
git diff --cached --stat
# 查看合并会影响哪些文件

# 放弃模拟合并
git merge --abort

# 查看合并后的 diff(不实际合并)
git diff main...feature
# 显示 feature 相对于 merge base 的差异

记录合并但不修改代码

bash
# 使用 -s ours 策略:记录合并关系但不引入修改
# 常用于「标记某个分支已经被处理过」
git merge -s ours obsolete-branch -m "merge: 标记 obsolete-branch 已处理"

实用技巧

技巧 1:合并前检查清单

bash
# 1. 确保工作区干净
git status

# 2. 更新目标分支
git checkout main
git pull origin main

# 3. 预览要合并的提交
git log main..feature --oneline

# 4. 预览文件变更
git diff main...feature --stat

# 5. 执行合并
git merge --no-ff feature

技巧 2:撤销合并

bash
# 方式 1:合并后还没推送 → 用 reset
git reset --hard HEAD~1
# 或
git reset --hard ORIG_HEAD

# 方式 2:合并后已推送 → 用 revert
git revert -m 1 <merge-commit-sha>
# -m 1 表示回到第一个父提交(即 main 在合并前的状态)

详见 Git resetGit revert

技巧 3:查看合并提交的详情

bash
# 查看合并提交的两个父提交
git log --oneline -1 <merge-sha>
git cat-file -p <merge-sha>
# 显示 parent 1 和 parent 2

# 查看合并引入了什么修改
git diff <merge-sha>^1 <merge-sha>   # 相对于 main 的差异
git diff <merge-sha>^2 <merge-sha>   # 相对于 feature 的差异

# 查看哪些文件在合并中被修改
git show <merge-sha> --stat

技巧 4:配置合并行为

bash
# 在 ~/.gitconfig 中
[merge]
    # 默认不允许 fast-forward(推荐团队使用)
    ff = false
    # 合并提交信息模板
    log = true
    # 合并时显示冲突的共同祖先版本
    conflictstyle = diff3

# diff3 风格的冲突标记(多显示祖先版本)
# <<<<<<< HEAD
# value = 2
# ||||||| base
# value = 1        ← 祖先版本,帮助理解变更意图
# =======
# value = 3
# >>>>>>> feature
强烈推荐 conflictstyle = diff3

diff3 多显示了冲突区域的祖先版本(原始代码),让你知道「之前是什么 → 两边分别改成了什么」,极大提高冲突解决的准确性。

技巧 5:Git alias 加速合并操作

bash
[alias]
    # 安全合并(no-ff + 显示 log)
    mg = merge --no-ff
    # 查看待合并的提交
    mg-preview = "!f() { git log HEAD..$1 --oneline; }; f"
    # 查看待合并的文件变更
    mg-files = "!f() { git diff HEAD...$1 --stat; }; f"
    # 快速放弃合并
    mg-abort = merge --abort
    # 检查分支是否已合并到当前分支
    mg-check = branch --merged

技巧 6:批量检查未合并分支

bash
# 查看已合并到 main 的分支(可以安全删除)
git branch --merged main

# 查看未合并到 main 的分支
git branch --no-merged main

# 批量删除已合并的分支
git branch --merged main | grep -v "main\|develop" | xargs git branch -d

更多实战场景

场景 8:Git Flow 完整合并流程

在 Git Flow 模型中,不同类型的分支有不同的合并规则:

bash
# Feature → develop(squash 或 no-ff)
git checkout develop
git merge --no-ff feature/payment

# Release → main + develop
git checkout main
git merge --no-ff release/v3.0
git tag -a v3.0.0 -m "Release v3.0.0"
git checkout develop
git merge --no-ff release/v3.0

# Hotfix → main + develop
git checkout main
git merge --no-ff hotfix/critical
git tag -a v3.0.1 -m "Hotfix v3.0.1"
git checkout develop
git merge --no-ff hotfix/critical

场景 9:处理大量冲突的合并

长期未同步的分支合并时可能有几十个冲突文件:

bash
# 1. 先预览冲突量
git merge --no-commit --no-ff feature
git diff --name-only --diff-filter=U
# 列出所有冲突文件
git diff --name-only --diff-filter=U | wc -l
# 42 个冲突文件

# 2. 如果太多,考虑放弃并分批合并
git merge --abort

# 3. 分批策略:先 merge main 到 feature(在 feature 上解决冲突)
git checkout feature
git merge main
# 在 feature 上解决冲突(不影响 main 的稳定性)
# 解决完毕后
git checkout main
git merge --no-ff feature   # 此时应该没有冲突了
大量冲突的处理策略
  • 不要在 main 上解决冲突——先在 feature 上 merge main,在 feature 上解决
  • 按文件类型分批处理——先解决配置文件,再解决业务代码
  • 使用 --ours / --theirs 快速处理明确的文件——如锁文件、自动生成的文件
bash
# 对于 package-lock.json 等锁文件,直接用 theirs
git checkout --theirs package-lock.json
npm install   # 重新生成正确的锁文件
git add package-lock.json

场景 10:合并后的回归测试工作流

bash
# 1. 合并到 staging 分支先测试
git checkout staging
git merge --no-ff feature/payment

# 2. 运行完整测试
npm run test:all
npm run test:e2e

# 3. 测试通过后,再合并到 main
git checkout main
git merge --no-ff feature/payment

# 4. 测试失败?在 staging 上 revert
git checkout staging
git revert -m 1 HEAD

场景 11:处理子模块(submodule)的合并冲突

bash
# 子模块冲突的表现
git merge feature
# CONFLICT (submodule): Merge conflict in lib/shared

# 解决方式 1:使用 feature 分支的子模块版本
git checkout --theirs lib/shared
git add lib/shared

# 解决方式 2:手动选择子模块版本
cd lib/shared
git checkout <desired-commit>
cd ../..
git add lib/shared

# 解决方式 3:更新到子模块的最新版本
git submodule update --remote lib/shared
git add lib/shared

git merge --continue

场景 12:保护分支的合并策略

在团队中配置分支保护规则,限制合并方式:

bash
# GitHub 分支保护规则(Settings → Branches → Branch protection rules)
# ✅ Require pull request reviews before merging(需要审查)
# ✅ Require status checks to pass before merging(需要 CI 通过)
# ✅ Require linear history(只允许 squash 或 rebase merge)
# ✅ Include administrators(管理员也受限制)

# 本地配置:禁止直接推送到 main
# 在 .git/hooks/pre-push 中
#!/bin/bash
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$BRANCH" = "main" ]; then
    echo "❌ 禁止直接推送到 main 分支!请使用 Pull Request。"
    exit 1
fi

冲突解决进阶

使用 rerere 自动记忆冲突解决方案

rerere(REuse REcorded REsolution)让 Git 记住你解决过的冲突,下次遇到相同的冲突时自动应用:

bash
# 启用 rerere
git config --global rerere.enabled true

# 工作流程:
# 1. 第一次合并遇到冲突
git merge feature
# CONFLICT!

# 2. 手动解决冲突
vim src/config.ts
git add src/config.ts
git commit

# 3. rerere 自动记录了解决方案

# 4. 下次遇到相同的冲突(如 rebase 时),rerere 自动应用
git rebase main
# rerere: Resolved 'src/config.ts' using previous resolution

# 管理 rerere 缓存
git rerere status      # 查看当前记录
git rerere diff        # 查看记录的解决方案
git rerere forget <file>  # 忘记特定文件的记录
何时 rerere 特别有用
  • 频繁 rebase 的工作流(每次 rebase 都可能遇到相同冲突)
  • 长期维护的 feature 分支定期同步 main
  • 多个分支需要合并相同的修改

冲突解决的最佳实践清单

bash
# 合并冲突解决完整流程
# 1. 查看冲突文件列表
git diff --name-only --diff-filter=U

# 2. 逐个解决冲突(推荐使用 IDE 的可视化工具)
# VS Code: 点击 "Accept Current" / "Accept Incoming" / "Accept Both"

# 3. 对特定类型文件使用自动策略
git checkout --ours .eslintrc.json       # 保留我们的配置
git checkout --theirs package-lock.json  # 使用他们的锁文件
npm install                              # 重新生成锁文件

# 4. 验证没有残留冲突标记
grep -rn "<<<<<<\|>>>>>>" src/

# 5. 构建和测试
npm run build
npm test

# 6. 提交
git add .
git merge --continue

常见陷阱

陷阱 1:在 main 上直接 merge 而不更新
bash
# ❌ 不更新就合并
git checkout main
git merge feature    # main 可能不是最新的!

# ✅ 先拉取最新代码
git checkout main
git pull origin main
git merge feature
陷阱 2:混淆 -s ours-X ours
bash
# ❌ 想保留 main 的冲突版本,但用了 -s ours
git merge -s ours feature
# 结果:feature 的所有修改都被丢弃了!

# ✅ 应该用 -X ours
git merge -X ours feature
# 只在冲突时保留 main 的版本
陷阱 3:Squash merge 后继续在原分支开发

Squash merge 不会创建合并关系,Git 不知道这些提交已经被合并了。在原分支上继续开发再合并时可能出问题。

解决方案:squash merge 后删除原分支,从 main 重新拉新分支。

陷阱 4:合并冲突时忘记 git add
bash
git merge feature
# 冲突!手动编辑文件解决冲突...

# ❌ 直接 commit(冲突标记还在!)
git commit -m "merge"

# ✅ 先 add,再 commit
git add src/config.ts
git commit -m "merge"
陷阱 5:误用 git merge --abort 丢失工作

merge --abort丢弃所有合并过程中的修改(包括你已经解决的冲突)。确认不再需要这些修改后再使用。

陷阱 6:合并大量冲突时遗漏文件
bash
# 解决冲突后检查是否有遗漏
git diff --check          # 检查冲突标记残留
grep -rn "<<<<<<" src/    # 搜索冲突标记
grep -rn ">>>>>>" src/

常见问题

merge 和 rebase 该用哪个?

没有绝对答案,取决于团队约定:

场景推荐方式
合入 mainmerge
同步上游到个人分支rebase
需要保留完整历史merge
追求线性历史rebase
共享的长期分支merge
个人的短期 feature 分支rebase

详见 Git rebase

Fast-forward 好还是 No-ff 好?
  • Fast-forward:历史更简洁,但看不出分支的起止
  • No-ff:历史更完整,可以清晰看到每个功能的开发范围

推荐默认使用 --no-ff,在 .gitconfig 中设置 merge.ff = false

合并后发现有问题怎么办?
  • 还没推送:git reset --hard ORIG_HEAD
  • 已经推送:git revert -m 1 <merge-sha>

详见 Git resetGit revert

如何查看一个分支是否已合并?
bash
# 检查 feature 是否已合并到 main
git branch --merged main | grep feature

# 或查看 feature 的最新提交是否在 main 中
git merge-base --is-ancestor feature main && echo "已合并" || echo "未合并"
合并时只想保留特定文件的修改怎么办?

merge 不支持只合并部分文件。替代方案:

  1. 使用 git cherry-pick --no-commit + git checkout HEAD -- <files> 选择性保留
  2. 使用 git checkout <branch> -- <file> 获取特定文件

详见 Git cherry-pick

在 Windows PowerShell 中 merge 的注意事项?
powershell
# PowerShell 中 ^ 是转义字符,需要用引号
git diff "HEAD^1" "HEAD"    # 查看合并的第一个父提交差异

# 或使用 ~ 代替
git diff HEAD~1 HEAD

速查表

目标命令
基本合并git merge <branch>
强制创建合并提交git merge --no-ff <branch>
只允许 fast-forwardgit merge --ff-only <branch>
Squash 合并git merge --squash <branch>
合并不提交git merge --no-commit <branch>
冲突时用当前分支版本git merge -X ours <branch>
冲突时用被合并分支版本git merge -X theirs <branch>
完全忽略被合并分支git merge -s ours <branch>
同时合并多个分支git merge <branch1> <branch2>
合并不相关的历史git merge --allow-unrelated-histories
放弃合并git merge --abort
冲突解决后继续git merge --continue
撤销已完成的合并(未推送)git reset --hard ORIG_HEAD
撤销已推送的合并git revert -m 1 <merge-sha>
查看已合并分支git branch --merged
查看未合并分支git branch --no-merged

决策流程图

相关命令

参考资料