前面五篇把 Git 的基础命令、暂存区、提交、远程仓库、分支管理都盘了一遍,分支怎么建、怎么切、怎么删,大家应该都练过几轮了。但很多同学学到这一步,心里其实一直悬着一个大问题:分支是能随便开,可最后怎么合回去?合并的时候突然跳出一堆 conflict,密密麻麻的红字,该怎么处理?今天这篇就专门把这个坎儿迈过去。
我会从合并的底层逻辑讲起,再走一遍完整的合并流程,最后把常见的几类冲突逐一拆开,给出一套能直接抄作业的解决思路。不管是个人项目开发,还是团队里走 GitLab / GitHub 的 Merge Request 流程,这篇文章都能用上。内容不绕弯子,按顺序往下看就行。
1. 合并之前,先把三件事搞清楚
1.1 合并的本质是什么
在动手敲git merge之前,建议你先把“合并”这两个字到底在做什么想明白。Git 里的分支,本质上只是一个个提交对象组成的有向无环图上的游标。所谓合并,就是把两个分支各自“走岔”的提交历史重新捏到一起,让它们在逻辑上成为一个新的整体。
举个例子:你从master拉出feature-login分支做登录功能,与此同时master上别人又提交了一个修复 bug 的改动。这时候两条分支的提交历史就分叉了。合并的动作,就是要把feature-login上新增的提交,和master上新增的提交都保留下来,最终汇成一个包含双方改动的结果。
理解了这一点,你就能明白为什么合并有时候是件简单事,有时候却变成一场灾难。简单的时候,是因为双方改动的区域根本不重叠;灾难的时候,是因为大家都在同一块地盘上动土,Git 没法替你拍板。
1.2 值班两种人:直接登陆的 Fast-forward 合并与三方合并
很多教程上来就讲命令,但我觉得先把两两种合并机制分清楚更重要,否则即使执行git merge成功了,你都不知道自己刚才经历了什么。
第一种叫 fast-forward,也就是快进合并。它的触发条件很苛刻:当前分支从某个点切出去之后,这个“根”分支自己一直没有产生新的提交。这时候 Git 不需要真正“合并”任何内容,只需要把当前分支的指针直接挪到目标分支的最新位置,就像顺着一条直路往前走。比如你从master拉出feature分支,做了三个提交,期间master纹丝不动,那么把feature合回master时,Git 就会执行一次 fast-forward。结果就是提交历史变成了一条直线,看起来非常清爽。
第二种叫三方合并(three-way merge),这才是合并真正复杂的地方。它的触发条件是两条分支自从分叉之后都有新的提交。此时 Git 会寻找三个对象:共同祖先(merge base)、当前分支的 HEAD、以及你要合并进来的那个分支的最新提交。Git 分别拿着这三份内容做比较,尽量把双方各自的新改动融合成一份结果。
共同祖先(base)---- 当前分支改动(HEAD) \ / ---- 目标分支改动(feature)三方合并既是 Git 最强大的地方,也是各种冲突的来源。理解它,你才能在被冲突折磨的时候知道 Git 到底在纠结什么。
1.3 合并前必须做的两个检查
实操之前,有两个习惯我强烈建议你先养成。第一,确认你当前工作区是干净的。git status输出 “nothing to commit, working tree clean” 再合并,否则会把未提交的改动和合并过程搅在一起,一旦冲突出现,现场会非常难看。第二,确认你要合到哪个分支上。很多人习惯站在feature分支上执行git merge master,结果把 master 的改动合到了 feature 分支里,这本身没有错,但如果本意是“发布 feature 到 master”,方向就反了。
我见过不少刚入门的朋友在这个地方翻车,原因就是没有先切到目标分支。所以记住这个口诀:先切到接收方,再执行合并命令。
提示:执行
git merge之前,先git status看一眼工作区,再git log --oneline --graph --all看一眼分支走向,双重确认再动手。
2. 合并实操:从本地到远程的完整流程
2.1 本地分支合并的标准三步走
先给出一套最稳妥的本地合并流程,适合大多数场景。
第一步,切到接收方分支。假设你要把feature-login合到master,那就先执行:
git checkout master git pull origin master这里先pull是为了让本地master和远程保持同步,避免拿一个过时的 master 去合并。很多冲突其实是“本地 master 已经落后了”造成的,这一步能挡掉不少无谓的 pain。
第二步,执行合并:
git merge feature-login如果一切顺利,你会看到一条消息,提示这次 merge 是 fast-forward 还是 “Merge made by the 'recursive' strategy”,后面跟着被改动的文件列表。
第三步,查看合并结果并提交(如果需要的话)。Git 在执行git merge的时候,会自动生成一个 merge commit,前提是这次合并不是 fast-forward。也就是说,正常情况下你不需要手动git commit,除非你主动加了--no-commit参数。
合并完用git log --oneline --graph看一下,如果看到分叉的两条线汇聚到一个点,那就说明一次标准的三方合并完成了。
2.2 怎么判断合并用了哪种方式
我见过不少人在合并之后一头雾水:同样是git merge,为什么有时候历史是直的,有时候会多出一个“Merge branch”的提交?
判断方法很简单:git log --oneline --graph展示出来,如果提交历史是一条直线,没有出现分叉交汇的节点,那刚才就是 fast-forward。如果出现了一个带两个父提交的 merge commit,在 graph 图上表现为两个分支线汇聚到一点,那就是普通的三方合并。
如果你希望强制走三方合并、哪怕实际上可以 fast-forward,也就是为了在历史上保留一个“我合过一次”的提交节点,可以在合并时加--no-ff:
git merge --no-ff feature-login这个参数在团队合作里用得比较多。因为有些团队喜欢用 merge commit 作为“一次功能合并”的标记,方便以后通过git log --graph快速回溯“这个功能是什么时候合进主干的”。
与之相反,如果你完全不希望保留 feature 分支的细碎提交历史,只想把最终结果作为一个提交合进来,那就用--squash:
git merge --squash feature-login git commit -m "feat: 登录功能"使用--squash之后,Git 会把 feature 分支上的所有改动压缩成一个“待提交状态”,你需要手动执行一次 commit。这种方式的代价是会丢失原分支上每个提交的独立意义,适合功能比较简单、一次提交就能说清楚的情况。
2.3 远程分支与 GitLab / GitHub 的合并流程
除了本地命令行,现在很多团队用的是 GitLab 或 GitHub 上的 Merge Request(MR) / Pull Request(PR) 流程。这个流程的本质还是在远程仓库那边执行一次合并操作,但多了一层“人工审核”的门槛。
在 GitLab 上,你通常是把feature分支推到远程,然后发起一个 MR 请求把feature合并到master。如果代码没有冲突,页面上会直接出现 “Merge” 按钮。如果出现冲突,GitLab 会提示 “Cannot be merged”,需要你手动处理。处理方式有两种:一种是在页面上用 “Resolve conflicts” 在线解决简单冲突,另一种更稳妥的做法是在本地把master拉回feature分支,先解决冲突后再推上来更新 MR。
具体到本地操作,一般是:
git checkout feature-login git fetch origin git merge origin/master这一条命令是真正的“协同利器”。它的意思是把远程 master 的最新改动合并到当前功能分支里,提前把冲突在功能分支上解决掉,而不是等到 MR 合并的时候再抓瞎。功能分支自己更新得越勤快,最后合并回 master 的时候就越顺畅。
注意:如果在本地直接合并 origin/master 后发现冲突很多,不要急着强推。先用
git status看清楚,解决了再git push更新功能分支,否则会把冲突标记原样推上去,队友拉下来全是<<<<<<<符号,非常坑。
2.4 合并到哪里最容易出问题:目录结构变动
很多人忽略了另一种“大合并”:当功能分支改动了大量目录结构,比如把某个模块整体移动位置,而 master 分支上其他人又在这个模块里新增了文件,合并时 Git 的 rename detection(重命名检测)不一定能百分之百识破你的意图。这时候常见的表现是:新文件没有被识别为移动后的文件,而是显示为删掉旧文件、新增一份一模一样的文件。
遇到这种情况,不要急着手动改文件。可以先强制让 Git 做一次 rename 检测:
git merge master -X rename-threshold=30这个参数的意思是当两个文件相似度超过 30% 时,就认为是“同一个文件被重命名了”。阈值设低一点,Git 更容易识别出改名后的对应关系,从而把改动更正确地合并过去。不过这个参数属于进阶玩法,遇到目录大变动的时候再用,平时保持默认即可。
3. 合并冲突是怎么产生的
3.1 冲突的本质:Git 没有上帝视角
说了这么多,终于到了正题:冲突。首先给个结论:冲突不可怕,它是三方合并机制正常工作的一种信号。Git 不是不让你合并,而是告诉你:“两边的改动我都看到了,但它们放在同一个位置,我决定不了以哪边为准,你来看看。”
冲突的前提是两条分支都修改了同一个文件的同一个区域。Git 拿共同祖先、当前分支和目标分支三份版本做 diff,如果发现当前分支和目标分支对同一行做了不同的修改,它就判断为冲突。这时候它会把自己能合的部分都合好,只把拿不准的部分用冲突标记标出来,等你人工裁决。
打个比方:你和同事同时在改一份合同。你想把第 3 条改成“付款期限 30 天”,他想把同一家改成“付款期限 60 天”。两个人改的是同一句话,系统不知道该听谁的,只能把两种版本都摆出来,让你俩商量。代码里的冲突就是这么回事。
3.2 冲突标记逐行解读
当你触发了冲突,打开冲突文件,会看到类似这样的内容:
<<<<<<< HEAD 当前分支的代码 function login() { return 'login from master'; } ======= 功能分支的代码 function login() { return 'login from feature'; } >>>>>>> feature-login这里每一段符号都有明确含义:
<<<<<<< HEAD:冲突块开始,下面到=======之前的内容,是当前分支(HEAD)里的版本。=======:分界线,把它上下两部分隔开。>>>>>>> feature-login:冲突块结束,上面到=======之后的内容,是你要合并进来的那个分支(这里是 feature-login)的版本。
如果你用的是 VSCode 这类编辑器,冲突区域会有明显的红绿色背景,并且提供 “Accept Current Change”、“Accept Incoming Change”、“Accept Both Changes” 等按钮,这就是把上面的标记转换成了可视化操作。
但这里我要特别提醒一句:讨论冲突时,我们常把 HEAD 叫“当前版本”,把feature-login叫“对方版本”,这只是从“谁在合并谁”的角度说的。并不代表 HEAD 版本天生优先级更高。最终保留谁的代码,完全取决于代码逻辑,而不是谁站在哪一边。
3.3 一个容易忽略的细节:diff3 风格
有时候默认的冲突标记让你非常困惑:你既想知道两边各自改了什么,更想知道改之前原来是长什么样的。默认merge.conflictStyle是merge,只会显示两边版本。但如果把配置改成diff3:
git config --global merge.conflictstyle diff3冲突块会变成三段,多了一个“合并前的基础版本”:
<<<<<<< HEAD 当前分支的代码 ||||||| 1a2b3c4 基础版本(共同祖先) ======= 功能分支的代码 >>>>>>> feature-login这个|||||||段就是手工解决冲突时最重要的参考。因为它能帮你判断:两边到底是谁改了?谁没动?比如基础版本是count = 0;,当前分支把它改成count = 10;,功能分支也把它改成count = 15;,那你就能很清楚地看到两边都有意修改,剩下的问题就是选 10 还是 15,或者结合上下文取一个新值。
我自己的经验是:遇到复杂冲突(超过一个代码块那种),先切到 diff3 风格再处理,思路会清晰一大截。
3.4 Git 不报冲突但你可能还是想干预的几种情况
还有一种隐蔽情况:Git 觉得能自动合并,不报任何冲突,但真正的“逻辑冲突”其实已经发生了。比如两个人同时改了一个上下文切换的变量名,一个人把user改成了account,另一个人在他没读到的地方也写了user。三方合并按行匹配,可能把变量名合并出一种“一半新一半旧”的诡异状态,代码跑起来直接报错。
这种情况 Git 帮不了你,只能靠测试和人工 review 去揪出来。所以合并完,尤其是多人协作的合并,跑一遍测试用例,或者至少编译一下,是必须的动作,不要以为没有冲突就万事大吉了。
4. 六类合并冲突的具体表现与解决思路
下面我把日常开发里见到频率最高的冲突类型逐一拆开,每类都给明确的操作思路。为了好记,我用一个表格先把各类冲突的特征和解决要点列出来,再逐个展开。
| 冲突类型 | 特征 | 优先解决思路 |
|---|---|---|
| 同一文件不同位置被修改 | 本地混入双方改动后仍可合 | 手动合并,必要时保留两侧内容 |
| 同一代码块被修改 | 同一函数/语句两边逻辑不同 | 对比共同祖先,人工决定最终逻辑 |
| 一边修改一边删除 | 一方删了文件,另一方改了内容 | 确认该文件是否还需要存在,再决定保留删除还是内容 |
| 文件重命名 | 一方移动文件,另一方改了文件内容 | 用 rename 检测降低阈值,手动确认对应关系 |
| 二进制文件 | 图片、Excel 等无法按行合并 | 只能二选一,或用专门工具重新生成 |
| 空白字符差异 | 缩进、换行符不同导致大面积伪冲突 | 开启忽略空白选项,统一团队格式化规则 |
4.1 同一文件不同位置修改:最友好的冲突
这类冲突的特征是两边都动了同一个文件,但改的位置不重叠。多数情况下 Git 能自动合并,只有在边界线上紧挨着的改动才会被标记为冲突。比如一个函数库里,当前分支在上面加了一个函数,功能分支在下面加了一个函数,Git 通常自己就能合好。如果恰好一个把位置标在文件末尾,另一个也在文件末尾,才会出现冲突。
解决思路很简单:冲突标记里的两段内容很可能可以同时保留。你只需要确认上下文的顺序没有错乱,把两个版本的代码都留下即可。编辑器里选择 “Accept Both Changes” 通常就是正确答案,然后删除多余的冲突标记。
4.2 同一代码块被修改:最常见的硬仗
这是最常遇到的场景。两个分支都对同一个函数、同一个表达式做了实质性修改。这时候别上来就想着“选一边”,先停下来看共同祖先版本,搞清楚各方改动的目的,再决定是选一边、还是把两边的意图揉成一个新版本。
假设基础版本是:
function getDiscount(userType) { if (userType === 'vip') { return 0.8; } return 1; }当前分支把它改成了vip打 7 折,功能分支加入了new_user打 9 折的逻辑。冲突标记里两段都是合理的改动,正确解绝不是二选一,而是把两段合并成:
function getDiscount(userType) { if (userType === 'vip') { return 0.7; } if (userType === 'new_user') { return 0.9; } return 1; }这种“第三方方案”只靠冲突标记本身无法自动完成,需要你理解两边的业务逻辑。所以解决冲突时,条件允许的话,最好把改动的队友叫过来问一句,比你自己猜要快得多。
4.3 一边修改一边删除:文件到底还要不要
这种冲突有一个典型识别标志:git status里显示的是deleted by them或deleted by us,而不是both modified。
场景重现:你在功能分支上大改特改文件config.js,另一个分支上同事觉得这个文件没用就直接删了。合并时 Git 骑虎难下:按你的版本,文件应该保留;按对方的版本,文件应该是消失的。
解决前先问自己一个问题:这个文件现在还需要吗?
- 如果确定要保留,就用
git add config.js把这个文件从冲突标记里“解救”回来,或者执行:
git add config.js- 如果确定要删除,执行:
git rm config.js然后在后续的git merge --continue中提交这个决定。这类冲突最忌讳的是犹豫不决,删除保留二选一,没有中间态,赶紧拿主意就对了。
4.4 文件重命名与合并的组合拳
重名合并冲突比较隐蔽,且容易被当成“新文件冲突”忽略掉。当功能分支把report.js重命名为annual-report.js,而 master 分支上有人又改了几行report.js的内容,Git 的 rename detection 可能成功识别“这是同一个文件”,然后把改动合过去。但如果相似度不高,Git 就不会把它当作重命名,于是出现新文件annual-report.js和改动过的旧文件report.js同时存在于工作区的结果。
处理方式:
git merge master -X rename-threshold=30刚才提过这个参数,这里是它最实用的场景。如果这样处理后,Git 仍然没有正确配对,那就只能手动把旧文件的内容合并到新文件里,然后显式删除旧文件:
# 手动把旧文件内容合并到新文件后 git rm report.js git add annual-report.js这招别随便用,只有在目录重构后出现大量错误配对时才值得手动干预。
4.5 二进制文件冲突:不靠编辑器,靠人决策
二进制文件,比如图片、Excel、Word 文档、编译产物,合并时的表现和源码完全不同。因为 Git 不能逐行比对,它只会说“两边都动了这个文件,我无从下手”。你打开文件看到的只会是二进制内容,全是乱码,没法像文本一样手动拉取两段。
面对二进制冲突,现实一点的方案有三种:
- 选其中一方,放弃另一方:
git checkout --ours -- path/to/file或git checkout --theirs -- path/to/file,然后git add标记解决。 - 重新生成:如果这是一个由代码生成的报告或图片,最快的办法是修复生成源,然后重新导出覆盖。
- 用支持二进制合并的专业工具:比如某些设计工具的 .psd 文件插件、Excel 的联机合并,这类工具能做到“基于两个版本合并”而不是“二选一”。
对于团队里频繁出现的二进制文件,我的建议是:图纸、设计稿最好别直接塞 Git,而是用专门的文件管理平台,代码库里只保留引用或说明。非要入库的话,也要约定好:一次只让一个人改,改完立刻合并分发,避免多人同时操作。
4.6 空白字符引发的“伪冲突”
这种冲突最气人,因为它不是真正的逻辑冲突。A 同事用了 4 个空格缩进,B 同事的编辑器配置了 Tab 缩进,一合并整个文件到处飘出冲突标记,代码逻辑其实一点都没冲突。
对付这种情况,Git 提供了空白处理参数:
git merge feature -Xignore-space-change-Xignore-space-change会忽略行末空白变化,-Xignore-space-at-eol只忽略行尾空白。但要注意,这些参数都是临时策略,它们把“空白差异”这个信息扔掉了,如果某次修改真的只改了空白,你用了这个参数后合并结果里可能连这个修改都看不到。
根治办法是团队内统一编辑器配置,提交一份.editorconfig文件放到仓库根目录,再配合项目里的代码格式化工具(如 Prettier、ESLint 等),让所有人的换行符、缩进风格一致。这个坑,早统一早解脱。
5. 冲突实战拆解:从开始合并到提交完成
5.1 模拟一次完整的冲突现场
步骤有点多,我从零开始模拟,大家可以直接照抄着练一遍。
假设当前在master分支上,仓库结构如下:
# 创建一个演示文件 echo "第一版内容" > demo.md git add demo.md git commit -m "init demo" # 创建功能分支 git checkout -b feature-modify # 在功能分支上修改文件 echo "功能分支修改" >> demo.md git commit -am "feat: 功能分支的改动" # 切回 master,也修改同一个位置 git checkout master echo "主分支修改" >> demo.md git commit -am "master 的改动" # 执行合并 git merge feature-modify执行到git merge feature-modify,大概率会出现:
Auto-merging demo.md CONFLICT (content): Merge conflict in demo.md Automatic merge failed; fix conflicts and then commit the result.看到CONFLICT (content)这一行,就说明冲突了。此时git status会给出类似:
Unmerged paths: (use "git add <file>..." to mark resolution) both modified: demo.md5.2 手工解决冲突的完整操作
打开demo.md,你会看到类似这样的内容:
第一版内容 <<<<<<< HEAD 主分支修改 ======= 功能分支修改 >>>>>>> feature-modify这里我们假设两边的改动其实都有意义,决定同时保留,于是把文件手动改成:
第一版内容 主分支修改 功能分支修改然后把增量标记解决掉,告诉 Git 这个冲突已经处理完毕:
git add demo.mdgit add在这里的作用就是标记“冲突已解决”,所以可以不急着 commit。接着执行:
git merge --continue这时 Git 会打开编辑器让你写 merge commit 的提交信息,默认已经写好了,直接保存退出即可。至此,一次从冲突出现到解决的过程就完成了。
注意:不要用
git commit直接提交,虽然效果上差不多,但git merge --continue会按正规矩拼接 merge commit 的信息,不会漏掉 MERGE 状态,建议用它。
5.3 使用图形化工具提高效率
如果团队里冲突比较多,纯靠手写冲突标记效率太低。可以考虑配置外部合并工具。命令行里最常用的是vimdiff,不过对新手不友好。我推荐用 VSCode 或 Beyond Compare。
VSCode 本身自带冲突可视化界面。开启红色高亮后,可以直接点击按钮选取当前改动、引入改动或同时保留。修改完保存,去终端执行git add就行。
如果要配置 Beyond Compare 这类外部工具,一条命令就能设置:
git config --global merge.tool bc3 git config --global mergetool.bc3.path "/usr/bin/bcompare"合并时遇到冲突,直接执行:
git mergetoolGit 会逐个打开冲突文件,让你在外部工具中完成三栏合并(左边当前分支、右边目标分支、中间共同祖先)。这个三栏视图对复杂冲突尤其友好,可以直观看到两边各自改了什么、改之前什么样。
6. 合并到一半,想反悔的保命操作
6.1 放弃合并:冲突还没解决时
合并过程中发现冲突太多,或者发现自己合错分支了,第一反应不要是手动删文件,而是先撤:
git merge --abort这条命令会完全放弃本次合并,把工作区恢复到执行git merge之前的状态,所有合并产生的未提交改动都会被丢弃。前提是合并过程中你没有手动git add并git commit。如果你已经提交了 merge commit,那就不是abort能解决的了,得用下面的方式。
6.2 回退误合并:已经生成 merge commit 之后
分两种情况。
第一种情况,merge commit 还只在本地,没有被推送到远程。这时最简单粗暴的方式是:
git reset --hard ORIG_HEADORIG_HEAD是 Git 在执行合并等危险操作前自动记录的“原来位置”。执行这条命令,当前分支会跳回到合并之前的位置,合并产生的提交全部消失。注意--hard会把工作区的未提交修改也一并丢弃,执行前确认没有要保留的东西。
第二种情况,merge commit 已经被推送到远程,别人可能已经拉走了。千万别用reset强推覆盖,因为这会改写公开历史,队友下次 pull 会非常痛苦。正确做法是用 revert 生成一个反向提交:
git revert -m 1 <merge-commit的哈希值>-m 1表示保留 merge commit 的第一个父提交(也就是合并前的原分支),把第二个父提交(被合并进来的那个分支)的影响回退掉。这样本地会生成一个新的提交,这个提交能把之前合并的内容全部撤销,而且不破坏历史。
6.3 用 reflog 找回丢失的提交
如果你不小心执行了git reset --hard,又发现合错了的其实是可以用的东西,那就该git reflog出场了。它记录着所有分支指针移动的历史,包括你刚才 reset 掉的那次合并。
git reflog输出会列出当前的提交 SHA、执行过的操作及说明。找到合并产生的那条记录,用git cherry-pick或git branch恢复即可。reflog 是 Git 最容易被忽视的保命技能,建议每个人都练一遍。
7. 团队协作中,如何从源头上减少冲突
7.1 小步提交,高频同步
冲突的产生有一个铁律:两条分支分叉越久,合并的代价越大。如果你拉了一条功能分支,一埋头写了三个星期不更新,等回头合并的时候,基本就是一场灾难。正确做法是功能分支每次有阶段性的完成品,就执行:
git fetch origin git merge origin/master高频同步的好处是,你会发现大多数冲突都是小冲突,因为两边的改动间隔很短,共同祖先离得很近,Git 自动合并的成功率非常高。
7.2 功能分支保持“短命”
理想的功能分支应该在一两天内合回主干。一个功能分支存活的时间越长,和别人发生交集的可能性就越大。如果任务太大,不要局限于一个分支,可以拆成多个小步骤,每个步骤都合回主干一次,做到“小步快跑”。这不仅减少冲突,review 起来也轻松。
7.3 用 MR / PR 流程给自动合并加一道保险
GitLab 和 GitHub 的 MR/PR 流程虽然有“人工审核”这一步,但它的另一个好处是可以让你看到冲突是否出现。如果 MR 页面提示 “Cannot merge automatically”,不要慌,在本地先处理冲突再 push 更新,MR 就能重新变绿。这一套流程本身就是倒逼大家保持高频同步的机制。
另外,很多团队会在 MR 里配置 CI,代码合并前自动跑测试。这一步是压死“无声逻辑冲突”的最后一根稻草,推荐有条件的一定要加上。
7.4 统一格式化和编码规范
在 4.6 里我提过.editorconfig和格式化工具,这里再强调一遍:团队仓库根目录放一份.editorconfig,统一缩进、换行符和末尾换行规则。对于前端项目,强制用 Prettier 之类的工具,提交前执行格式化。只要大家习惯一致,因为空白差异引发的伪冲突能减少八成以上。
7.5 合理划分模块,减少“共用文件”
从软件工程的角度看,冲突的根源是两个人在同一时间碰了同一个文件。如果模块划分得足够合理,每个人负责的目录尽量不重叠,冲突自然无从谈起。但这属于架构层面的事,一时半会改不动也不用急,先养成“改文件前看一眼谁最近也在动这个文件”的习惯,及时沟通,比事后解冲突省力得多。
最后再分享一个我自己的习惯:解决冲突时,我永远不会先看冲突标记里的“当前版本”和“对方版本”谁对谁错,而是先看共同祖先那一版。几乎所有复杂的冲突,答案都在那一版里。想清楚改动意图之后,再去决定是单边保留还是两边揉合,基本就不会处理出逻辑上的问题。这个思路对新手尤其有效,希望大家也能在自己的项目里试试看。