我们团队上个月做一次版本迭代开发到一半产品经理突然说线上有个紧急bug要马上修。当时新功能的代码已经散落在两个feature分支上线上分支的代码和它们差了差不多几十个提交。那一刻我在终端里敲git branch看着屏幕上的分支列表突然意识到理解git分支的创建、合并、删除这套基本功是真的能在关键时刻救命的。不少开发干了两三年git clone和git commit用得贼溜但一碰到多分支协作就慌不知道远程分支怎么建合并代码时遇到冲突直接傻眼分支删不干净弄得本地分支列表越来越脏。这篇文章我把这几块东西拆开揉碎了讲一遍。不追求把git文档翻译一遍只讲实际开发中真正用得上的操作以及这些操作背后为什么是这样设计的。1. 分支的设计思路先搞懂Git到底在解决什么问题1.1 分支不是文件夹拷贝是指针很多新手理解的“分支”是代码复制了一份各改各的最后再想办法合到一起。这个理解不算错但会把后面很多操作带偏。Git里的分支本质上是一个指向某次提交commit的可移动指针。你执行git branch feature-login它不复制任何代码文件只是在.git/refs/heads/目录下新建了一个文件里面存了当前提交的哈希值。真正的代码对象全部存在对象库object database里靠哈希值引用。所以git创建分支是瞬时操作不管你的仓库是10MB还是10GB创建分支都是毫秒级完成。理解了“指针”这个模型很多行为就说得通了分支名带不带origin/前缀含义完全不同。origin/feature-login是远程仓库的“快照指针”它只在fetch或push时更新不跟着你的本地提交走。切换分支时工作区文件会变是因为git用指针找到对应提交然后把文件内容从对象库里解出来填进工作区。所谓“删除分支”本质就是删掉那个指针文件。只要提交还在对象库里用git reflog把它找回来不过是几秒钟的事。1.2 为什么要有远程分支这个概念把分支想象成“在某个时间点给代码拍了一张快照并起了个名字”本地分支就是你自己手里的快照远程分支就是托管服务器上的快照。多人协作时谁也不希望自己的本地快照直接暴露给其他人看于是git设计了origin/xxx这种远程跟踪分支remote-tracking branch来做“同步的中间态”。我见过不少团队协作流程是“谁做完谁push到master”然后靠聊天工具吼一声让大家拉最新代码。这种流程在小团队、低并发时能跑但只要两个人同时改了同一个文件痛苦的合并过程就开始了。规范的做法是用feature分支隔离开发用远程分支做同步边界用合并merge或变基rebase把各自的工作成果汇到一起。这套流程的根基就是先把分支的“指针模型”和“远程跟踪机制”吃透。2. 创建远程分支从本地到远端的完整链路2.1 最常见的操作路径先说结论git没有一个叫“创建远程分支”的独立命令。远程分支的创建实际是“本地分支 push”的组合动作。过程分三步# 第一步基于当前所在分支创建并切换到新分支 git checkout -b feature/payment-v2 # 第二步在新分支上正常开发、提交 git add . git commit -m feat: 新增支付v2接口 # 第三步把本地分支推送到远程并建立跟踪关系 git push -u origin feature/payment-v2执行完第三步git branch -r会看到origin/feature/payment-v2远程分支就建好了。-u参数是--set-upstream的简写它的作用是给本地分支设置上游分支。设置之后以后直接敲git push和git pullgit就知道该跟哪个远程分支打交道不用每次把分支名写全。2.2 push命令的参数拆解那条git push -u origin feature/payment-v2值得细说。origin是远程仓库的别名通常在git clone时自动生成指向你拷贝的远程地址。feature/payment-v2是你要推送的本地分支名没有加冒号所以它既表示“本地分支名”也表示“远程分支名”这是一条简写规则。如果你想把本地分支推到远程另一个名字下可以写全映射关系git push origin feature/payment-v2:feature/payment-release这样远程会多出一个叫feature/payment-release的分支内容与本地feature/payment-v2一致。但这种改名推送在日常协作里用得不多反而容易在团队里造成“远程名和本地名对不上”的困惑建议只在临时应急时用。2.3 实际操作时会踩的坑第一个坑不小心把开发分支推到了master。有些人习惯用git push origin master推送但你当前在feature/payment-v2分支上时这条命令并不会把feature分支推上去而是会把本地的master分支推上去如果你本地master落后远程很多Git会拒绝推送non-fast-forward。但这已经够吓人了。更稳妥的做法是推之前先看一眼当前分支git branch --show-current。第二个坑clone下来的仓库本地没有远程的新分支。你同事推了一个feature/order-export上去你想切到这个分支开发直接git checkout feature/order-export通常是不行的较新版本的git可以但老版本不行。规范操作是先抓取远程分支信息再基于它创建本地分支git fetch origin git checkout -b feature/order-export origin/feature/order-export第二句也可以简写成git checkout --track origin/feature/order-exportgit会自动创建一个同名的本地分支并设置跟踪关系。第三个坑分支名不规范。我见过分支名叫final、test2、bugfix2222的等过一个月再看谁也不知道这分支是干嘛的。推荐的分支命名规范大概是feature/功能、bugfix/缺陷修复、hotfix/紧急修复、release/发版分支后面接简短描述比如feature/user-center-refactor。2.4 创建远程分支之前先想想从哪个分支切出来很多人创建分支时不太在意基于哪个分支这是个隐患。比如你在develop上有一些本地未提交的修改这时执行git checkout -b feature/abc新分支会带着工作区里那些还没提交的改动一起切过去。你以为是新起点实际上把一堆“半成品”也带过去了。我自己的习惯是切新分支前先git status确认工作区干净。如果有未提交的改动要么先提交到当前分支要么用git stash暂存等新分支建好后再git stash pop恢复。这个习惯看着啰嗦但能省掉后面合并时大量“怎么这个文件有这个改动”的排查时间。3. 分支合并merge与rebase的选择题3.1 合并的方法不止一种分支合并的最终目的是把另一个分支的提交整合到当前分支。但实现方式分成两大流派git merge和git rebase。两者的工作方式完全不同理解它们的区别是处理合并冲突的关键。git merge是“把两个分支的历史汇合在一起”。它会找两个分支的最近共同祖先merge base然后把当前分支和待合并分支各自相对于这个祖先的改动都保留下来生成一个新的合并提交merge commit。这个提交有两个父提交git的提交历史上会看到明显的“分叉再汇合”痕迹。# 在develop分支上把feature/login的改动合并进来 git checkout develop git merge feature/logingit rebase则是“把当前分支的提交在目标分支最新的提交之上重新放一遍”。它不做“汇合”而是把你分支上的每一次提交取出来在目标分支的新提交之后逐个重放。结果是当前分支的历史变成一条直线没有合并提交提交历史非常干净。# 在feature/login分支上把develop的更新变基进来 git checkout feature/login git rebase develop两者的核心差异可以总结为merge保留“真实发生过什么”rebase重塑“看起来发生了什么”。3.2 什么时候用merge什么时候用rebase我见过最混乱的git协作场景就是把merge和rebase混用且没有任何规则。我自己的经验是分场景对待合并功能分支回主线比如feature合并到develop/master用merge。理由很简单合并提交记录了“这个功能在什么时候、从哪个分支合进来的”团队其他人review历史时能清楚地看到功能开发的脉络。而且merge是“非破坏性”操作它不会改动已有提交的哈希也不改写历史任何人都可以放心执行。更新自己的功能分支比如把develop的最新代码同步到feature分支用rebase。理由也很实际如果你在feature分支上反复merge develop分支历史里会出现大量“Merge branch develop into feature/login”这种无意义的合并提交看着很吵。用rebase把feature分支的提交“搬”到develop最新代码之上历史是一条直线干净清爽。但rebase有一个必须注意的前提只能对那些“没有推到远程共享分支”的提交执行。因为rebase会改写提交哈希你本地feature/login的提交哈希和远程origin/feature/login上的哈希会变得完全不一样。如果别人已经基于远程那个分支做了开发你rebase再强推就是把别人脚下的瓷砖抽了。三句话原则要合并到共享分支merge保留历史。要把共享分支的更新同步到个人分支rebase保持线性。共享分支上的提交永远不要rebase。3.3 合并冲突它是流程的一部分不是错误多人开发同一批文件冲突几乎是必然的。Git解决冲突的核心机制是当两个分支都修改了同一个文件的同一块区域时git自己无法判断该保留哪个覆盖哪个于是把决定权交还给人。实际遇到冲突时文件里会出现这样的标记 HEAD 这里是你当前分支比如develop上的内容 这里是待合并分支比如feature/login上的内容 feature/login处理步骤是git status查看冲突文件列表。逐个打开冲突文件手动决定保留哪部分、删除哪部分标记符号。改完后git add标记为已解决。如果是merge执行git merge --continue编辑器里填一下合并提交信息保存退出。如果是rebase执行git rebase --continue这会重放下一个提交如果还有冲突继续重复上面步骤。这里有两条血泪教训第一不要用编辑器自带的“接受当前/接受传入”按钮无脑处理冲突。vscode、IDEA里确实有这个按钮但“接受传入”意味着你完全丢弃了当前分支这一段修改如果那两个分支本身有相互依赖的逻辑这样简单粗暴处理常常会编出一个逻辑上不自洽的代码来。正确做法是把冲突两边的代码都看一遍理解各自意图再决定怎么合并。第二冲突文件多的时候一个一个来别着急。我记得有一次合并冲突文件有23个我头昏眼花半小时处理完以为自己搞定了结果编译报错十几处。后来养成了习惯冲突解决后先跑一遍项目构建或测试确认没问题再提交这个步骤能拦下一大批“合并冲突虽然解决但代码逻辑坏掉”的事故。3.4 两个容易翻车的合并操作操作一revert了一个分支之后再合并其他分支时会冲突。比如master上有一个提交A后来发现有问题用git revert A把这个改动“回滚”掉。此时master上的这个文件回到了A之前的状态。接着你有一个feature分支它基于A之后的状态开发也改了同一文件等你把feature合并回master时git会看到master上“Arevert”这两次提交都接触过该文件feature也接触过于是大概率冲突而且冲突内容极其晦涩因为A和revert A正好把改动抵消了。处理这种冲突真的只能静下心把三个时间点的文件版本逐一对比有时甚至要git log -p查看完整修改脉络才能理清。操作二rebase时把提交弄丢了。如果你在rebase过程中发现冲突太多想放弃这次rebase可以用git rebase --abort回到rebase开始前的状态。但如果你已经做了一次git rebase --continue再想反悔就不能简单地abort了。此时可以用git reflog找到rebase之前所在的那个提交哈希然后git reset --hard 那个哈希整个分支就回到rebase前的样子了。reflog是git的“后悔药开关”强烈建议每个开发者都记住这个命令。4. 删除分支本地、远程以及那些残留引用4.1 删除本地分支的正确姿势删除分支的核心命令是git branch -d# 删除本地分支 git branch -d feature/old-feature-d是--delete的简写。git在这里做了一个保护如果这个分支还有未被合并的提交它会拒绝删除并提示“not fully merged”。这是git在防止你误删“还没合入主线的代码”。如果确认这个分支上的提交确实不需要保留了可以用大写-D强制删除git branch -D feature/abandoned-work-D是--delete --force的合并简写。注意强制删除之前请务必确认两点一是这个分支没有推到远程或者虽然推了但远程也打算删二是这些提交确实没有任何保留价值。一旦删掉虽然git reflog里还能找回但时间长了就会淹没在大量记录里。还有个细节不能删除当前所在的分支。Git会提示“Can not delete branch checked out”意思是“你不能删掉自己正站着的树枝”。必须先切到别的分支再执行删除。4.2 删除远程分支远程分支的删除是push的反向操作用的是--delete参数git push origin --delete feature/old-feature也可以写成简写形式git push origin -d feature/old-feature这个命令的本质是告诉远程仓库“把这个分支引用删掉”。执行成功后远程的分支就不存在了你可以在GitHub/GitLab的页面上确认。需要注意删除远程分支不会自动删除你本地的同名分支也不会自动删除本地的远程跟踪引用origin/feature/old-feature。所以正常情况下删完远程之后还要删一遍本地分支并且清理跟踪引用git branch -d feature/old-feature git fetch --prune--prune会清理本地的远程跟踪分支引用把那些远程已经不存在但本地还留着的origin/xxx标记删掉。4.3 清理残留分支的三种场景场景一本地一堆号开头的分支。终端里执行git branch时明明远程分支在GitLab上已经没了本地却还显示着一大堆列表很长。这是远程跟踪引用没清干净执行git fetch --prune或者更明确一点git remote prune origin两条命令作用类似推荐用git fetch --prune因为fetch还能顺便把其他远程分支的最新状态拉下来。场景二vscode里看不到远程已删除的分支但本地分支列表还能看到。vscode的Git插件GitLens等通常会读取本地的远程跟踪引用如果引用没清理界面上就会挂着一些“幽灵分支”。执行一遍git fetch --prune刷新一下Git视图基本就干净了。场景三idea里分支列表特别长想统一清理。在IDEA的Git工具窗口里右键分支节点可以选择“Delete”删除本地分支远程分支在origin/节点下右键也可以删除。但如果远程分支很多一条条右键显然不现实我一般直接在终端里批量处理# 把本地所有的远程跟踪分支列出来看看 git branch -r # 把确认不需要的远程分支批量删掉一条条来别用通配符乱删批量操作时务必一条条列清楚再删。git没有“按前缀批量删除远程分支”的开箱用法用脚本来删也不是不行但要非常谨慎别把同事正在用的分支误删了。4.4 关于删除分支和找回提交的补充说一个我踩过的坑删除了一个看似“已经完全合并”的功能分支结果后来发现那个合并本身被revert过等于功能代码实际上没有生效。此时那个分支上的提交已经找不到了revert后再合并git认为分支已合并所以git branch -d不会拦截。后来同事在代码里找不到某个接口实现我凭着印象用git log --all -- 文件名找到那个提交的哈希再用git cherry-pick把关键提交救回来了。这件事实在有点反直觉。git branch -d提示“分支已完全合并”看起来是安全的但如果你曾经对这个分支做过revert、reset这类“反悔操作”“完全合并”的判断不一定代表“代码最终生效”。所以删除分支前最稳妥的做法是看一眼git log里这个分支的实际提交记录确认它们真的存活在目标分支上再删。5. 实际开发中绕不开的典型场景问答5.1 场景一合并master时冲突了如何处理max热搜词里有一条“master分支revert后其他分支合并master冲突”这个前面讲过原理了。这里补充一下实际处理步骤先git merge mastergit会报告冲突文件。打开每个冲突文件看和标记。如果某一块改动是“master上revert而feature分支正好改的是同一代码”你基本只能人工判断这个功能到底要不要保留。要保留就把feature侧的改动手动合进去不要保留就把两边内容一起删掉换成旧版本。每次处理完一个文件git add最后git merge --continue。我的体会是这种“revert叠加合并”的冲突是git里最烧脑的冲突类型之一因为它本质上不是代码冲突而是“需求决策冲突”——到底哪个版本才是最终要的机器判断不了代码本身也看不出端倪只能靠人来定。5.2 场景二在idea里commit了但还没push怎么撤掉热搜词里有一条“怎么删除idea上git某个分支上commit但未push的代码”。这个问题很常见一般出现在“啊我刚才commit错了分支”或者“这个提交里有一个不该提交的文件”。方法分两种如果只是想撤销提交但保留工作区改动也就是把提交“退”回暂存区git reset --soft HEAD~1HEAD~1表示当前提交的前一个提交。--soft的意思是“移动指针但不动暂存区和工作区”。执行后那个commit里的改动会回到暂存区绿色你可以重新组织文件再提交。如果是想撤销提交同时把工作区改动一并清除彻底回到上一个提交的状态git reset --hard HEAD~1这个操作会把工作区里所有未提交的改动也一并丢掉慎用。如果提交里有些文件改动很重要只是提交信息写错了千万别用--hard改用git commit --amend修改提交信息git commit --amend -m 正确的提交信息--amend其实就是“把当前暂存区的改动和上一个提交合并生成一个新提交替换掉原来的提交”。很多新手以为它是“修改最后一次提交信息专用”实际上它也能用来往最后一次提交里追加遗漏的文件。在IDEA里也有对应的图形化操作在Git工具窗口右键提交记录会看到“Undo Commit”和“Drop Commit”之类的选项原理和reset一致。但图形界面容易让人忘了底层原理我还是推荐大家先把命令行的reset练明白。5.3 场景三git commit --amend用错了怎么补救--amend确实好用但它也会改写提交哈希。如果这个提交已经push到远程共享分支你再amend并强行push就会造成远程历史被改写其他同事拉代码时会看到一堆奇怪的冲突甚至“丢失提交”的错觉。补救方法# 先找到amend之前的提交哈希 git reflog # 记住想要恢复的那条记录 git reset --hard 那个哈希reflog记录了你本地分支指针移动的历史包括amend、reset、rebase等操作都记录在案。只要操作还没超过默认的保留时间通常90天都有机会找回。5.4 场景四gitlab上分支合并的权限管理企业里用GitLab做代码托管时通常会给master或main分支设置保护Protected Branches。被保护的分支普通开发者不能直接push只能通过Merge Request合并请求来合入代码而且通常还要求至少一个评审人approved之后才能点Merge。这种情况下“分支合并”的动作往往不是开发者自己执行的而是你提交Merge Request、评审人审核通过、最后由自动化流水线或具备权限的维护者完成merge。这时你自己就只需要保证功能分支干净、冲突已解决、commit信息规范。如果MR页面显示“Cannot merge due to conflicts”你先在本地把目标分支合并进自己的功能分支解决冲突后再pushMR就变绿了git fetch origin git checkout feature/your-branch git merge origin/master # 把目标分支的最新代码合并进来 # 解决冲突 git push origin feature/your-branch5.5 场景五清理“已经删除的远程分支”在vscode里的残留vscode用过一段时间后分支列表经常混进一堆已经不存在的远程分支。简单操作路径在vscode的终端里执行git fetch --prune。点击左侧源代码管理图标刷新一下Git视图。如果还残留可以执行git remote prune origin再刷一次。这个操作很安全它只清理“远程已删除”的跟踪引用不会动任何本地未推送的提交。5.6 常见问题速查表问题现象原因解决方法git push提示non-fast-forward本地落后于远程git pull --rebase再push删除本地分支提示未合并分支有未合并提交确认不需要后改用-D远程分支删了本地还显示远程跟踪引用残留git fetch --prune合并后代码丢了可能revert或reset过git reflog找回提交信息写错了最后一次提交git commit --amendcommit后想撤回提交未pushgit reset --soft HEAD~1rebase冲突太多想反悔rebase进行中git rebase --abort改了错误分支的代码代码在working treegit stash后切分支git stash pop6. 把git操作变成肌肉记忆的练习建议Git这个东西光看文章不实操很快就忘了。我自己带了几个实习生总结出一个规律真正能稳定上手git的不是背命令背得最多的人而是把每一条命令的效果想明白的人。我建议你做这样几组练习每次都用同一个测试仓库随便造点文件就行第一组练习创建和推送。在本地建一个仓库创建三个分支分别推送到远程然后用git branch -vv查看每个分支的跟踪关系看看和你想的是否一致。第二组练习合并和冲突。故意在两个分支上修改同一个文件的同一行然后merge体会冲突标记的呈现方式和解决流程。这个练习要多做几次直到你能不看教程就独立处理完冲突。第三组练习删除和找回。创建一个分支提交几次然后删掉它再用git reflog把它找回来。经历过一次“删除可恢复”你对分支本质的理解会瞬间加深。第四组练习rebase与revert。在feature分支上做几个提交rebase到develop上然后用git log --graph观察历史形态变化。再手动造一个“合并后revert再合并”的场景体验那种最烧脑的冲突。这四组练习做完你的git基本功就基本夯实了。后面遇到复杂的协作流程比如cherry-pick、submodule、bisect就都是在此基础上的增量学习了。我在实际开发里越用越觉得git命令不多关键是把“指针”“引用”“提交图”这几个底层概念吃透好多操作根本不用背推演一遍就知道该怎么写。
