1. 合并分支时最纠结的选择题:冲突文件到底听谁的
我打赌每个用过 Git 的人都被这种局面折磨过:你在 feature/login 分支上吭哧吭哧改了两周,终于要合回 main,结果 git merge 报出一串冲突,打开文件一看,<<<<<<< HEAD 和 >>>>>>> feature/login 把代码切成了两块。你很清楚,这个文件里该保留的是 feature/login 这边的版本,因为那边才是这次需求真正改动的代码,而 main 上的改动只是别人顺手加的注释。
这时候你会怎么做?
大多数人第一反应是打开编辑器,找到每个冲突标记,把当前分支的段落删掉,再把冲突标记清理干净,最后 git add 提交。小范围冲突这么干没问题,可一旦冲突文件有十几二十个,每个文件里还有五六处冲突地段,纯手工处理简直就是体力活。明明心里已经确定了"全部用被合并分支的代码",Git 却没有提供一个专门针对这种需求的一键命令吗?
其实是有的,而且不止一个。关键看你处在什么阶段、想作用到多大范围。本文要把这些方案彻底讲透:从文件级操作到全分支合并策略,再到那些容易把方向搞反的细节坑,全部过一遍。适合正在被代码冲突折磨、希望快速掌握"合并时以某一方为准"这种操作套路的开发者阅读,无论你用的是纯命令行、SourceTree 还是 VS Code 的 Git 插件,底层逻辑都是一套,看懂了到哪儿都好使。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搞懂冲突的本质,才知道"用被合并分支的代码"到底在说什么
2.1 一次合并冲突的完整生命周期
要有针对性地解决问题,先得知道问题是怎么发生的。Git 的合并(merge)本质上是一个三方合并过程:当前分支(HEAD)、被合并分支(要并进来的那个分支)、以及两个分支的共同祖先提交(merge base)。Git 会拿这三个版本去对比,如果共同祖先到两个分支的改动发生在不同的位置,Git 自动完成合并;如果两边改的是同一行甚至同一块内容,Git 没法判断哪个是"正确"的版本,于是把它标记为冲突,把决定权交给你。
举个例子,假设 main 分支和 feature/login 分支的公共祖先版本里有一行配置:
code复制timeout = 30
main 上有人改成了 timeout = 60,feature/login 上你改成了 timeout = 120。两边都是基于同一祖先做的修改,Git 完全猜不到公司最终想要哪个值,于是冲突出现。这个场景里,如果你作为合并的执行人,明确知道"这次要以 feature/login 的代码为准",那么你要做的事就是:让最终合并结果里,这个文件的内容完全等于 feature/login 分支上的样子。
这个目标听起来简单,但要注意一个关键细节:一个文件被标记为冲突,不代表整个文件的每一行都需要选边站。可能文件里只有一处冲突,其他部分 Git 已经自动合并好了。你"用被合并分支的代码",到底是指整个文件都换成对方的,还是只针对冲突片段用对方的?这两种需求的对应解法完全不一样,别搞混。
2.2 "整体换掉"和"冲突片段选边"是两种需求
我先说结论,后面逐个展开:
- 需求 A:冲突文件里,我只想保留被合并分支的版本,当前分支对这个文件的所有改动统统不要。 这种情况用
git checkout --theirs <file>最直接。 - 需求 B:整个文件大部分改动都要保留,只有冲突的那几段代码想采用被合并分支的写法。 这种情况更适合在合并时通过参数
-X theirs让 Git 自动选择,或者用git mergetool配合三方合并工具手动挑选。 - 需求 C:不仅冲突文件,整个分支合并过程中所有出现冲突的地方,全部无条件采用被合并分支。 这种属于"全面倒向一边",用
git merge -X theirs <branch>一次性搞定。
很多人一上来就找命令,结果发现 git checkout --theirs 和 git merge -X theirs 都听说过,但不知道区别,更不知道什么时候该用哪个。下面我用一次完整的实操把每个方案都走一遍。
3. 从零手工造一个冲突现场,再把它亲手解决掉
3.1 三分钟搭出实验仓库
纸上谈兵没意思,我建议你直接照着敲一遍。下面这套操作会在一个临时目录里造出一个真实的冲突现场,全程不碰你的真实项目,随便折腾不心疼。
bash复制# 创建一个实验目录并初始化仓库
mkdir git-merge-demo
cd git-merge-demo
git init
# 创建公共祖先版本
echo "timeout = 30" > config.ini
git add config.ini
git commit -m "init: add config.ini"
# 创建 feature 分支,并在 feature 上修改 config.ini
git checkout -b feature/login
echo "timeout = 120" > config.ini
git commit -am "feat: set login timeout to 120s"
# 切回 main 分支,修改同一个文件
git checkout main
echo "timeout = 60" > config.ini
git commit -am "chore: adjust login timeout to 60s"
执行完之后,仓库结构是这样的:main 和 feature/login 从同一个提交分岔,各自改动了 config.ini 的同一行。接下来执行:
bash复制git merge feature/login
你会看到 Git 的提示:
code复制Auto-merging config.ini
CONFLICT (content): Merge conflict in config.ini
Automatic merge failed; fix conflicts and then commit the result.
冲突如期出现。看一下 config.ini 的内容,标准冲突标记长这样:
code复制<<<<<<< HEAD
timeout = 60
=======
timeout = 120
>>>>>>> feature/login
这里 HEAD 代表你当前所在的 main 分支,feature/login 就是这次合并的被合并分支。如果我们的需求是"冲突时使用被合并分支的代码",那么最终保留的应该是 timeout = 120。
3.2 经典解法:手动清理冲突标记
先看一眼最笨但最稳的方法,因为理解它才能理解其他快捷方式在做什么。打开 config.ini,手动删除 <<<<<<< HEAD、=======、>>>>>>> feature/login 这三行标记,并把 timeout = 60 那行删掉,保留 timeout = 120,保存文件后执行:
bash复制git add config.ini
git commit -m "merge feature/login, resolve conflict with theirs"
至此合并完成。手动方案的优势是灵活,你可以在保留两端代码的基础上自己拼接逻辑;缺点则是前面说过的,冲突一多就累人。当你非常确定"这个文件就要用被合并分支的全部内容"时,完全可以省掉打开编辑器这一步。
3.3 文件级快捷键:git checkout --theirs
真正的主角登场。在合并冲突状态下,Git 会给当前处于冲突状态的文件做两个特殊标记:ours 对应当前分支(HEAD)的版本,theirs 对应被合并分支的版本。这两个标记只有在你处于合并还未完成的状态时才会生效,它们在正常的工作区快照里是不存在的。
我们的需求是"用被合并分支的代码",所以直接执行:
bash复制git checkout --theirs config.ini
这行命令的意思就是:把 config.ini 这个文件的内容,整个替换成被合并分支(theirs)的版本。执行后再看一下文件内容,已经变成 timeout = 120,没有任何冲突标记。接着:
bash复制git add config.ini
git commit -m "merge feature/login, keep theirs config"
两步收尾,搞定。
这里有个容易让人不安的点:git checkout --theirs 会把整个文件覆盖成对方的版本,也就是说当前分支对这个文件做的其他非冲突改动也会一并被丢弃。所以你在用它之前,最好先确认一下这个文件的改动情况:
bash复制git diff main feature/login -- config.ini
如果两边改动比较简单,只有一个冲突点,--theirs 完全是杀鸡用牛刀的安全操作。但如果文件里既有冲突,又有当前分支独有的部分,你用 --theirs 等于把这些独有部分全扔了,这时候就要警惕。
3.4 另一种写法:git checkout --
如果你觉得 ours/theirs 这两个词容易搞混(记住它们其实不难,难的是记忆压力),还有另一种更直白的写法,同样能实现"用被合并分支的代码覆盖当前文件":
bash复制git checkout feature/login -- config.ini
这条命令的含义是:从 feature/login 分支取出 config.ini 这个文件,写入当前工作区和暂存区。它和 git checkout --theirs config.ini 在合并冲突场景下效果基本一致。区别在于:
git checkout --theirs只能在合并过程中使用,一旦合并提交完成或者你中途 abort 了,theirs这个引用就不存在了。git checkout <branch> -- <file>是通用的文件级恢复命令,任何时候你都能从任意分支把任意文件拉过来,不限于合并状态。哪怕你不在合并中,只是想"把这个文件换成隔壁分支的版本",也能用。
我个人的习惯是:如果正处于合并冲突中且明确要选边,用 --theirs/--ours 更直观;如果我要在非合并状态下跨分支取文件,一定用 git checkout <branch> -- <file>。两种写法的切换时要意识到,--theirs 依赖的并不是"某个分支名",而是合并过程中 Git 内部维护的两方索引,所以它不能脱离合并语境存在。
4. 方向陷阱重灾区:theirs 和 ours 在 merge 和 rebase 下语义完全相反
4.1 为什么这么多人在这里翻车
这是全 Git 最容易把人绕晕的概念之一,我必须单独拿出来说,因为它直接影响你"用被合并分支的代码"这个需求能不能达成。
在 git merge 的语境下,方向是这样的:
ours:当前所在分支,也就是执行git merge 其他分支时,HEAD指向的那个分支。theirs:被合并进来的分支,也就是你 merge 命令里写的那个分支。
比如你站在 main 上执行 git merge feature/login,ours = main,theirs = feature/login。这和我们日常的中文直觉一致:主人家是我们的,客人是他们的。
但如果你执行的是 git rebase,方向就完全反过来了。假设你在 feature/login 分支上执行 git rebase main,意思是把 feature/login 的提交一个个取出来,在 main 的基础上重新播放。在 rebase 过程中,ours 指的是 main(因为 rebase 内部把 main 设为基点),而 theirs 反而指的是你正在 rebase 的 feature/login 的提交。这和 merge 时的直觉彻底颠倒。
有实际血泪教训:有人在 feature/login 分支上执行 git rebase main 时遇到冲突,想"用自己分支的代码",于是他用了 git checkout --ours(因为他觉得自己在 feature 分支上,feature 就是"我们的"),结果文件内容被替换成了 main 的版本,半天的心血差点没找回来。
4.2 怎么确认方向,而不是靠背口诀
记忆口诀容易忘,我教你一个在任何场景下都能核实方向的办法。在冲突发生后,直接跑:
bash复制git status
Git 会在输出里用 Unmerged paths 标记出冲突文件。接着对某个冲突文件跑:
bash复制git show :1:config.ini # 公共祖先版本
git show :2:config.ini # ours 版本
git show :3:config.ini # theirs 版本
看到文件内容,你立刻就知道 2 和 3 分别对应哪个分支的代码了。这个方式不依赖任何记忆,直接看内容对号入座,尤其适合在合并中夹着 rebase 的时候用来确认。
再给你一个辅助的判断思路:在 merge 冲突中,git log --oneline -1 HEAD 显示的是当前分支的提交;在 rebase 冲突中,HEAD 很可能是 detached 状态,指向的是 rebase 的目标分支。看到 detached HEAD,就提醒自己这里的方向可能和 merge 时是反的。
4.3 一个小技巧:用 reflog 兜底
我自己处理过太多"用错方向把文件覆盖掉"的情况,所以给所有读者一个保险动作:再执行任何 checkout --theirs/--ours 这种覆盖性操作之前,先跑 git reflog 记下当前 HEAD 的位置。一旦发现操作方向搞反、文件被覆盖,执行:
bash复制git checkout HEAD@{1} -- config.ini
可以快速找回之前的版本。这个习惯成本极低,收益极高。特别是当你面对几十个冲突文件时,手一快可能就造成不可逆后果,reflog 就是你的后悔药。
5. 全局兜底策略:用 merge -X theirs 一次吃下整个分支
5.1 为什么要有整分支级方案
文件级 checkout --theirs 虽好,但架不住场景升级。回到开头说的:如果你要合并的分支里有几十个冲突文件,并且你已经确定"所有冲突都要以被合并分支为准",难道要一条条 git checkout --theirs 去处理吗?虽然能配合脚本批量处理(后面会讲),但更干净的方案是让 Git 在合并时就按你的偏好自动选边。
这就是 -X 参数(全称 --strategy-option)的用武之地。执行合并时加上 -X theirs,Git 遇到冲突会自动选择被合并分支的版本,完全不会停下来打断你:
bash复制git merge -X theirs feature/login
注意,-X theirs 和 --theirs 是两码事,我见过有人把它们混为一谈:
git checkout --theirs <file>是"合并发生后,手动把某个文件整体替换成对方版本"。git merge -X theirs <branch>是"在合并发生时,对每个冲突片段自动采用对方版本"。
5.2 -X theirs 到底帮我们处理了什么、没处理什么
我必须负责任地讲清楚 -X theirs 的作用边界,否则你会踩更大的坑。-X theirs 解决的是内容冲突(content conflict),就是那种两边改了同一行代码导致 Git 不知道该保留哪个的场景。它让 Git 自动选择 theirs 侧的内容。
但它不解决以下情况:
- 双方在同一个文件不同位置都有新增内容,这种通常不会产生冲突,Git 会自动合并,不需要
-X介入。 - 一方删除了文件,另一方修改了文件,这叫 modify/delete 冲突,
-X theirs无法自动处理,Git 依然会停下来问你要不要删除。 - 目录重命名冲突(比如你改了文件名,另一分支把目录整个改造了),同样需要人工介入。
- 二进制文件冲突,
-X theirs可以直接选一边的文件,但选完你最好确认一下这个二进制是不是你要的版本。
所以 -X theirs 适合的场景是:你知道自己分支对某个文件的改动,在合并时全部不重要,冲突时一律采用对方版本。典型的例子是:重新生成的文件(如 package-lock.json、composer.lock 之类)或者自动生成的前端 dist 产物,你在本地不小心改了,合并时希望完全以对方分支为准,这种用 -X theirs 非常省心。
5.3 实测一下:全局自动选边的效果
继续用上面的实验仓库,重新来一遍。先回到干净状态:
bash复制git checkout main
git reset --hard HEAD~1 # 把刚才的合并提交撤销,回到 main 的最新提交
然后执行:
bash复制git merge -X theirs feature/login
注意这次控制台的输出:
code复制Auto-merging config.ini
Merge made by the 'ort' strategy.
没有任何冲突提示,合并直接完成。查看 config.ini,内容是 timeout = 120,确实是 feature/login 的版本。这就是 -X theirs 的威力:一次合并,全部冲突自动选择被合并分支。
如果你反向也想了解一下,-X ours 则是在冲突时一律保留当前分支的版本。注意,这同样只在内容冲突时有效。
5.4 全局用 theirs 之前,请先回答这三个问题
我从不建议无脑用 -X theirs,除非你确定自己的需求确实如此。在执行前,问问自己:
- 我对当前分支的这些文件改动,是不是真的全都无所谓? 如果你的分支在这个文件里有独立需求,只是碰巧和对方改了同一行,用
-X theirs等于把自己的需求整段丢弃。 - 被合并分支的代码能不能独立编译/运行?
-X theirs可能把一些依赖当前分支代码的调用关系一起改了,导致合并后项目编译不过。冲突选边只是内容层面,不保证语义正确。 - 这个分支以后还会不会参与合并? 如果
feature/login之后还会长期维护,把main的内容在冲突时全部丢弃,可能让feature/login的代码长期脱离主干实际状态,最后导致技术债务累积。
6. 冲突文件太多时:批量处理脚本与危险操作清单
6.1 一条命令列出所有冲突文件
有时候你已经完成了合并,冲突文件有二十多个,全是类似"两边都改了注释"这种低价值冲突。你不想逐个人工处理,也错过了用 -X theirs 的时机,怎么办?可以把文件级操作批量跑一遍。
先用这个命令列出所有处于冲突状态的文件:
bash复制git diff --name-only --diff-filter=U
--diff-filter=U 表示只显示 Unmerged 的文件。执行后你会得到一个文件清单,每行一个文件。接下来用 xargs 批量执行 git checkout --theirs:
bash复制git diff --name-only --diff-filter=U | xargs git checkout --theirs
等等,先别急着跑。这个命令有个隐患:如果冲突文件里有刚才说过的 modify/delete 冲突,--theirs 处理不了,命令会报错;如果冲突文件里有些是你精心改过、只因为和对方撞了一行,直接整个用 theirs 会丢掉你的主要工作。所以批量操作的正确姿势是:先看看都有哪些文件冲突,再决定哪些能批量、哪些要单独处理。
更稳妥的做法是分两步:
bash复制# 第一步:查看所有冲突文件列表
git diff --name-only --diff-filter=U
# 第二步:人工确认后,只对挑出来的文件批量替换
git diff --name-only --diff-filter=U -- config.ini package-lock.json | xargs git checkout --theirs
把明确的文件列在命令后面,相当于白名单机制,误伤面小很多。
6.2 批量执行后,别忘记验收
执行完批量 checkout --theirs 后,所有冲突文件已经被覆盖成被合并分支的版本,但此时它们还处于"已解决但未暂存"的状态吗?
实际上 git checkout --theirs 会把文件同时写入工作区和暂存区,所以你可以直接提交。但提交前我强烈建议做一次全局检查:
bash复制# 检查还有没有残留的冲突标记
grep -rn '<<<<<<<\|>>>>>>>\|=======' --include='*.java' --include='*.js' --include='*.ts' . | head -20
如果输出为空,说明所有冲突标记都清理干净了。然后把所有文件加入暂存区,提交即可:
bash复制git add -A
git commit -m "merge branch: resolve all conflicts with theirs"
6.3 千万别用的一条“伪捷径”,以及替代方案
网上一度流传一个"把所有文件都替换成某分支版本"的写法:
bash复制# 危险写法,不要直接复制
git checkout --theirs .
这条命令的本意是"所有冲突文件都采用 theirs",但它的实际行为很容易超出预期:--theirs 配合 . 会把当前目录下所有能匹配到的未合并文件都尝试替换,但它并不会智能地跳过非冲突文件,还可能在某些 Git 版本上有奇怪行为。我见过有人在合并一半时跑这条命令,把原本 Git 已经自动合并好的文件也覆盖成了 theirs 版本,导致当前分支部分合法改动悄悄丢失。
如果确实要实现"当前目录下所有冲突文件用 theirs",请用 6.1 的命令,或者用更明确的 git restore 系列命令:
bash复制git diff --name-only --diff-filter=U | xargs git restore --theirs
git restore --theirs 是 git checkout --theirs 的现代替代写法,语义同样是把文件恢复到 theirs 版本,只是命令名更符合"restore"的直觉。两个命令效果等价,新版 Git 更推荐 git restore。
7. 那些容易被文档忽略的边缘场景与我的实战建议
7.1 当“被合并分支的代码”不止一份时
有一种容易被忽略的情况:被合并分支本身不是一条直线,它可能又合并过其他分支,历史是复杂的。这时候"theirs"对应的是这个分支的最终状态,而不是某一次特定提交。git checkout --theirs 拿到的文件版本,等同于 git checkout feature/login -- <file> 拿到的版本,也等同于分支 tip 处的文件内容。如果你的真实需求是"采用某个特定历史提交的版本",那就不能用 theirs 了,得直接用 git checkout <commit-hash> -- <file>。
比如:
bash复制git checkout a1b2c3d -- config.ini
这条命令会把 a1b2c3d 那次提交里的 config.ini 拿到当前分支,不管它是不是被合并分支的 tip。这种精确到提交的做法,在处理"对方分支最近几次提交把配置改坏了,但我不想合并这些改动"时特别管用。
7.2 CRLF/LF 换行符导致的“伪冲突”
Windows 和 Linux 开发者协作时,经常遇到一种让人想摔键盘的假冲突:两边代码逻辑完全没改,但整个文件都飘红。原因是 Git 把 CRLF 和 LF 的差异也当成了冲突。这种情况下,--theirs 虽然能帮你快速选边,但治标不治本。推荐做一个预防性配置:
bash复制git config --global core.autocrlf true # Windows 用户
git config --global core.autocrlf input # Mac/Linux 用户
同时,在仓库根目录放一个 .gitattributes 文件,强制指定文本文件的换行符规范:
code复制* text=auto
*.js text eol=lf
*.json text eol=lf
这样可以大大减少因换行符导致的虚假冲突。如果仓库已经里已经混入换行符差异,可能还要配合 git add --renormalize . 统一一行。但这类冲突本身和"用哪边代码"无关,属于另一个体系的问题,这里只是提醒你别误把换行符差异当成内容冲突来处理。
7.3 子模块(submodule)冲突里的分支身份
如果你的项目里用了 submodule,子模块的指针变化也可能造成合并冲突。子模块冲突和普通文件冲突的解决方式不太一样:--theirs 不一定能在子模块上正常工作。遇到子模块冲突时,需要先确定你想用哪个版本的 submodule 指针:
bash复制git ls-tree feature/login path/to/submodule
git ls-tree HEAD path/to/submodule
两个命令的输出分别是被合并分支和当前分支记录的 submodule 提交哈希,然后手动把 submodule 切到你想要的那个提交:
bash复制cd path/to/submodule
git checkout <commit-hash>
cd ..
git add path/to/submodule
这种处理方式本质上是"手动选择被合并分支的 submodule 版本",不能简单依赖 --theirs。如果是资产目录或模型文件类的 submodule,我建议你在合并前就明确好应该用哪个版本,避免合并过程中慌乱。
7.4 图形化工具用户怎么对应这套逻辑
不少同学用 SourceTree 或 VS Code 处理合并冲突。这些工具底层用的还是 Git 的那套概念,只是把命令包装成了界面操作。
- 在 SourceTree 里遇到冲突时,你可以对冲突文件右键,通过"解决冲突"菜单选择"采用 Themis 版本"或"采用我的版本",对应的就是本文说的
--theirs和--ours。注意 SourceTree 的"我的版本"="ours","他们的版本"="theirs"。 - 在 VS Code 里,冲突区域会直接显示三个按钮:"Accept Current Change"(保留当前分支)、"Accept Incoming Change"(保留被合并分支)、"Accept Both Changes"(两端都保留)。你要选的是第二个,也就是"Incoming(传入的)"。
- 如果要用 IDE 解决完冲突后还需不需要执行 Git 命令,答案是看工具是否已经帮你完成
git add。SourceTree 和 VS Code 通常在操作时会自动暂存,但保险起见,提交前在git status里确认一下有没有遗漏。
7.5 合并完成后的一步“体检”
不管用哪种方式解决了冲突,提交之后我建议再做一次快速体检:
bash复制git log --oneline --graph -5 # 确认合并拓扑正确
git diff main feature/login --stat # 确认两边最终差异是否符合预期
git status # 确认工作区干净
如果发现合并结果有问题、需要推翻重来,记住你还有后悔药:
bash复制git reset --hard ORIG_HEAD
ORIG_HEAD 是 Git 在执行合并这样的大操作前自动记录的 HEAD 原位置。注意 reset --hard 会丢工作区未提交的改动,所以执行前确认一下当前状态。
8. 最后分享一点实际项目里的经验取舍
处理过大量合并之后,我个人的习惯是:如果冲突文件少于三个,直接手动解决,不动用 -X 或批量脚本;如果冲突文件多且对"以哪边为准"有明确答案,优先在合并命令上就加 -X 参数,让 Git 一步到位;如果冲突里混杂着多种类型,比如大部分能用 theirs,但个别文件需要手工整理,那就先 git merge -X theirs 跑一遍,再对剩下的特殊情况单独处理。
至于用 --theirs 还是 --ours 的选择,我的经验法则是:想清楚"这次合并的目的"是什么。如果目的是把 feature/login 的功能带回主干,那主干和 feature 冲突时,主干上的临时改动通常可以让步;如果目的是把 main 的最新修复同步到 feature 分支上(比如 merge main into feature),那 feature 分支自身的功能改动才是核心,冲突时反而要优先保护 feature 侧,也就是用 --ours(因为当前在 feature 分支上,ours 才是 feature)。方向问题想清楚,命令只是执行工具。
还有一个很多人不知道但很实用的小技巧:当一个文件冲突时,你其实可以先看 git diff --cc 或 git log --merge 来了解两边的提交历史,搞清楚"对方为什么改这一行",然后再决定选边。真正的资深开发者处理冲突,从来不只看代码本身,而是会顺着提交记录理解意图。代码冲突往往不是技术问题,而是沟通问题,搞清楚两边各自想干什么,合并结果才不会在语义上留下隐患。
希望这篇文章能帮你把"合并时采用被合并分支代码"这个操作彻底吃透。不管是单文件还是全分支,不管是 merge 还是 rebase,遇到冲突时多做几次实验,很快就能形成肌肉记忆。
