git pull 冲突解决示意图:分支合并时出现冲突标记,最终解决并显示绿色对勾

如何解决 git pull 冲突?

git pull 本质是 fetch + merge/rebase,看懂报错、找到冲突标记、编辑后 add 再 commit 或 rebase --continue,冲突就能有条不紊地解决。本文整理了从报错识别、ours/theirs 选择、stash 与撤回,到工具和预防习惯的完整流程。

写代码这些年,git pull 报错大概是我在终端里见得最多的提示之一。尤其是多台电脑之间切换、或者和别人一起维护同一个仓库的时候,拉一下代码就冒出一堆红字,第一反应往往是慌。

其实 git pull 冲突并不神秘,搞清楚它在做什么、看懂报错在说什么,处理起来就是几条固定命令的事。这篇把我平时的处理流程完整整理一遍,方便自己查,也给遇到同样问题的朋友参考。

为什么 git pull 会冲突

先记住一句话:git pull 就是 git fetch 加上 git merge(或 git rebase)。

git pull origin main
# 大致等价于
git fetch origin
git merge origin/main      # 默认行为
# 或者(配置了 pull.rebase=true 时)
git rebase origin/main

fetch 只是把远端的新提交下载下来,永远不会冲突。出问题的是第二步:Git 要把远端的提交和你本地的提交合到一起。

如果你和远端改的是不同文件,或者同一个文件的不同位置,Git 会自动合并。只有当双方改了同一个文件的同一块区域,Git 没法判断该留谁的,才会停下来交给你处理,这就是冲突。

先看懂报错:三种常见情况

pull 失败的提示不止一种,处理方法也不一样,先分清楚是哪种。

1. 本地有未提交的修改会被覆盖

error: Your local changes to the following files would be overwritten by merge:
        src/config.js
Please commit your changes or stash them before you merge.
Aborting

这还不是冲突。意思是你工作区里有没提交的改动,而远端恰好也改了这些文件,Git 为了不弄丢你的修改,直接拒绝执行,什么都没动。

处理方式有两种:要么先 commit,要么先 stash(后面细说)。

2. 分支已经分叉,需要指定合并方式

较新版本的 Git 里,如果本地和远端各自都有新提交,可能会看到:

hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
fatal: Need to specify how to reconcile divergent branches.

这也不是冲突,而是 Git 要你选一个策略:

  • pull.rebase false:用 merge,会多一个合并提交,历史如实保留。
  • pull.rebase true:用 rebase,把你的本地提交"挪到"远端最新提交之后,历史是一条直线。
  • pull.ff only:只允许快进,分叉了就直接失败,让你手动处理。

我自己个人项目一般全局设成 rebase:

git config --global pull.rebase true

团队项目就按团队约定来。只想这一次临时指定,也可以直接用 git pull --rebase 或 git pull --no-rebase。

3. 真正的合并冲突

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

看到 CONFLICT 才是真正的冲突。此时 Git 已经合并了能自动合并的部分,剩下冲突的文件等你手动处理,仓库处于"合并进行中"的状态。

一步步解决冲突

第一步:git status 看清现状

git status

输出里 Unmerged paths 下面列出的就是冲突文件,标记为 both modified。文件多的时候,可以只列冲突文件名:

git diff --name-only --diff-filter=U

第二步:读懂冲突标记

打开冲突文件,会看到类似这样的内容:

<<<<<<< HEAD
const API_BASE = "https://api.example.com/v2";
=======
const API_BASE = "https://api.example.com/v3";
>>>>>>> origin/main
  • <<<<<<< HEAD 到 ======= 之间:你当前分支(本地)的版本。
  • ======= 到 >>>>>>> 之间:要合并进来的版本(这里是远端)。

推荐打开一个配置,让冲突块里额外显示"双方改之前的原始版本",判断起来清楚很多:

git config --global merge.conflictStyle zdiff3   # Git 2.35+,旧版本用 diff3

开启后冲突块中间会多一段 ||||||| base,你能看到原来是什么、双方分别改成了什么。

第三步:手动编辑,保留正确的结果

冲突的解决不是"二选一"这么简单,有时候要保留一边,有时候要两边都要,有时候要重新写一版。关键是理解双方为什么这么改。

编辑完之后,把 <<<<<<<、=======、>>>>>>> 这些标记全部删掉,文件内容应该是你最终想要的样子。

提交前可以检查一下有没有漏掉的标记:

git diff --check

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

git add src/config.js

git add 在这里的含义是"这个文件的冲突我处理好了"。所有冲突文件都 add 之后:

如果是 merge,直接提交,Git 会自动生成合并提交信息,保存即可:

git commit

如果是 rebase(用了 git pull --rebase 或 pull.rebase=true),则继续 rebase:

git rebase --continue

注意 rebase 过程中不要自己 git commit。rebase 是逐个重放你的本地提交,后面的提交可能还会再冲突,重复"编辑 → add → continue"直到结束。如果某个提交的改动已经没有意义了,可以用 git rebase --skip 跳过。

整个文件直接用一方的版本:ours 和 theirs

有时候不需要逐行比对,比如某个配置文件你确定就用远端的。可以直接选一边:

git checkout --ours   path/to/file    # 用"ours"的版本
git checkout --theirs path/to/file    # 用"theirs"的版本
git add path/to/file

新版本 Git 也可以用 git restore --ours / --theirs,效果一样。

这里有个很容易踩的坑:ours 和 theirs 的含义在 merge 和 rebase 中是反过来的。

  • merge(git pull):ours 是你的本地分支,theirs 是远端拉下来的提交。
  • rebase(git pull --rebase):ours 是远端(被变基到的那一方),theirs 是你正在重放的本地提交。

原因是 rebase 时 Git 先切到远端最新的提交上,再把你的提交一个个"摘"过来,所以站在 Git 的视角,远端才是"我们",你的提交反而成了"他们"。我每次在 rebase 中用 --ours/--theirs 之前都会先想一下这张表,选错了等于把自己的改动丢掉。

先 stash,再 pull

遇到第一种报错(本地改动会被覆盖),又暂时不想提交半成品时,用 stash 最方便:

git stash          # 把未提交的修改暂存起来,工作区恢复干净
git pull
git stash pop      # 把修改恢复回来

git stash pop 也可能产生冲突,处理方式和上面一样:编辑文件、git add。需要注意的是,pop 冲突时这条 stash 不会被自动删除,确认没问题后要手动清理:

git stash drop

如果你用 rebase 方式 pull,还可以让 Git 自动帮你做 stash:

git pull --rebase --autostash
# 或者全局开启
git config --global rebase.autoStash true

搞乱了?先撤回再说

冲突处理到一半发现思路不对,或者改乱了,不用硬着头皮继续,可以直接回到 pull 之前的状态:

git merge --abort     # 放弃本次 merge
git rebase --abort    # 放弃本次 rebase

这两条命令会把仓库恢复到操作开始前的样子。冷静一下,想清楚再重新 pull。这是我觉得最值得记住的两条命令,有退路心里就不慌。

借助工具:别硬啃冲突标记

冲突多、文件长的时候,纯文本编辑很容易看花眼,用图形化工具效率高很多。

VS Code

VS Code 打开冲突文件后,每个冲突块上方会出现几个按钮:

  • Accept Current Change:保留当前(HEAD,即 ours)的版本
  • Accept Incoming Change:保留合并进来的版本(theirs)
  • Accept Both Changes:两边都保留
  • Compare Changes:并排对比

文件右下角还有 Resolve in Merge Editor,打开三栏合并编辑器:左右分别是双方的版本,下方是最终结果,可以逐块勾选,比手动删标记直观。左侧源代码管理面板里也会单独列出 "Merge Changes",处理完点 + 就等于 git add。

git mergetool

习惯命令行的话,可以配置 git mergetool,冲突时一条命令依次打开所有冲突文件:

# 以 VS Code 作为合并工具为例
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

# 有冲突时运行
git mergetool

也可以换成 vimdiff、Beyond Compare、Meld、Kaleidoscope 等任何你顺手的工具。

预防:少冲突比会解决更重要

冲突不可能完全避免,但很多冲突是习惯问题造成的。下面这些是我觉得最有用的几条:

  1. 经常 pull。开始干活前先拉一下,长时间不同步,积攒的差异越大,冲突越难处理。
  2. 小步提交。一个提交只做一件事,冲突时上下文清楚,也方便定位。几天才提交一次的大改动,冲突起来最痛苦。
  3. 用 pull --rebase 保持历史整洁。个人分支上效果尤其好,没有一堆无意义的合并提交。但已经推送、别人也在用的公共分支,不要随便 rebase 改写历史。
  4. 共享文件提前沟通。路由表、全局配置、公共样式这类大家都会改的文件,动之前打个招呼,或者拆成多个文件,能省掉很多麻烦。
  5. lockfile 冲突别手改。package-lock.json、pnpm-lock.yaml、yarn.lock、composer.lock 这类文件是工具生成的,手动合并几乎不可能改对。正确做法是先解决 package.json 等依赖声明文件的冲突,然后让 lockfile 取一方版本,再重新安装让工具重新生成:
# merge 场景下,先取远端的 lockfile(注意 rebase 时 ours/theirs 相反)
git checkout --theirs package-lock.json
npm install                       # 根据最终的 package.json 重新生成 lockfile
git add package.json package-lock.json

另外还有一个可选项:开启 rerere。git config --global rerere.enabled true 之后,Git 会记住你解决过的冲突,同样的冲突再出现时自动套用之前的解法,经常反复 rebase 的分支很有用。

总结:遇到 git pull 冲突的检查清单

最后把流程压缩成一张清单,下次遇到直接照着走:

  • 看报错:是"本地修改会被覆盖"、"分支分叉",还是真正的 CONFLICT?
  • 本地有未提交修改:先 git commit 或 git stash
  • 分支分叉:选择 merge 或 rebase(git config pull.rebase ...)
  • 真冲突:git status 找到冲突文件
  • 打开文件,理解 <<<<<<< / ======= / >>>>>>> 两边的改动,编辑成最终结果
  • 删干净冲突标记,git diff --check 检查一遍
  • git add 标记已解决
  • merge 用 git commit,rebase 用 git rebase --continue
  • 整个文件选一方:--ours / --theirs,记得 rebase 时含义相反
  • 搞乱了:git merge --abort / git rebase --abort
  • lockfile 冲突:解决依赖声明后重新 install 生成
  • 最后跑一遍项目和测试,确认合并后的代码能正常工作,再 push

冲突本身不可怕,可怕的是没看懂就乱删一通。理解了 pull 的本质是 fetch + merge/rebase,剩下的就是按步骤来。

暂无评论