你是不是也遇到过这种情况——明明只是改了十几行代码,git pull一下,直接弹出一大段“CONFLICT”提示,瞬间整个人都不好了。再严重点,如果冲突发生在几百行的文件里,处理起来就像在拆定时炸弹。我在测某个跨平台项目的时候,开发分支和主分支各改各的,合并那天光是解决冲突就花了一下午,从那以后我认真把 Git 冲突的原理和实操从头梳理了一遍,才发现这件事完全有规律可循。
这篇内容不打算只教你怎么“选别人的版本”或者“选自己的版本”就完事。我要从 Git 的底层合并机制讲起,再到实际解决冲突的每一步操作、工具选型、常见翻车点,全部按项目实操的方式走一遍。如果你刚接触 Git 没多久,或者正在被一堆<<<<<<<标记折磨,跟着这篇走完,至少以后看到CONFLICT不会慌。
1. 冲突的根源:为什么 Git 会“看不懂”你想要什么
1.1 分叉提交:冲突产生的物理前提
Git 的冲突不是“文件被改了”就会发生,它有一个严格的前提——提交历史发生了分叉。
想象一下,你和同事在同一份文档上写内容。如果两个人是一个接一个改的,Git 只需要把后一个人的改动直接接到前面,这叫 fast-forward 合并,非常顺滑。但只要两个人基于同一个旧版本各自创造新历史,分歧就出现了。
具体到 Git 操作上,典型的场景是:从main分支切出feature分支,A 在feature上改了文件第一行,B 在main上改了文件第一行。此时两个分支的最近共同提交点(merge base)是同一个 commit,但两边都从那一点“往前走了”。Git 要合并这两个方向时,发现同一个位置存在两种不同的意图,它没法替你判断该听谁的,于是冲突。
这里有个关键知识点:Git 是基于内容(上下文)合并的,不是基于行号合并的。即使两个分支改动的是同一文件的不同行,只要改动位置靠得很近,Git 也可能因为“改动上下文重叠”而判定冲突。所以别以为“他改了文件头,我改了文件尾”就一定安全。
1.2 三路合并:Git 判断冲突的底层算法
很多人以为 Git 冲突是把两份文件直接比一比,不一样就报错,这是不对的。Git 用的是三路合并(three-way merge)机制,它对比的是三份内容:
- 两边的目标版本(各自分支的最新内容)
- 它们的共同祖先版本(merge base)
为什么要引入祖先版本?因为只有当“两边相对共同祖先都发生了改动,且改动互相矛盾”时,才算真正冲突。如果只有一边改了,Git 会直接采用改了的那边;如果两边都没改或只有一方没改,它也能自动做选择。理解这一点很重要——很多新手在冲突后惊慌失措,其实许多冲突根本不是真的“矛盾”,只是 Git 的保守策略。
这也是为什么 Git 在合并时经常提示Automatic merge failed; fix conflicts and then commit the result.而不是直接报错崩溃。它已经把大部分工作做完了,剩下的是那些它无法自动决策的部分,需要你来判断。
1.3 冲突类型不完全一样:内容冲突与目录冲突
在实操中,冲突不止一种形态,我整理过一张速查表:
| 冲突类型 | 表现形式 | 典型触发场景 |
|---|---|---|
| 内容冲突 | 文件内出现冲突标记,解决后需要 add + commit | 同一文件的同一区域被两个分支修改 |
| 文件删除/修改冲突 | 一边删了文件,另一边改了文件 | 重构时删除了一个旧模块,另一分支还在维护它 |
| 重命名冲突 | 两个分支把文件重命名成了不同名字 | 不同方对文件结构调整方向不一致 |
| 目录冲突 | 一个分支把目录变成文件,另一分支在目录里加了内容 | 极端情况下结构大变 |
每一种处理方式都略有不同。比如删除/修改冲突,通常要判断这个文件“该不该活”;重命名冲突则需要确认最终采用哪个名字。这也是我后面要单独讲实操的原因——光知道“出现 <<<<<<< 要手动改”是不够的。
2. 冲突标记完全解读:看懂 Git 在人机对话中留下的信息
2.1 冲突标记的构成与含义
当 Git 在文件里留下冲突标记时,它的信息结构是固定的,但很多人只是把看到的内容“删掉多余部分”,却不知道自己为什么要这么删。
<<<<<<< HEAD 这里是你当前分支(HEAD)里的内容 ======= 这里是你正在合并进来的分支的内容 >>>>>>> feature-branch以<<<<<<<开头到=======结束的是当前分支的版本;=======之后到>>>>>>>结束的是外来分支的版本。注意后面跟着的分支名或 commit 编号,它告诉你是“谁带进来的内容”。
很多 GUI 工具(比如 VS Code 的 Git Graph、SourceTree 等)会把这个结构渲染得很友好,但命令行操作时你必须能直接读这四行符号。我在指导 A 同学的团队时发现,他们经常把<<<<<<<和>>>>>>>以外的内容全部“合并”成一段,但保留了一个=======,结果代码直接语法错误。这种问题归根结底是没理解:三个标记行最终都应该被删除,它们只是用于定位的边界。
2.2 识别冲突发生的“位置上下文”
光看懂标记还不够,解读冲突应该带着上下文去看。比如:
<<<<<<< HEAD def calculate_price(price, discount): return price * (1 - discount) ======= def calculate_price(price, discount, tax_rate=0.05): return price * (1 - discount) * (1 + tax_rate) >>>>>>> feature-double-tax这种情况下,两边都对calculate_price函数做了不同的签名变更。直接取舍一方会丢失另一方的重要改动。正确处理方式是第三套方案:手动整合两者,比如综合采用默认税率参数,同时保留原来的单返结构。这正是解决冲突的“价值判断”所在:不是选 A 或选 B,而是理解两边意图后做出最优决策。
2.3 文件状态:冲突时 Git 会告诉你哪些信息
当冲突发生时,很多人只盯着那个报红文件,忽略了git status给出的高价值提示:
Unmerged paths: (use "git add <file>..." to mark resolution) both modified: src/config.jsboth modified表示两边都修改了,是最典型的内容冲突;deleted by us/them则是删除/修改冲突。你还会看到git diff显示的特殊格式——冲突中文件的 diff 会以组合式 diff(combined diff)格式呈现,可能包含复杂的++<<<<<<<前缀。这时候要学会优先看git status,因为它已经帮你把冲突类型做了初步分类。
3. 从零复现一次冲突:三分钟摸清冲突现场
3.1 搭建冲突演示环境
理论说得再多,都不如亲手制造一次冲突来得直观。我通常在讲解时都会带大家做一个最简实验,建议你也跟着敲一遍:
mkdir conflict-demo cd conflict-demo git init git config user.name "Demo" git config user.email "demo@example.com" echo "第一行" > README.md echo "第二行" >> README.md echo "第三行" >> README.md git add README.md git commit -m "initial commit" git checkout -b feature echo "第一行(feature 修改)" > README.md git add README.md git commit -m "feature: 修改第一行" git checkout main echo "第一行(main 修改)" > README.md git add README.md git commit -m "main: 修改第一行"此时两个分支都在第一行做了修改,共同祖先是同一个 initial commit。执行合并:
git merge featureGit 会立刻告诉你CONFLICT (content): Merge conflict in README.md。打开文件,你就能看到上面讲过的四行标记结构。
这也是我强烈推荐的学习方式:用最简化的文件结构去感知冲突的产生,而不是等到真实项目里几百行代码报错时才去现学。真实项目中的冲突往往叠加了多重逻辑,新手很难分辨某个标记究竟是哪次改动引入的。
3.2 冲突解决的第一步:弄清当前状态
在你动手打开编辑器改文件之前,有几个命令值得先跑一遍:
# 查看正在合并中的状态 git status # 查看冲突文件列表(比 status 更简洁) git diff --name-only --diff-filter=U # 显示冲突的具体内容,带上下文 git diffgit diff --name-only --diff-filter=U这个组合我非常依赖,它只列出行合并冲突的文件,不会把正常改动的文件混进来。在几十个文件同时变更的大合并中,这个命令能帮你快速锁定处理范围。
看清当前状态后,再开始打开文件解决。记住:git add是告诉 Git “这个文件我处理完了”的唯一方式。在解决完所有冲突文件之前,你的仓库一直处于 merging 状态,不能提交。
3.3 从简单到复杂的解决策略
对刚入门的团队,我建议按冲突复杂度分层处理:
第一层:单文件单处冲突。直接编辑文件,确认逻辑后删除标记,git add后提交。
第二层:多文件多处冲突,但每处都相对独立。可以逐个文件处理,每处理完一个就git add一下。
第三层:同一个文件里出现多处冲突,且改动之间存在依赖关系。不要急着删标记,先把所有冲突片段通读一遍,理解两个分支在这个文件中各自的“改动意图”,再统筹解决。这有点像拼图:先看全貌,再动手。
我见过不少人在第二层时就已经开 20 个标签页,结果每个文件改到一半又回头改上一个,非常乱。更合理的做法是:先处理共同的模块依赖文件,再处理业务入口文件。依赖问题不解决,业务层怎么改都可能编译不过。
4. 非 merge 场景下的冲突:rebase、cherry-pick 与 stash
4.1 rebase 冲突:交互式变基中的重复冲突
很多人以为git rebase和git merge的冲突解决方式一样,操作起来也差不多,其实差别很大。git merge只产生一次冲突,解决完提交一次;git rebase可能产生多次冲突——每重放一个 commit,如果它和另一个分支有冲突,Git 就会停下来让你处理。
举个具体例子:你在feature分支上有 3 个 commit,准备git rebase main。Git 会把这 3 个 commit 逐一“搬”到main的最新提交上。如果这 3 个 commit 都改到了同一个冲突区域,你可能需要解决 3 次几乎相同的冲突。每次解决后执行的是:
git add 冲突文件 git rebase --continue而不是git commit。因为 rebase 的过程本质上是逐个提交“重现”,--continue会继续下一个提交的重放,并自动使用你修改后的内容生成新的 commit。
这里有个非常容易踩的坑:很多人第一次解决 rebase 冲突时,习惯性打了git commit,结果 Git 提示“error: Committing is not possible because you have unmerged files.”然后整个人懵了。记住这个机制就行:在 rebase 中你不需要手动创建提交,改完文件后git add,然后git rebase --continue,Git 会接管提交过程。
4.2 cherry-pick 和 stash pop 的冲突处理
git cherry-pick本质上是把某一个 commit 的改动“拿来”放到当前分支上。如果这个 commit 改的上下文,和你当前分支中同一位置的代码已经不一样了,冲突一样会爆发。解决方式与 merge 基本一致,但有一点不同:cherry-pick 通常只涉及一个或少数几个 commit,冲突范围一般较小,解决后执行git cherry-pick --continue即可。
git stash pop的冲突则更隐蔽。你在旧分支上 stash 了一堆改动,然后切到新分支,这里代码结构已经变化,git stash pop大概率会报冲突。但 stash pop 的冲突文件不会进入 unmerged 状态,而是作为未暂存的修改出现。也就是说,你只会看到Changes not staged for commit下面有一些冲突标记,没有 “CONFLICT” 大标题。处理方式和之前相同:编辑冲突片段,git add,然后正常提交即可。
4.3 记住不同命令的“代表场景”
我整理了一个口诀式的对照表,方便你遇到不同场景时快速判断该用什么流程:
| 场景 | 冲突解决后执行的命令 | 特点 |
|---|---|---|
| git merge | git add + git commit | 一次性冲突,结束后创建合并提交 |
| git rebase | git add + git rebase --continue | 可能多次冲突,不手动提交 |
| git cherry-pick | git add + git cherry-pick --continue | 单提交重放,冲突范围小 |
| git stash pop | git add + git commit | 冲突文件不会立即出现在 unmerged 列表 |
| git pull | 按 merge 或 rebase 方式处理 | 取决于本地的 pull.rebase 配置 |
理解了这张表,你在各种 Git 操作中遇到 CONFLICT 提示,至少能第一时间知道自己处于什么状态、下一步要做什么。
5. 手把手实操:一次完整的冲突解决过程
5.1 实操项目背景设定
以模拟项目 X 为例,这个项目里有一个config.js文件,用于读取环境配置:
const config = { env: 'development', debug: true, };某个功能分支feature-env修改了env字段并加入了一个apiVersion字段;同时另一个分支hotfix-log修改了debug字段并加了一个logLevel字段。如果从main执行git merge feature-env时又合并了hotfix-log,这个文件很可能会发生冲突。下面我们用命令行完整走一遍流程。
5.2 构建冲突现场(复现用代码)
你可以按下面步骤创建一个可复现的场景:
mkdir project-x cd project-x git init # 生成初始 config.js cat > config.js <<'EOF' const config = { env: 'development', debug: true, }; EOF git add config.js git commit -m "init: 初始配置文件" # 切到 feature-env 分支修改 env/apiVersion git checkout -b feature-env sed -i "s/env: 'development'/env: 'production'/" config.js sed -i "s/debug: true,/debug: true,\n apiVersion: 'v2',/" config.js git add config.js git commit -m "feat: 生产环境配置与 API 版本" # 切回 main,新建 hotfix-log 修改 debug/logLevel git checkout main git checkout -b hotfix-log sed -i "s/debug: true/debug: false/" config.js sed -i "s/debug: false,/debug: false,\n logLevel: 'info',/" config.js git add config.js git commit -m "fix: 关闭调试日志并设置日志级别"接下来模拟集成:先合并一个分支,再合并另一个分支,制造冲突。
git checkout main git merge feature-env git merge hotfix-log第二次 merge 时,config.js大概率会出现both modified的冲突。
5.3 使用 Git 自带的 mergetool 辅助解决
纯手工编辑冲突标记虽然完全可行,但在处理真实项目时效率偏低。我推荐先熟悉 Git 内置的mergetool机制。配置方式很简单:
# 以 vimdiff 为例(不需要额外安装,随 Git 自带) git config --global merge.tool vimdiff运行git mergetool后,Git 会打开一个四窗口对比界面:左上角是当前分支版本(LOCAL),右上角是外部分支版本(REMOTE),中间是共同祖先(BASE),下方是工作区文件中实际带冲突标记的状态(MERGED)。你的目标是让下方窗口变成你想要的最终结果,然后保存退出。
如果你不习惯 vim,也可以用--tool参数指定别的工具:
git mergetool --tool=vimdiff或者配置为 Beyond Compare、Kaleidoscope 等图形工具。我用过很长一段时间 vimdiff,它的优势是随 Git 就装好了,不依赖额外软件;缺点是刚上手时看四窗口布局有点晕。我的建议是,凡是第一次接触 mergetool 的人,先在一个极小文件上练习一遍 vimdiff 的基本操作,不要直接在真实项目里现学。
5.4 手工编辑冲突标记:不依赖 GUI 的基本功
不管用不用 GUI,我都建议所有开发者把手工编辑冲突标记的基本功练熟。这不只是为了应急,而是为了真正理解冲突结构。回到刚才那个冲突现场,假设你直接打开config.js,看到的可能是:
const config = { <<<<<<< HEAD env: 'production', debug: true, apiVersion: 'v2', ======= env: 'development', debug: false, logLevel: 'info', >>>>>>> hotfix-log };两边都改了env和debug,还各自新增了一个字段。这里正确的取舍是:env应该取production,debug应该取false,同时把两个新增字段都保留。最终文件应该写成:
const config = { env: 'production', debug: false, apiVersion: 'v2', logLevel: 'info', };编辑完成后删除残留的冲突标记,然后执行:
git add config.js git commit -m "merge: 合并 feature-env 与 hotfix-log 配置调整"到这里,整个 merge 冲突的解决过程就完成了。
6. 工具链选择:命令行和 GUI 怎么搭配最顺手?
6.1 命令行的优势与边界
命令行解决冲突的优势是可控、透明。你用git status、git diff、git show可以精确知道每个文件的冲突内容和来源。劣势也很明显:需要你手工编辑冲突标记,在大文件里找<<<<<<<比较费眼神,特别是三四处冲突挤在一起时会眼花缭乱。
提升命令行效率的几个实用技巧:
# 搜索所有冲突文件中的标记行 grep -rn "^<<<<<<< HEAD" --include="*.js" . # 只查看第一个冲突文件的短状态 git diff --name-only --diff-filter=U6.2 适合不同阶段的 GUI 工具
如果你所在团队已经在用 VS Code,那我建议直接用它的内置合并冲突编辑器。它会把<<<<<<< / ======= / >>>>>>>渲染成彩色块,还提供 “Accept Current / Accept Incoming / Accept Both” 按钮。这个界面对于刚接触冲突的新手特别友好,因为不需要记忆标记语法。
SourceTree、Fork、GitKraken 的冲突界面也各有特色。我的偏好是:小冲突用命令行直接改,大冲突(多文件、交叉修改)用 mergetool 或 GUI 辅助。因为多文件冲突时,GUI 能让你同时看到冲突位置和文件树,不容易漏。
不过这里要特别提醒:GUI 工具的“Accept Both”不一定等于“正确合并”。它通常只是把两边内容按顺序拼接,不代表逻辑上没问题。尤其是函数、语义相关的代码,需要你肉眼审视代码逻辑是否还能编译、运行。
6.3 我推荐的“准 GUI 工作流”
有人觉得,GUI 工具显得不够“硬核”,命令行才是真本事,其实完全没必要对立。我的日常工作是:
- 用
git status和git diff --name-only --diff-filter=U确定哪些文件冲突; - 打开冲突文件,在编辑器里先通读整个文件,标出每处冲突的大致位置;
- 对逻辑简单、重复性高的冲突直接手工处理;对复杂交集,用编辑器自带的 diff 视图或 mergetool;
- 每解决完一个文件立即
git add; - 最后跑一次编译或测试,确认整体逻辑无碍再提交。
这套流程的核心是:不要把“工具”当目的,工具只是帮你快速理解冲突上下文的助手,真正的决策者始终是你。
7. 实战中的踩坑记录:冲突处理翻车实录
7.1 “冲突竟然不冲突”:自动合并成功但逻辑崩了
有次我合并两个功能分支时,Git 没有提示任何冲突,git merge顺利完成。结果跑测试才发现,两个分支同时给同一个请求对象加了不同的默认字段,虽然改的是不同位置,但合并后的请求数据在特定业务场景下完全错乱。
这类问题比显式冲突更难排查,因为 Git 告诉你“没问题”,但实际逻辑上出现了隐性问题。我后来的补救方案是:在合并重要分支前,先做一次小范围集成测试,特别关注文件行尾、边界处理、默认值相关的改动。Git 解决的是“文本冲突”,不负责“逻辑冲突”,这就是它永远无法替代代码审查的原因。
7.2 合并到一半发现方向错了:如何干净退出
有时候你合并到一半,发现整个人已经不在状态,或者深思后发现这个合并策略根本不对。此时需要回滚到合并前的干净状态。不要硬着头皮继续,干净的退出方式:
# 如果是 merge 进行中,先取消合并 git merge --abort # 如果是 rebase 进行中 git rebase --abort # 如果是 cherry-pick 进行中 git cherry-pick --abort这些命令都会把工作区还原到操作开始前的状态。注意它们不会删除你已经完成的其他未提交改动,只是放弃当前这个合并操作。所以我会在合并前确保工作区干净,至少把未提交的改动先 stash 或 commit,避免意外丢失。
7.3 pull 时的重复冲突:配置与境外合作场景无关,但与习惯有关
如果你的git pull默认触发 merge,但在团队中又强制git pull --rebase,那么你会在每次同步时反复解决同一个区域的冲突。这通常不是 Git 的问题,而是团队没有约定固定策略。比如某成员把pull.rebase设为 true,另一成员用默认 merge,两个人反复来回同步时,就会出现一方解决完,另一方又被“新冲突”缠住。
我的经验是:团队必须统一“拉取策略”。如果团队流程简单,就统一用git config pull.rebase false保持默认 merge;如果追求线性历史,就统一设为 true。不要一会 merge 一会 rebase,不然你会在重复解决冲突上浪费大量时间。
7.4 冲突标记没删干净:比冲突本身更常见的失败
这是我在代码评审中最常发现的问题。看起来所有冲突都处理完了,提交也成功了,但合并里残留了<<<<<<<标记。代码虽然能过 lint,同事却会在接下来的合并中第二次甚至第三次看到你的残留标记。
一个简单的自检命令:
grep -rn "<<<<<<< HEAD" --include="*.js" .提交前先全局搜一遍。这个习惯非常小,但能避免大量低级错误。
8. 大型合并中的冲突治理策略:不止是技术问题
8.1 冲突是“信号”不是“问题”
在几十人协作的仓库里,频繁出现冲突往往说明模块划分有问题。如果某个文件总是被多个分支同时修改,那么很有可能是这个文件的职责边界不够清晰。我见到过最典型的是config类文件——几乎每个功能分支都会往里面加东西,结果全局冲突一触即发。
这种情况下,真正的解决方式不是锻炼“手速”,而是重构:把配置按模块拆分,各模块维护自己的配置文件,再通过聚合入口读取。或者调整团队的合并节奏,让分支生命周期短一点,减少同时存在的长期分支。冲突多到影响交付时,它们就不再是“你不会操作”的技术问题,而是工程结构和协作分工的信号。
8.2 分支粒度与合并频率的管理建议
我在实际项目中比较看好的做法是:
- 任务分支尽量从最新
main切出,且生命周期控制在 1~2 天内; - 每天至少同步一次上游变更,确保差异空间不大;
- 每个任务分支只负责一个独立模块的改动,避免“一分支到底”的大规模重构;
- 大型重构必须提前在团队内对齐,并拆分成可独立合并的小步提交。
这不是严格的“敏捷教条”,而是基于冲突产生的本质推导出来的——差异越小,自动合并成功率越高;越早同步,隐藏的冲突越早暴露。
8.3 规范化 Commit Message 对冲突排查的间接帮助
很多人不重视提交信息,但我在定位冲突来源时,经常依赖提交历史:
git log --oneline --decorate如果每次提交写得清楚,比如 “feat: 增加视频上传接口”“fix: 修复视频播放终止条件”,那么当你看到冲突文件里两边的版本时,能迅速判断出哪个 commit 引入了哪段逻辑。反之,如果提交信息全是 “update”“fix bug”,你只能从代码主体反推意图,效率差距非常大。
这也算是我实测下来最被低估的“冲突解决加速器”之一。
9. 常见问题与速查手册
9.1 冲突解决时最常见的 8 个问题
| 问题现象 | 可能原因 | 快速处理 |
|---|---|---|
| 打开文件找不到冲突标记 | 可能是二进制文件冲突 | 检查git status中的提示信息 |
| 解决了所有冲突,但还提示 unmerged | 漏了某个文件没git add | 运行git status逐个核对 |
| rebase 时手滑提交了 commit | 不熟悉 rebase 流程 | 使用git rebase --abort重新开始 |
| merge 完成但测试挂了 | 逻辑冲突而非文本冲突 | 检查依赖关系,跑单测,而不是只看 diff |
| 冲突标记携带大量无关改动 | 两个分支在某区域改动重叠 | 用对比工具分别查看 BASE、LOCAL、REMOTE |
| pull 时反复出现相同冲突 | 分支间未及时同步 | 尽快同步主线,减少分支存活时间 |
| 冲突文件太多不知道从哪着手 | 缺少优先级划分 | 按依赖关系先处理公共模块 |
| 文件被误认为 “both deleted” | 策略冲突导致 | 查看未合并路径提示,确认文件是否应保留 |
9.2 我最推荐的五个“保命”命令
| 命令 | 适用时机 | 作用 |
|---|---|---|
| git status | 冲突后第一件事 | 查看冲突文件清单与剩余步骤 |
| git diff --name-only --diff-filter=U | 冲突范围较大时 | 快速列出所有冲突文件 |
| git merge --abort | 方向错了或不想要这次合并 | 干净退出到合并前 |
| git log --oneline --decorate --graph | 需要理解分支结构 | 看清分叉与合并历史 |
| git show : | 需要查看某个历史版本的文件内容 | 定位某次改动来源 |
9.3 合并前预防冲突的三个动作
再分享三个主动预防冲突的实操动作,都是我实践后觉得非常有效的:
第一,开始大型合并前,先git fetch并确认主线是否已更新,必要时先合并一次主干以减小差异面积;
第二,处理冲突前先本地git stash掉未提交的临时修改,避免冲突解决过程中和第二批未提交内容混在一起;
第三,冲突解决完毕后,不要立刻提交,先执行一次项目构建或相关单测,确认代码逻辑没问题再提交。
这三个动作的成本都很低,但能把“处理冲突”和“部署正常”之间的衔接做得很顺滑。我曾经因为偷懒直接提交后 CI 跑挂,最后排查出来的问题其实就是某个冲突片段里逻辑顺序错了,非常尴尬。
10. 从我个人的实操经验谈几点心得
如果你完整走完上面那些步骤,解决普通冲突已经没什么问题了。但我想在最后补充三个从更高视角得出的体会。
第一,冲突不是“敌人”,它是代码库的声音。当它大声喊叫时,说明有两个真实修改在同一处交汇,而你此刻是唯一能判断双方真实意图的人。这个判断力只能靠在真实项目中反复训练,没有捷径。
第二,团队协作的铁律是“小步快跑”。我见过最顺利的集成流程,恰恰是那些每天同步、每周合并的团队。他们的冲突虽然还是有,但永远在那个可控范围内,解决起来甚至不需要加班。相比之下,憋了两周才合并的大仓库,光是一次冲突就能让一两个人焦头烂额半天。
第三,趁早把mergetool的配置和习惯养起来。我到现在仍然会在大型合并中借助 mergetool 的四项对比视图,它能让我一眼看到 BASE、LOCAL、REMOTE 三方差异,而不是手工在两个文件间来回比对。磨刀不误砍柴工,花一个小时熟悉工具,之后省下的时间远超投入。
如果你看完这篇文章,我建议你立刻做两件事:先打开一个实验仓库,用文中的最小示例制造一次冲突,从头到尾过一遍完整流程;再打开你手头最常冲突的那个项目的文件结构,思考它是不是因为职责太集中才总让你头疼。做完这两步,你对 Git 冲突的理解会上升一个明显的层次。