教程

git merge:如何合并分支并解决冲突

更新于: · Skillaria.Top

git merge 把另一个分支的历史整合进当前分支:如果当前分支没有前进,Git 只移动指针(快进);否则会创建一个有两个父提交的合并提交。切换到目标分支后执行 git merge 分支名;出现冲突时修改文件,执行 git add 和 git commit,或用 git merge --abort 全部撤销。

git merge 的作用

命令 git merge 取来另一个分支的修改,并把它们嵌入你当前所在的分支。注意合并的方向:是“合并到自己”。要把 feature/login 合并进 main,先切换到 main,然后执行合并。

git switch main
git pull                 # 从服务器拉取最新的 main
git merge feature/login

Git 并不会逐行手工复制。它先找到两个分支的共同祖先(merge base),将两边的版本都与该祖先比较,然后把修改合并起来。如果双方改动的是不同位置,结果会自动生成。如果改动的是同一位置,就会产生冲突,由你来决定最终结果。

两种模式:快进与三方合并

快进(fast-forward):不需要合并提交

假设你从 main 分出了 feature,做了三个提交,而这期间 main 没有任何新提交。那么 main 的历史恰好是 feature 历史的开头部分。Git 会把 main 的指针直接向前移动到 feature 的最后一个提交,不会创建新的提交。

$ git merge feature
Updating 3f2a1c4..9b7e0d2
Fast-forward
 src/auth.js | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

历史保持线性,但“这里曾经有一个 feature 分支”的信息也就消失了。这有时是好事,有时不是,下面的 --no-ff 一节会详细说明。

三方合并与合并提交

如果两个分支都在共同祖先之后前进了,就无法简单地移动指针。Git 执行三方合并:基础版本(共同祖先)、“我们的”一侧(当前分支)和“他们的”一侧(被合并的分支)。结果被记录为一个合并提交,它有两个父提交。

$ git merge feature
Merge made by the 'ort' strategy.
 src/auth.js   | 42 ++++++++++++++++++++
 src/router.js |  3 +-
 2 files changed, 44 insertions(+), 1 deletion(-)

Git 会打开编辑器,并给出类似 Merge branch 'feature' 的说明。你可以保留它,也可以补充说明为什么要合并。想直观地看到这些“分叉”,可以执行:

git log --oneline --graph --all

从 Git 2.34 起,默认策略是 ort;之前使用的是 recursive。日常工作中无需修改策略。

git merge 的常用参数

参数作用何时使用
--no-ff即使可以快进,也总是创建合并提交希望在历史中看出哪些提交来自同一个功能
--ff-only只允许快进,否则直接报错更新本地分支,又不想产生意外的合并提交
--squash把分支的所有修改放入暂存区,但不提交,也不记录与该分支的关联想把一个功能压缩成一个整洁的提交
--no-commit执行合并,但在提交之前停下想在提交前检查结果或运行测试
-m "文本"指定合并提交的说明在脚本中,或不打开编辑器时
--abort中止正在进行的合并,恢复到合并前的状态冲突太多,想重新开始
--continue解决冲突后完成合并作为最后一步 git commit 的替代
-X ours / -X theirs冲突片段自动采用“我们的”或“他们的”版本明确知道哪个版本正确时(请谨慎使用!)

--no-ff:保留功能的边界

很多团队只用 --no-ff 合并功能分支。这样在提交图中,每个任务都会呈现为一个“气泡”,并且可以通过一条 git revert -m 1 <合并提交> 整体撤销。如果是快进,就没有这样的边界,撤销时只能逐个提交处理。

--squash:用一个提交代替十个

git switch main
git merge --squash feature/login
git commit -m "Login with e-mail and password"

使用 --squash 之后,Git 不会把该分支视为已合并:git branch -d feature/login 会拒绝删除,并提示 “not fully merged”。确认所有内容都已正确保存后,再用 git branch -D 强制删除。另外,squash 之后不要继续在同一个分支上工作:下一次合并会再次带入已经合并过的修改,几乎一定会产生冲突。

合并冲突:如何解决

冲突是正常的工作情况,而不是事故。它只意味着一件事:两个分支修改了相同的行(或者一方删除了另一方正在修改的文件),而 Git 不会擅自猜测哪个版本才是正确的。

$ git merge feature/login
Auto-merging src/config.js
CONFLICT (content): Merge conflict in src/config.js
Automatic merge failed; fix conflicts and then commit the result.

第 1 步:查看哪些文件冲突

$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   src/config.js

第 2 步:编辑文件

Git 会在冲突文件中插入标记:

<<<<<<< HEAD
const TIMEOUT = 5000;
=======
const TIMEOUT = 10000;
>>>>>>> feature/login

上半部分是当前分支(HEAD),下半部分是被合并的分支。保留正确的版本(或将两者合并),删除这三行标记,然后保存文件。常见错误是遗漏了文件中间的某个标记;提交前可以用 git diff --check 找出残留的标记。

第 3 步:标记已解决并完成合并

git add src/config.js
git commit          # 或者 git merge --continue

如果整个冲突只需选择一方的版本,就不必手动编辑文件:

git checkout --ours   src/config.js   # 保留当前分支的版本
git checkout --theirs src/config.js   # 采用被合并分支的版本
git add src/config.js

在编辑器中处理更方便

VS Code、JetBrains IDE 等编辑器会以三栏形式显示冲突,并提供“接受当前 / 接受传入 / 接受两者”的按钮。也可以通过 git mergetool 调用外部工具。一个有用的设置是 git config --global merge.conflictStyle zdiff3:标记中会额外显示共同祖先的原始版本,便于看出双方各自改了什么。

改变主意:撤销合并

git merge --abort

该命令会把工作目录和暂存区恢复到执行 git merge 之前的状态。因此合并之前最好保持工作区干净:先提交或用 git stash 暂存未完成的修改,否则它们很难与合并结果区分开。

如何撤销已经完成的合并

如果合并提交还没有推送到服务器,最简单的做法是把分支重置回去:

git reset --hard ORIG_HEAD

ORIG_HEAD 是 Git 在执行危险操作(包括合并)之前记录的分支位置。--hard 会丢弃未提交的修改,所以执行前请检查 git status。

如果合并已经在共享仓库中,就不能改写历史,而应该创建一个反向提交:

git revert -m 1 a1b2c3d

参数 -m 1 表示“主线”是第一个父提交(被合并进的那个分支)。需要注意一个陷阱:经过这样的 revert 之后,Git 会认为该功能的提交已经合并过。如果以后想重新合并修正后的功能,必须先 revert 这个 revert 提交。

团队协作中的合并

git pull 就是 fetch 加上 merge

普通的 git pull 会下载修改,并把远程分支合并进本地分支。如果你和同事同时提交,就会出现“Merge branch 'main' of ...”这样的提交。为了避免这些技术性的合并提交,可以设置 git config --global pull.ff only(无法快进时 pull 失败,由你决定如何处理),或者使用 git pull --rebase。

Pull request 与 Merge 按钮

在 GitHub 和 GitLab 上,合并通常通过 pull/merge request 完成。“Create a merge commit”按钮相当于 git merge --no-ff,“Squash and merge”相当于压缩合并,“Rebase and merge”相当于变基合并。选择哪种方式由团队约定决定,其底层机制与命令行完全相同。

把 main 合并进自己的分支

长期存在的分支会逐渐偏离 main,冲突也会不断累积。应当定期把主分支合并进来:

git switch feature/login
git fetch origin
git merge origin/main

这样冲突会随着出现一点点地解决,最后合并进 main 时就会顺利得多。

常见错误

  • 合并方向搞反了。 在 main 中执行了 git merge main,或者把一个功能合并进另一个功能。合并前可以用 git branch --show-current 确认当前分支。
  • 与过时的分支合并。 合并的是一周没有更新的本地 main。应该先 git pull,再合并。
  • “Your local changes would be overwritten by merge”。 工作目录中有与合并涉及的文件相同的未提交修改。先提交或放入 stash。
  • “refusing to merge unrelated histories”。 两个分支没有共同祖先,例如本地仓库是用 git init 创建的,而远程仓库已经包含了 README。如果确实需要合并,可以加上 --allow-unrelated-histories。
  • 提交了冲突标记。 构建因 <<<<<<< 而失败。提交之前用 git diff --check 检查,并运行测试。

总结

git merge 是一种安全的分支合并方式:它不改写已有的提交,只会添加一个新提交(或移动指针)。应当在目标分支上执行合并,保持工作区干净;如果需要保留功能的边界,使用 --no-ff;如果需要一个提交,使用 --squash。冲突的处理分三步:修改文件,git add,git commit;而 git merge --abort 总能让你回到起点。分支合并与冲突解决在 Git 课程中有实践讲解。

常见问题

git merge 和 git pull 有什么区别?

git pull 先从服务器下载修改(即 git fetch),然后把远程分支合并进当前分支,设置为 rebase 时则变基。git merge 只处理本地已有的内容。

能否一次合并多个分支?

可以,git merge 功能A 功能B 会执行章鱼式合并(octopus),生成多父提交,但前提是没有冲突。实际中通常一次只合并一个分支。

如何查看哪些分支已经合并到当前分支?

git branch --merged 显示已完全合并到当前分支的分支,git branch --no-merged 显示尚未合并的分支。已合并的分支可以放心地用 git branch -d 删除。

合并之前如何预览会带来什么?

执行 git log main..feature 查看 main 中没有的提交,执行 git diff main...feature 查看分支相对于共同祖先的修改。

合并之后的 ORIG_HEAD 是什么?

它是 Git 在合并之前保存的引用,指向分支合并前的位置。只要合并还没有推送到服务器,就可以用 git reset --hard ORIG_HEAD 撤销本地的合并。

通过视频学习本主题

课程“Git 从零基础到胜任开发者”结合实践详细讲解了这一主题。

前往课程