ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Git合并冲突完全指南:从冲突标记到解决方案

Git合并冲突完全指南:从冲突标记到解决方案 很多人第一次在终端里看到那串 HEAD时第一反应都是完了代码被我弄坏了。上周我还在给一位新人做代码 review他憋了半天才开口“哥我 git pull 了一下然后登录页的代码就变成了一堆尖括号我不敢动也不知道怎么把它变回去。”他看到的东西就是这期真正的主角——合并冲突merge conflict。我的第一反应是恭喜你终于遇到合并冲突了。这不是 Git 在惩罚你而是它在保护你的代码。与其说崩溃不如说这是每一个用 Git 干活的人迟早都要补的一课。这篇文章不讲玄学只讲三件事冲突标记到底是什么意思、冲突是怎么产生的、以及从头到尾怎么把冲突干净利落地解决掉顺手把那些让人崩溃的坑也排一遍。文章里的命令在 Windows 的 git bash、macOS 和 Linux 终端下都能跑看的时候建议开一个仓库边看边试。1. HEAD不是代码坏了冲突标记逐行拆解1.1 一个完整的冲突块长什么样先直接看一个最典型的冲突块我随便写了个例子 HEAD 我们团队的首页 Banner 文案专注互联网技术分享 新需求要放的文案欢迎来到我们的技术社区每天都有干货 feature/new-banner如果这段内容出现在你的文件里说明这一段代码同时被两个分支改过。、、这三行是 Git 插入的标记它们不属于项目代码是你手动解决冲突时必须亲手删掉的东西。很多人第一次看到这个会下意识地想“我是不是该把整段都删了”不是。你要做的是在 HEAD和 feature/new-banner之间选择保留其中一个版本、或者把两个版本合并成新代码然后把三行标记连同不要的内容一起清理掉。处理完之后这个文件不再包含任何尖括号冲突标记才算解决完。1.2 HEAD、、 各代表什么逐行拆开看其实含义非常简单 HEAD标记的是你当前所在分支的最新版本。HEAD在 Git 里不是一个刺猬它是“当前检出的那个提交”的指针说人话就是“你现在站在哪里”。你 checkout 到哪个分支HEAD通常就指向哪个分支的最新一次 commit。所以这一行下方、上方的内容是你本地的代码。分隔符。上面是你这边的版本下面是对方分支的版本。 feature/new-banner标记对方分支的身份。feature/new-banner就是那个跟你合并的分支名。这一行下方是“别人改过的”版本。类比一下就很好懂你和同事同时给同一句话交了两种改法Git 没有能力判断谁改得好于是把两个版本都用红笔抄下来放到同一个地方旁边批注一句“这里冲突了你自己 decide”。就是那个把两个版本左右摊开的书桌。1.3 Git 为什么不自动帮你选这是新人最容易误解的点。有人会问Git 不是号称很智能吗怎么连一个文案都替我选不了Git 的合并确实不是“文件级别”的简单覆盖它做的是行级别的三方合并后面会展开讲。但它不会做业务判断两个分支都改了同一行到底哪一行才是产品经理想要的Git 不知道。它知道的是“这个改动有歧义”。在歧义存在的情况下宁可把冲突显式抛给你也不能一声不吭地选一边然后丢掉另一边。所以在 Git 的设计哲学里冲突不是错误而是一种“强制你去理解代码”的保护机制。你崩溃是因为第一次见它不崩溃是因为你懂了它。2. 冲突从哪来merge、pull、rebase 背后的三方合并2.1 三方合并base、ours、theirs想彻底理解冲突来源不能只看“两边改了同一行”这种表面说法。Git 的分支合并其实是一次三方合并three-way merge三方指的是merge base两个分支分叉之前的那个共同祖先提交也就是“你俩最初一起写的那版代码”。ours我们这边当前所在分支的最新提交。theirs他们那边你要合并进来的那个分支的最新提交。Git 拿着这三份内容做对比。如果某个文件在 merge base 之后只有一边改过Git 能直接采用那边的版本这叫“干净合并”。如果两边都改过但改的不是相邻的位置Git 也可以把两边改动拼在一起这叫“自动合并”。只有当两边都修改了同一块区域或者一方删除一条线而另一方又在同一位置做了修改Git 才判断“我不知道该听谁的”于是生成冲突块。举个例子你从 dev 拉出 feature 分支的时候login.vue的第 80 行是const title 老版本标题。你在 feature 上把它改成const title V2标题同事在 dev 上把它改成了const title V3标题。等到你想把 dev 合并回 feature 时Git 对着三方一比base 是‘老版本标题’ours 是‘V2标题’theirs 是‘V3标题’两边都从 base 改走了谁才是对的它不知道于是抛给你 HEAD。2.2 哪些操作会触发冲突merge 和 rebase 有何不同常见的触发冲突操作有三种git merge合并分支时。最典型例如把 dev 合并到 feature。git pull拉取远程更新时。很多人以为 pull 只是“下载代码”其实 pull fetch merge或 fetch rebase取决于配置。下载本身不会冲突合并你那本地 commit 和同事的新 commit 时才可能冲突。git rebase变基时。比如git pull --rebase、或者多人协作时想把自己的分支“移植”到新的主干上。rebase 的原理是把你的提交一个个摘下来再依次放到新起点上。每个提交放上去时都可能和当前目标分支产生冲突所以 rebase 可能让你连续处理好几次冲突而不是像 merge 一样只处理一次总账。merge 和 rebase 冲突解决的大方向是一样的小区别在于处理 merge 冲突时 Git 还没生成合并提交你可以随时git merge --abort一键回到没合并之前的状态处理 rebase 冲突时可以用git rebase --abort回到 rebase 之前的状态。新手我强烈建议先记住这两个逃生口令它能保证你折腾到一半想反悔时还能全身而退。2.3 新人最容易触发冲突的三种姿势根据我看到的真实案例新人踩的坑基本就这三类分支活了太久。一个 feature 分支从创建到合并拖了三周三周里主干被别人推进了上百个 commit你在自己分支上又东改西改。合并的那一天冲突规模已经不是“一处两处”而是几十个文件。pull 之前不先看工作区。本地有未提交的修改直接git pullGit 可能会拒绝合并或者把冲突和未提交改动搅在一起处理起来非常酸爽。喜欢用编辑器右下角的按钮一键 Merge。不是不能用而是按钮背后发生了什么完全不可见一旦出问题你连怎么还原都不知道。3. 第一次动手解决冲突从命令行到编辑器的完整流程3.1 动手前先确认战场git status 与冲突文件清单先假设一个场景你在 feature/my-work 分支上敲了git merge dev终端里刷出一段提示包含CONFLICT (content): Merge conflict in src/pages/login.js。这时候别慌也别乱点。先执行git status它会用清晰的分类告诉你整个仓库现在处于什么状态On branch feature/my-work You have unmerged paths. (fix conflicts and run git commit) (use git merge --abort to abort the merge) Unmerged paths: both modified: src/pages/login.js both modified: src/utils/format.js看到both modified意思就是这两个文件两边都动过产生了冲突。git status给你的是“地图”接下来你对要动几个文件心里得有个数。理想情况是只列出一两个文件如果列出了十几二十个也别崩溃先压缩规模从里面挑出你真正写过的文件来处理其他文件大概率是别人大范围重构造成的打开看一眼确认可以取哪边就行。3.2 逐个处理冲突块保留、替换、还是手写合并处理单个冲突文件的步骤很简单其实就四步打开冲突文件。用任意文本编辑器都行比如 VSCode、Vim、Notepad 都可以。在文件里搜索冲突标记。搜索字符串或者更精准一点搜索^。找完一个删一个直到搜索不到任何冲突标记为止。逐个冲突块判断取舍。如果文件里一个标记都没有了这个文件就算处理完了回到终端执行git add 文件名。关键是第 3 步怎么判断。第一种情况是明显该用哪边的比如你改的是“页面标题的文案”对方改的是“接口地址”两边在同一行附近但语义互不影响你可以直接保留一方、删掉另一方。第二种情况是两边都要留比如上下两段代码分别是两个人加的新功能只是物理位置碰巧挨在一起那就把两段都留下来谁在上谁在下根据逻辑调整就好。第三种情况最麻烦两边改的是同一行而且都是核心逻辑这种时候没有银弹只能打开两边真正的意图去判断——这也是为什么我一直建议解决冲突不能只依赖工具你必须大概知道这个文件原本是干嘛的。给一个处理后的示例。原始冲突 HEAD const apiBaseUrl https://api.example.com/v2; const apiBaseUrl https://staging-api.example.com/v2; dev/environment如果确认当前分支应该用 v2 线上地址就把对方环境分支的改动删除只留一行并清掉所有标记const apiBaseUrl https://api.example.com/v2;也完全可以两行都保留但改成更明确的变量名比如const apiBaseUrl https://api.example.com/v2; // 线上。总之目标不是“删掉标记”而是“得到一份语义正确、能编译通过、能跑起来的代码”。3.3 提交合并结果几个关键命令和常见误区当git status里不再有Unmerged paths、所有文件都git add完之后最后一步是提交合并结果git commit这次提交和普通提交不同Git 会带出一个预填好的合并提交信息例如Merge branch dev into feature/my-work。你通常不需要改它直接保存退出就行。退出编辑器后用git log --oneline -1看一眼确认新提交已经生成整个合并过程就结束了。这里有几个命令我建议你在“提交之前”记牢git merge --abort在合并中途想放弃回到合并前状态。git diff --check处理完冲突后跑一下它能检测文件里残留的冲突标记和行尾空白错误。很多人以为自己清理干净了一跑这条命令还是能看到漏网之鱼。git status永远以它为准不要凭记忆判断完没完成。另外提一嘴热词里的git commit --amend。这个命令的正确用途是“修改最近一次提交的提交信息”或者把新改动合并进上一次提交。它跟解决冲突没有任何关系。我见过有新人处理完合并冲突后不想留下一个“Merge”提交于是用amend把合并提交压得看不到——这种做法只会让历史变得虚假且难以排查。合并提交就大方留着它记录的是真实发生过的事情。3.4 用 IDE 内置工具解决冲突跟在命令行手改有什么区别如果你用的是 VSCode 或 WebStorm 这类现代编辑器打开冲突文件后编辑器会自动识别冲突区域给出三个选项Accept Current Change保留我方、Accept Incoming Change保留对方、Accept Both Changes两边都留。有的编辑器还支持“逐个对比”的视图左边右边分别是两个版本中间是最终结果选起来确实直观。我的建议是IDE 工具可以用但你最好先能在命令行环境里手改一两个文件。原因是 IDE 把很多信息藏起来了比如它默认只显示冲突块不显示冲突文件里那些没有被冲突的上下文。某些冲突需要根据上下文判断语义只在工具里点“Accept”很容易选错。命令行手改虽然看起来土但能让你看清文件的完整上下文养成习惯以后对理解合并逻辑有很大帮助。4. 冲突里最容易踩的四个坑每一条都有人崩溃过4.1 只删标记不改代码或者删掉了不该删的版本这是我的真实观察新手第一次处理冲突最常见的操作就是把、、三行标记删了但中间的两版代码没有做任何取舍。这样确实不会再有冲突标记可原本两份不同的代码都被保留了下来。如果两份代码互相冲突你相当于制造了一个语法正确但逻辑完全错乱的文件。还有一种更隐蔽的在粒度比较细的冲突块里手指轻轻一滑把一整段原本应该保留的代码删掉了自己还不知道。所以我处理冲突有个习惯处理完一个文件后用它之前的内容做一次diff。具体做法是处理之前先复制一份冲突文件到临时位置处理完之后用 diff 工具逐一比对确认只有该改的位置变了。虽然有点多此一举但在几十个冲突文件这种大规模场面下它能让你避免“自以为解决了、其实把你的功能弄丢了”这种事故。4.2 没有 HEAD的冲突二进制文件、行尾符和空白冲突不一定都会以尖括号形式出现在文件里。最典型的是二进制文件比如图片、Excel、压缩包、Word 文档。两个分支各存了一个同名文件但内容不同Git 没法在你文本编辑器里展示它们的差异只能告诉你“文件名冲突”或者“文件冲突”。通常的解决办法是跟当事人商量确认用哪个版本然后把另一个版本改名备份或者干脆重新生成。另一个坑是行尾符和空白字符差异。WindowsCLRF和 Mac/LinuxLF之间的行尾差异、某次格式化工具把所有行尾空格清理了一遍都可能让 Git 认为“整个文件都被改了”。这种冲突里你会看到几千行的 HEAD但你根本不想一行行去看。治本的办法是统一团队的行尾规范在仓库根目录放.gitattributes并配置core.autocrlf。处理眼前冲突时判断一下哪个版本的文件内容更符合当前需求整体保留那一版然后让 IDE 重新格式化成统一规范即可。4.3 带着未提交的修改直接 pull被 Git 拒绝后一片慌乱场景往往是这样的你正在写代码写到一半看到群消息说主干更新了于是直接git pull。结果 Git 弹出一句error: Your local changes to the following files would be overwritten by merge后面的提示是Please commit your changes or stash them before you merge.新手遇到这句瞬间就慌了以为自己把仓库搞坏了。实际上这句话的意思是合并需要的文件在本地还没提交而那边的新改动会覆盖到这些未提交的改动。Git 认为未提交的工作属于“未定义状态”它宁可拒绝这次合并也不想把你的半成品搞丢。正确做法有两种如果未提交的改动已经完成一部分、想留着执行git stash把工作区暂存起来再执行git pullpull 完之后执行git stash pop把改动恢复回来如果那些改动已经不需要了就git reset --hard origin/dev注意这会删掉你全部未提交改动慎用。总之被拒绝时不用慌先读提示再决定怎么处理。4.4 冲突解决失败时的标准操作从头再来如果你处理到一半觉得局面彻底失控比如文件被改得面目全非、自己都不认识自己写的是什么了最省事的方法不是硬着头皮继续修而是放弃合并、回到原点。操作很简单git merge --abort执行完当前分支会恢复到这次合并开始之前的状态——你自己分支上原来的提交、工作区里的改动都还在。别小看这条命令它相当于把在明知道自己走错路的时候给了你一次重新开始的机会。rebase 场景下对应的是git rebase --abort。记住这两个逃生口之后你的心态会稳很多反正最坏的结果也就是“重新再来”那你还有什么好怕的4.5 解决完冲突的正确收尾跑测试、看 diff、别急着推git add和git commit完成以后我强烈建议你在 push 之前做两件事第一运行一次项目本地测试或至少编译一遍第二执行git diff HEAD^查看这次合并相比合并前到底改了哪些内容。合并冲突的特点就是“别人帮你把冲突文件切碎了”人眼在这种情况下很容易漏掉业务逻辑问题而测试和编译能帮你兜住一部分。如果你用的语言是 JavaScript/TypeScript且有 lint 脚本提交之前跑一遍npm run lint也很值。我见过不少冲突事后引发的问题是两边都对但合起来后变量名重复、函数引用错乱。这种问题在 resolve 冲突时根本看不出来只有跑编译或测试才暴露。5. 让冲突少发生小步提交、勤拉取、长分支拆短5.1 主干要干净分支要短命我见过太多团队把 dev 分支当成“公共停车场”谁都能直接往上面 push 半成品。这样的主干必然三天两头出问题。更健康的节奏是主干保持相对稳定、可发布个人开发在独立功能分支上进行分支从创建到合并的周期控制在两三天以内。分支活得越短分叉距离越短合并时的冲突面就越小。这不是行政规定而是 Git 合并逻辑的客观结果。前面说过冲突概率取决于两个分支在分叉之后各自改了多少重叠区域。分支越短差异越小冲突概率自然越低。所以“长分支拆短”不是工作流审美是降低冲突率的数学题。5.2 小步提交让每次合并都变小另外一个常见误区是“代码没写完不能提交”。很多新人习惯憋一个大 commit功能写完一次性提交。这会导致两个问题第一commit 粒度太大合并时 Git 拿的都是巨大差异冲突的“面积”自然更大第二某些半个功能状态下的代码一旦和别人冲突解决起来完全看不出上下文。更推荐的做法是按逻辑单元小步提交。写完一个函数就提交一次提交信息里说明“这个函数干嘛用的”。小步提交的好处是merge 或 rebase 时即使有冲突冲突也多半发生在一个单一文件、几行代码上。而且小步提交还能让你在某些出问题的时候用git bisect这类工具定位到具体位置这个后续可以单独写一篇。5.3 每天 git pull --rebase或配置 pull.rebasetrue我再强调一次git pull不是“下载代码”它是“fetch merge”。默认的 pull 是拉取远程改动后在本地生成一个 merge 提交把两个历史搅在一起。如果你希望历史更线性、冲突更好处理推荐把 pull 改成 rebase 模式。可以临时用git pull --rebase也可以永久配置git config --global pull.rebase truepull --rebase的逻辑是把你在本地尚未推送到远端的新提交一个一个“搬”到最新远端提交之上。这个过程同样可能遇到冲突而且可能每个提交都报一次冲突。但它的好处是处理完冲突后的历史是一条直线没有“两个脑袋”的 merge 提交后续往主干合并时也更干净。至于团队用什么策略最好由团队统一决定。我的个人建议是小团队、主干更新频繁的项目pull.rebase true能省掉不少历史纠缠。5.4 别和队友同时改写同一片区域最后一条听起来像废话但特别值得写出来两个人同时改同一个文件同一块区域必然冲突。你在改登录页的 JS 逻辑队友刚好也在改登录页的 JS 逻辑不管你们改的是不是同一个函数只要行号接近就有概率冲突。项目比较小的时候团队可以口头约定模块归属项目大了尽量通过代码评审、任务分配来避免“两个人长时间在同一个文件里互相叠加改动”。如果真的不得不动同一个文件也建议提前沟通好“你改上半段我改下半段”或者尽量错开时间提交。代码冲突的根源不是谁做错了什么而是信息不同步。同步做得越勤冲突爆发得越少。5.5 从 git bash 学起而不是只点按钮不是我复古而是我发现能在 IDE 按钮背后理解发生了什么的人处理起突发状况的速度快得多。新人学 Git 的前几天建议直接用 git bash 或终端敲常用命令status、log、branch、checkout、merge、pull、push、rebase、stash、diff。等这些命令都熟到不用查字典再回到 IDE 的图形界面里享受便利也不迟。如果你刚接触 Git 还没有环境顺手把 git bash 装好、配置好用户名和邮箱把这些基础准备好后面接触冲突时至少不会因为“环境没配好”而额外慌张。最后再分享一个我自己的小习惯处理任何一个带“合并”性质的操作merge、pull、rebase之前先看一眼git status确认工作区是干净的。如果工作区不干净先 stash。这个习惯花不了多少时间但能挡掉相当一部分让你崩溃的突发状况。Git 用久了你会发现大部分吓死人的问题其实都是可以在发生之前就避开的。
返回列表