ARTICLE DETAIL

资讯详情

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

SourceTree冲突解决全指南:从合并原理到外部工具实战

SourceTree冲突解决全指南:从合并原理到外部工具实战 SourceTree这个系列写到第四篇前几篇把安装、关联仓库、日常提交、拉取推送都过了一遍今天必须来啃最硬的那块骨头冲突解决。凡是使用Git超过一个礼拜的人基本都会遇到那个熟悉的画面——高高兴兴点下合并结果SourceTree里跳出一串带有黄色感叹号的文件提交按钮也被卡住项目还编译不过。很多人到这一步就慌了要么到处问人要么干脆把分支删了重来。这篇文章我打算掰开揉碎讲清楚冲突是怎么产生的、SourceTree把冲突藏在了哪些角落、以及一套从手动修改到外部合并工具都有覆盖的完整解法。1. 冲突从哪来先看Git合并的三方对比1.1 为什么有时候Git能自动合并有时候不能先纠正一个常见误解不少刚用SourceTree的人以为冲突是客户端抽风换个工具或者重新克隆就能解决。实际上SourceTree底层调用的还是Git那套合并逻辑它只是把git merge、git rebase这些命令的执行结果用图形界面呈现出来。真正决定冲突会不会出现的是Git在合并时做的一次三方对比。所谓三方对比指的是Git在合并两个分支时不是单纯看当前分支和要合并进来的分支两份代码而是还要找到这两个分支的公共祖先版本也就是merge-base。然后它会做三份比较base两个分支分道扬镳之前的样子local你当前所在分支的内容remote你准备合并进来的另一个分支的内容如果local和remote都对同一个文件的不同区域做了修改Git会把两边改的东西自动合并到一起这属于最常见的情况你甚至感觉不到合并发生过。真正产生冲突的条件是local和remote修改了同一个文件的同一段内容而且改出来的结果不一致。举一个容易理解的类比。想象同一份合同原本写着付款期限30天你和合作方各自复印了一份去修改。你把期限改成了15天对方把期限改成了45天最后把两份合同拿回来负责归档的人根本不知道到底该听谁的只能把两个版本都标出来让你亲自去和对方确认。Git在这里就是那个负责归档的人它不会擅自替你决定是15天还是45天只会诚实地举起红灯。1.2 冲突标记的解剖那排尖括号到底在说什么冲突发生时Git并没有把文件删掉也没有制造一个损坏的文件而是把两个分支的内容同时写进了同一个文件再用特殊的标记隔开。一个典型的冲突块长这样const timeout 5000; HEAD const timeout 10000; const timeout 8000; feature/login这里的 HEAD和之间是当前分支的版本和 feature/login之间是feature分支的版本。整个文件里可能会出现多个这样的冲突块你需要逐个处理最后保留其中一个或者把两边的思路融合起来然后删掉这些标记行。有个非常重要的认知冲突状态下文件里的内容并不代表最终结果它只是Git把双方意见摊开给你看。这时候项目代码通常是编译不过的因为你可能同时存在两个const timeout定义。所以冲突不是终点而是一个必须你亲自介入的中间状态。2. SourceTree把冲突摆在了哪里界面识别与我的/他们的语义2.1 冲突文件在文件状态面板里的几种提示当你点击合并之后SourceTree并不会像命令行那样直接打印一大段文字告诉你哪里冲突它的提示散落在界面好几个地方。第一次遇到的人很容易只盯着某个红色弹窗看结果什么都找不到。下面这些位置都要习惯性地检查一遍。第一处是左侧文件状态面板。处于冲突状态的文件文件名旁边会显示一个黄色感叹号或者橙色圆点的图标具体颜色在不同版本的SourceTree里略有差异但一眼看过去和普通的绿色加号、黄色圆点是明显不同的。有些版本还会在文件行背景上给出高亮提示。第二处是顶部导航栏附近。合并发生冲突后SourceTree通常会显示一个合并中的状态条告诉你当前正处于合并/变基过程中有多少个文件冲突。如果这个提示条一直没消失说明你还处于未完成的合并状态。第三处是提交按钮。普通状态下按钮写着提交冲突未解决时会变成类似提交合并或者干脆是灰的这是在提醒你目前还有东西没处理完。很多新人这时候习惯性点提交发现没反应才意识到哪里出了问题。排查冲突时建议先看文件状态面板里带感叹号的文件逐个打开处理。你可能会看到一部分文件虽然没有感叹号但被Git自动合并好了这些会自动进入暂存区不需要你操心。真正要动手的只有那些标记了冲突的文件。2.2 采用我的版本和采用他们的版本谁是谁在SourceTree里右键点一个冲突文件会看到解决冲突菜单里面通常有这几项打开外部合并工具、标记为已解决、采用我的版本、采用他们的版本。很多人在我的版本和他们的版本上栽过跟头。这里必须先搞清楚语义在合并场景下我的指的是你当前所在的分支。比如你把分支feature/login合并进main当前停在main分支上那么我的版本就是main里的代码他们的版本是feature/login里的代码。有个非常容易混淆的例外如果你在做rebase变基那么我的和他们的含义会和merge完全反过来。因为在rebase时SourceTree从你正在变基的分支视角出发当前分支成了临时载体他们的反而指向目标分支。所以当你看到这个菜单时先抬头看顶部的分支栏确认自己到底是在merge还是在rebase再决定点哪一边。场景我的版本他们的版本运行git merge时当前所在分支被合并进来的分支运行git rebase时正在变基的分支目标分支2.3 提交按钮变化与合并状态提示完成冲突处理后别急着提交先确认所有冲突文件都被标记为已解决。这一步本质上是执行git add把文件从冲突状态变为暂存状态。在SourceTree里你可以逐个右键文件点击标记为已解决也可以选中多个文件批量操作。当所有冲突文件都被标记之后顶部提示条会变成正常的可提交状态提交按钮恢复可用。这时候SourceTree会引导你创建一条合并提交。合并提交和普通提交不一样它通常带两个父提交代表着两个分支的汇聚点在提交历史图里会呈现出一个很明显的Y字形分叉汇合。3. 解决冲突的两条路线手动编辑与外部合并工具3.1 路线一纯手动修改适合冲突量小时秒收工如果冲突只涉及一两个文件而且每个文件里只有一两处冲突块我建议直接用编辑器手动改。步骤很简单在SourceTree文件状态面板里双击冲突文件。默认会用系统关联的文本编辑器打开比如VS Code。搜索定位所有冲突块逐个阅读两边的代码。删除不需要的部分保留最终版本同时删掉所有的、、标记行。保存文件回到SourceTree右键这个文件选择标记为已解决。手动修改的坑在于很容易漏掉某个冲突标记。如果你解决了三处还有一处藏在文件末尾保存后编译就会报错。所以每次手动改完强烈建议再用编辑器的全局搜索搜一遍确认文件中已经没有冲突标记。在VS Code里冲突块的底色会被高亮左右两边代码还会分别用不同颜色区分并且提供采用当前更改采用传入更改之类的快捷按钮。这些快捷按钮的本质和SourceTree里的我的版本他们的版本是一样的只是放在编辑器里更方便。3.2 路线二给SourceTree配好Beyond Compare这类外部合并工具冲突块多、文件多、或者两边代码逻辑比较复杂的时候纯粹手改很容易改乱。这时候我推荐走外部合并工具路线把SourceTree和Beyond Compare联动起来。为什么推荐Beyond Compare而不是别的我在Windows上对比过几款Beyond Compare的三向合并界面最直观左边是base、中间是local、右边是remote结果输出窗口放在最下方每个冲突块都能逐块选择取左取右保留base手动输入。Meld和KDiff3免费但界面观感比较老对新手不如BC友好。如果你在macOS上也可以用系统自带的FileMerge或者付费的Kaleidoscope操作逻辑差不太多。配置方法如下安装Beyond Compare 4或更高版本。打开SourceTree进入工具 - 选项 - 比较不同版本菜单名可能叫Diff或差异比较。在外部Diff/Merge相关区域把Diff工具和Merge工具都选择为Beyond Compare。如果SourceTree没有自动识别安装路径就手动填写。需要填写的命令本质上是这么两条。Diff命令C:\Program Files\Beyond Compare 4\BComp.exe $LOCAL $REMOTEMerge命令C:\Program Files\Beyond Compare 4\BComp.exe $LOCAL $REMOTE $MERGED $BASE这里的$LOCAL、$REMOTE、$MERGED、$BASE是SourceTree约定的占位符含义分别是当前分支版本、对方分支版本、合并输出后的文件路径、公共祖先版本。Beyond Compare会启动三向合并模式你改完内容保存后结果直接写回$MERGED指向的文件。配置好后双击冲突文件或者在右键菜单里选择解决冲突 - 打开外部合并工具Beyond Compare就会自动弹出。不同版本的SourceTree对参数的传递略有差异但核心占位符基本一致遇到行为不对时检查一下参数顺序即可。3.3 外部合并工具里的三向对比界面怎么读在Beyond Compare的三向合并界面里你会看到比较清晰的左右对照。左边一般显示当前分支版本右边显示对方分支版本下方是合并结果输出区。每个冲突块都会单独高亮工具栏上通常有从左复制到输出从右复制到输出从祖先复制到输出这类按钮。实际操作时我的习惯是先看一眼左边和右边各自做了什么改动逐块判断应该保留哪边。如果两边都是新增代码可以同时保留顺序上再调整。如果两边改的是同一个变量的赋值就要看业务逻辑上哪个值才是当前迭代真正需要的。改完后直接看输出窗口确认没有冲突标记残留再保存关闭。用完Beyond Compare回到SourceTree你会发现冲突文件的状态已经变成已修改但还没有被标记为已解决。记住这一步不会自动完成需要你右键标记为已解决。4. 从造一个冲突到收尾提交完整流程实录4.1 复现场景main和feature/login同时改timeout光说不练假把式我搭一个最简单但完全可复现的冲突现场。假设项目里有一个src/config.js文件最初在main分支上的内容是这样的const apiUrl https://api.example.com/v1; const timeout 5000; export function request(path, options) { return fetch(apiUrl path, { timeout, ...options, }); }接着你从main切出分支feature/login并把这个文件改成const apiUrl https://api.example.com/v2; const timeout 8000; export function request(path, options) { return fetch(apiUrl path, { timeout, ...options, }); }提交到feature/login后你切回main同事此时在main上提交了一版改动把timeout调成了10000还顺手加了一个header配置const apiUrl https://api.example.com/v1; const timeout 10000; const headers { X-Custom-Header: 1 }; export function request(path, options) { return fetch(apiUrl path, { timeout, ...options, }); }注意此时main上的apiUrl还是v1而feature/login上已经改成了v2。两边都动了timeout这就是冲突的种子。4.2 在SourceTree里合并、看冲突、打开Beyond Compare你现在停在main分支上双击feature/login分支选择合并feature/login到当前分支。SourceTree执行完合并后src/config.js进入冲突状态文件状态面板里出现黄色感叹号。双击这个文件如果按第3章配好了Beyond Compare它会自动弹出三向合并窗口。如果没配外部工具直接用VS Code打开你会看到文件被改成了这样const apiUrl https://api.example.com/v2; HEAD const timeout 10000; const timeout 8000; feature/login const headers { X-Custom-Header: 1 }; export function request(path, options) { return fetch(apiUrl path, { timeout, ...options, }); }仔细看这个例子很有意思apiUrl和headers这两处都没有冲突Git自动完成了合并只有timeout两边都改过产生了冲突块。这正是三方对比的直观体现——Git有能力自动合并一部分只把真正有分歧的内容交给你。在Beyond Compare里你会看到左侧main的timeout是10000右侧feature/login的timeout是8000。假设你确认这轮迭代需要的超时时间是10000那么直接把左边的值复制到输出窗口即可。如果两边都有必须保留的逻辑也可以手动在输出窗口把两边内容按顺序拼起来。4.3 解决后的收尾检查diff、跑编译、提交合并关闭Beyond Compare回到SourceTreesrc/config.js的状态从冲突变为已修改。先别急着提交再做三件事第一右键这个文件选择标记为已解决让文件进入暂存区。第二切到文件状态面板仔细看一遍Diff确认最终结果里apiUrl、timeout、headers都是你想要的。第三在本地跑一遍编译或相关测试确保代码能正常工作。全部通过后SourceTree提示你可以提交。提交信息通常保持默认的Merge branch feature/login into main即可也可以补充一些内容说明这次合并解决了什么。点下提交合并提交正式生成。这一步特别想强调很多人在解决完冲突后不跑测试直接推送到远端结果CI直接挂掉。尤其是那种两边代码都保留、顺序调整过的场景非常容易出现语法错误或逻辑矛盾。冲突解决后的验证步骤和解决冲突本身同样重要。5. 冲突处理翻车现场我踩过和见别人踩过的坑5.1 合并到一半想反悔怎么安全退出有一种情况特别常见合并下去之后发现冲突文件越来越多或者发现自己根本不了解对方分支的改动想冷静一下再处理。这时候怎么办如果还没开始标记任何文件最简单的方式是直接在SourceTree顶部提示条找到中止按钮它对应的是git merge --abort会把所有文件恢复到合并之前的状态冲突标记全部消失。如果已经手动改了一部分文件并且标记为已解决仍然可以中止合并。但要注意你改过的文件会因为没有进入提交历史而被丢弃等于白改。所以我个人习惯是只要发现冲突超出预期第一秒就先中止不纠结于已经改了哪几个文件。如果你是在rebase过程中想退出对应的命令是git rebase --abort。SourceTree的界面提示可能叫中止变基或者直接是英文Abort不要和merge的abort混在一起。5.2 漏掉冲突标记导致编译崩盘手动编辑最容易犯的错就是漏掉一个冲突块。这个坑我看了太多次我自己早期也踩过。明明把看着最大的那一块处理完了保存回SourceTree标记已解决提交推送结果CI报语法错误一查是文件末尾还留着一个 HEAD。解决这个问题的办法是走流程而不是靠眼神。改完文件后用编辑器的全局搜索搜一遍确保搜索结果是0行或者用命令行在项目目录下扫一遍grep -rn \|\| --include*.js --include*.ts .这里可能需要转义不同环境下写法略有差异但不影响思路。发现问题就回到编辑器处理没发现问题再标记为已解决。5.3 误用我的版本把同事的修复覆盖掉这个坑的破坏力更大。有一次同事在feature分支上修了一个线上bug我在main上合并这个分支碰到冲突时图省事直接点了采用我的版本结果把同事的修复彻底覆盖了。后来排查到原因就是没想清楚我的版本和他们的版本在合并场景下的定义。main作为当前分支里面当然没有同事的修复选中我的版本等于把同事那段改动丢掉了。更麻烦的是Git不会阻止这种操作它会乖乖把文件标记为已解决让你顺利完成合并。从那以后我的原则变成了遇到冲突先看两边的代码和提交记录确认到底哪一边是最新意图再决定保留谁。如果两段代码语义都理解不了宁可打开外部合并工具逐块对比也不要直接点我的版本或他们的版本。这种二选一的操作适合你已经百分百确定想要的场景适合新手了解界面用的场景不适合用在一个你根本不熟悉的模块上。5.4 冲突文件多的时候该怎么批量推进一次合并几十个文件冲突的情况虽然少见但碰上就是大活。我的处理顺序是先把所有冲突文件按类型和熟悉程度分组。核心业务代码优先处理因为牵一发动全身配置文件、测试文件、样式文件放在后面处理。SourceTree支持在文件面板里多选然后批量执行标记为已解决但我建议不要一次性全选标记。正确做法是每解决一个文件就单独标记一个。这样哪个文件还没处理一眼就能看出来。如果你批量把十个文件全标记了结果发现其中有两个根本没改干净后续要退回会很麻烦因为git add已经把它们从冲突状态挪到了暂存区。处理复杂冲突时我还会在编辑器里分屏打开base、local、remote三个版本方便对比。Beyond Compare本身就支持这种三向视图这也是我推荐它的原因。5.5 不小心把带冲突标记的文件提交并推送了如果说上面几个坑只是让人头疼这个坑就是灾难级的。场景通常是这样的你手动改了一半没保存SourceTree把它当成已经处理过了或者你保存了但漏看了一处提交时Git不会拦截你推送也畅通无阻。一旦推送到远端整个团队都会拉到一个编译不过的版本。这时候别慌分两步处理。如果还没有人拉取最新代码最稳的方式是本地重新改好文件提交一个修复提交再推上去。如果有人已经拉到了就不要再做force push了直接提交修复并把损坏的提交保留在历史上毕竟代码仓库的可追溯性比漂亮的历史更重要。修复提交里可以用上前面说的搜索命令确认全项目已经没有冲突标记。这个检查动作我建议每次都做。6. 日常少踩冲突的五个习惯6.1 小步提交分支别活太久冲突频率和分支生命周期强相关。一个功能分支存活三周每天都有人往main上合代码到了最后你可能要面对几百个冲突文件。反过来如果这个分支只活了两天每天合并一次每次冲突量都在可控范围内。所以我的习惯是一个分支只做一个功能功能不要做得太大完成了就尽快合并。哪怕分支还没完全收尾也可以先合并一部分或者把main往分支里merge一次让分支始终追着主干走而不是等最后一次性汇合。6.2 开工先同步、合并前先拉取很多人出门第一件事是打开SourceTree写代码这其实不给机会。正确的做法是开工先拉取最新代码确保你的本地分支是基于最新main的。合并分支之前也先拉取一次避免你基于一个过期版本做合并把别人几天前已经修掉的东西又重新引入冲突。关于拉取时用merge还是rebase我的建议是如果分支只在本地一个人用优先rebase历史更线性如果分支已经推到远端并且别人也拉过不要rebase老老实实用merge。否则会产生更复杂的变基冲突得不偿失。6.3 把格式化改动和功能改动分开这一条几乎人人都知道但真正做到的不多。比如你在一个功能分支上运行了一次全局格式化器Prettier整个项目几百个文件的行尾、引号、缩进全变了。合回main时几乎所有和你改过同一批文件的人都会碰到冲突。更让协作者崩溃的是这种冲突里混着大量仅格式化差异的diff根本看不出谁改了什么逻辑。正确做法是格式化的提交单独成一条功能改动单独成一条提交信息写清楚chore: format code with prettier这样别人在冲突解决时至少能分辨出那些纯格式变化是什么。6.4 统一处理换行符用.gitattributes兜底换行符是隐藏的冲突制造机。Windows上默认CRLFmacOS/Linux默认LFGit在checkout和commit时会做转换。如果某个文件被不同的换行符来回切换diff会显示整个文件都变了哪怕实际只改了一行。仓库根目录放一个.gitattributes文件能省掉大半这种麻烦* textauto *.js text eollf *.ts text eollf *.json text eollf *.md text eollf含义是普通文件交给Git自动处理行尾JS/TS/JSON/Markdown这些代码文件统一使用LF。团队里所有人checkout后看到的文件一致提交到仓库里的格式也一致无意义的冲突会大幅减少。6.5 复杂冲突动口不动手最后这条也算不上技术更多是协作经验。当你发现一个冲突涉及你完全不了解的业务模块直接去找改那部分代码的同事花两分钟问清楚他改这段代码是为了什么。这比你自己盯着两个版本猜半天要高效得多。我在团队里见到过太多这样的场景一个人对着冲突文件纠结半小时最后选了一个版本推送第二天测试反馈bug又要花一天排查。而另一个同事碰到同样情况直接拉上原作者打开Beyond Compare逐块过五分钟就确认了每一个冲突块该保留哪边。最后补一句我自己实实在在的体会冲突解决并不是写代码拼手速它考验的是对上下文的理解和对别人代码的尊重。我在团队里带下来的习惯是凡是冲突稍微复杂一点就拉上写另一段代码的同事一起看着Beyond Compare界面逐块过十次有九次都能在五分钟内达成共识。你花在沟通上的两分钟永远比盲选我的版本之后被测试找回来讨债的时间便宜。
返回列表