写代码这些年,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/mainfetch 只是把远端的新提交下载下来,永远不会冲突。出问题的是第二步: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.jsgit 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 等任何你顺手的工具。
预防:少冲突比会解决更重要
冲突不可能完全避免,但很多冲突是习惯问题造成的。下面这些是我觉得最有用的几条:
- 经常 pull。开始干活前先拉一下,长时间不同步,积攒的差异越大,冲突越难处理。
- 小步提交。一个提交只做一件事,冲突时上下文清楚,也方便定位。几天才提交一次的大改动,冲突起来最痛苦。
- 用 pull --rebase 保持历史整洁。个人分支上效果尤其好,没有一堆无意义的合并提交。但已经推送、别人也在用的公共分支,不要随便 rebase 改写历史。
- 共享文件提前沟通。路由表、全局配置、公共样式这类大家都会改的文件,动之前打个招呼,或者拆成多个文件,能省掉很多麻烦。
- 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,剩下的就是按步骤来。




暂无评论