
记得我第一次用 Git 切分支切到一半整个人是懵的在 dev 分支上改了三天的功能leader 说线上有个紧急 Bug让我切到 master 看一眼。我随手git checkout master打开项目一看文件内容全变了目录结构都少了几块。那一瞬间我甚至怀疑自己代码写丢了一部分。其实不是代码丢了而是我根本不理解分支到底是什么。当时的我以为分支就是代码的两个副本文件夹切分支就是从一个文件夹跳到另一个文件夹。后来我才明白Git 分支本质上只是一个 41 字节的文件——一个指向某次提交的指针。你切换的从来不是文件夹而是版本世界的坐标。这篇文章我不想堆砌命令大全而是围绕分支到底是什么 → 怎么建怎么切 → 怎么合并 → 冲突了怎么处理 → 多人在一个仓库协作时怎么约定工作流这条实际开发链路展开。内容主要基于我自己在项目里踩过的坑和用顺手的套路适合刚接触 Git 不久的开发者也适合那些会git commit但一遇到分支合并就头皮发麻的人。1. 分支不是文件夹是指针先修正对分支的底层认知1.1 三个核心文件帮你理解 Git 的分支机制如果你打开项目的.git目录会看到HEAD、refs/heads/、objects/。这三个东西就是理解分支的关键。refs/heads/目录下每个分支名对应一个文件文件内容是一串 40 位的 SHA-1 哈希值指向该分支最新的那次提交。比如refs/heads/dev文件里面存着某个 commit 的哈希这就是dev 分支的全部含义。HEAD是一个当前在哪条分支的指针文件。它的内容通常是ref: refs/heads/dev意思是当前 HEAD 指向 dev 分支。objects/目录保存了所有提交对象、树对象和数据对象也就是每次提交产生的真实快照内容。所以创建分支根本不是复制一份代码而是在refs/heads/下多写一个文件。这也解释了为什么git branch创建分支几乎是瞬间完成的——它只是写了一个 41 字节的引用文件。1.2 切换分支的本质移动 HEAD 并同步工作区git checkout dev这个操作底层做了两件事修改HEAD让它指向refs/heads/dev然后根据 dev 分支最新提交的快照更新你工作区的文件内容。如果你在当前分支有未提交的修改而切过去的目标分支里对应文件内容又不一样Git 就会拒绝切换提示你先提交或储藏。理解了这一点你就知道为什么切分支速度极快却让人觉得文件翻天覆地。文件变化大是因为两个分支指向的提交差异大切得快是因为 Git 只是更新指针和工作区而不是真的搬运大量文件副本。这个认知直接决定了后面合并策略的理解深度。1.3 为什么分支成本低是 Git 协作设计的核心Git 里分支的创建、切换、删除成本都非常低所以它鼓励你为每个功能建分支、大胆开分支、频繁合分支。这跟 SVN 那种分支等于目录拷贝的设计完全不同。我自己刚转 Git 时还带着 SVN 的习惯轻易不开分支怕分支多了乱、合并成本高。后来被带我的同事纠正过无数次才慢慢改成一个功能一条分支的做法。现在回头看这个习惯转换是效率提升最大的一步。2. 本地与远程分支的日常操作创建、切换、推送、删除2.1 命令行操作从那句最常用的git checkout -b说起创建分支并切换最常用的是git checkout -b feature/login。这条命令等价于git branch feature/login加git checkout feature/login。后来 Git 2.23 版本开始推荐使用git switch和git restore把切换分支和还原文件两个语义拆开。创建并切换的对应命令是git switch -c feature/login我个人的建议是新项目、新环境直接学git switch语义清晰不容易把分支切换和文件还原混在一起。但老项目里checkout -b依然随处可见你至少得看得懂别人在做什么。切换分支之前有个特别重要的习惯先确认工作区是干净的。git status输出里如果出现nothing to commit, working tree clean就可以放心切换到任意分支。如果有未提交的修改要么提交要么用git stash临时储藏。2.2 小乌龟TortoiseGit视角右键菜单背后的逻辑很多 Windows 开发者习惯用小乌龟因为右键菜单比敲命令直观。小乌龟里切换分支的操作是在项目目录右键 → TortoiseGit → Switch/Checkout → 在 Branch 下拉框选目标分支。这里有个容易被忽略的地方小乌龟的 Switch/Checkout 对话框里有一个 Create new branch 的选项。很多人想在远程分支基础上新建本地分支结果直接在远程分支上开发一直往远程分支提交这是很常见的工作流事故。正确做法是先切到远程分支或基于它创建本地分支再新建自己的功能分支。在小乌龟里勾上 Track 选项可以建立本地分支与远程分支的跟踪关系后面pull、push就省心很多。2.3 删除分支的细节为什么有时删不掉、远程分支怎么删删除本地分支的命令是git branch -d feature/login这里的-d会做安全检查如果这个分支的提交没有被合并到当前分支Git 会拒绝删除提示not fully merged。如果确定这个分支不要了用-D强制删除。远程分支的删除命令则不同git push origin --delete feature/login还有一个常见场景是你在本地删了分支但远程还留着其他同事的客户端里还能看到。为了让远程仓库彻底清理需要执行上面的删除命令。对使用小乌龟的同学删除逻辑完全一致右键 → TortoiseGit → Delete Branch注意本地分支和远端分支要分别选择。这东西不难难的是很多人从没注意过-d和-D的区别等误删了才发现根本没有后悔药。说真的-D这种强制参数还是等确实想清楚再敲。3. 合并分支的两种策略merge 与 rebase 的取舍3.1 Fast-forward 与真正的分叉合并合并分支最基础的概念是 Fast-forward快进合并。假设当前在 master 分支创建了 feature 分支并提交了两次master 分支期间没有产生新的提交。此时把 feature 合并回 master会看到Fast-forward提示git checkout master git merge feature因为 master 从创建 feature 分支后就一直没动过所以 Git 只需要把 master 的指针直接往前推到 feature 的最新提交就行不需要生成新的合并提交。历史是一条直线。但如果 master 也有新的提交两条分支就分叉了。此时合并时Git 会把两个分支的修改做整合并生成一个额外的 Merge commit合并提交历史会呈现分叉再汇合的形状。3.2 merge 和 rebase 分别改写了什么这是一个必须掰开揉碎讲清楚的点。git merge feature的语义是把 feature 分支的改动并入当前分支产生一个新的合并提交双方的历史都保留。优点是安全、不篡改历史适合合并主干和功能分支缺点是提交历史会出现分叉一旦分支多、合并频繁图中会很像一张蜘蛛网。git rebase feature的语义完全不一样它把当前分支的提交重新播放到 feature 分支的最新提交之上当前分支的历史会线性延伸。举个例子你在 feature 分支上提交了三次执行git checkout feature git rebase masterGit 会先把 feature 分支的基点从原来的旧 master 快照移到 master 的最新提交上然后依次重放你的三次提交。从结果看feature 分支仿佛是从 master 的最新位置一路直线开发的非常干净。但代价是三次提交的哈希值全部变了因为父提交变了整个提交链的指纹就变了。3.3 不同场景下的选择建议我之前带过一个小团队定了两条简单规则基本上没有因为合并策略吵过架功能分支合回主干的场景用 merge保留合入记录方便出问题时回溯。个人开发的功能分支想同步主干的最新代码时用 rebase保持自己这半边历史整洁。唯一的铁律绝不 rebase 公共分支。因为 rebase 会改写提交哈希如果你把一个别人也在用的分支 rebase 了别人再 pull 就会看到完全不同的提交链直接冲突到怀疑人生。这个坑我踩过一次差点把同事的本地提交搞丢从那以后我们团队就把这条写进了 Git 约定。4. 冲突解决的完整链路从冲突产生到可视化处理4.1 冲突到底是怎么产生的三路合并机制很多新手以为只要有两个人改了同一个文件就会冲突其实没那么糙。Git 合并采用的是三路合并three-way merge机制基于两个分支的共同祖先提交merge base、当前分支的最新提交、被合并分支的最新提交三方对比。只有当两个分支都对同一文件的同一处区域做了不同修改时Git 不知道你想保留哪边才会产生冲突。如果一个人改了文件第 1 行另一个人改的是第 50 行Git 能自动合并完全不需要你插手。理解了这一点你就明白了冲突不是刻意找人麻烦而是 Git 确实没有能力替你拍板这两份改动到底哪个算数。这就像两个人同时改了一份合同一个把付款期限改成了 15 天另一个改成了 30 天你总不能怪合并工具太笨吧。4.2 冲突标记详解、、分别代表什么合并产生冲突时Git 会把冲突内容直接写进文件并用特殊标记圈出来。拿一个真实的例子说 HEAD function calculatePrice(price, discount) { return price - price * discount; } function calculatePrice(price, discount) { return price * (1 - discount); } feature/new-price HEAD到之间当前分支HEAD 指向的分支的版本。到 feature/new-price之间被合并进来的 feature 分支的版本。你需要做的不是保留某个标记而是把整个冲突块包括标记符号改成一个最终版本然后保存文件。很多新手犯的错是只删了标记保留了其中一边内容但忘了检查逻辑是否完整。还有的人把两个版本都保留了结果代码逻辑出问题。正确操作是把你真正想要的代码留下标记全部删干净。4.3 命令行解决冲突的完整步骤命令行解决冲突流程很固定git merge feature/login如果出现冲突先看哪些文件有冲突git status有冲突的文件会用both modified标出来。编辑这些文件把冲突块改成最终版本。全部改完后git add 冲突文件 git commit注意这时候的 commit 不需要再写-mGit 会打开一个编辑器里面是默认的合并提交信息。如果你用的是 Vim:wq退出即可。如果不想编辑器弹出来可以直接git commit --no-edit如果是 rebase 过程中遇到冲突操作略有不同。rebase 会停在某个提交上让你解决完冲突后执行git add 冲突文件 git rebase --continuerebase 会把剩余的提交继续重放。这个流程很容易忽略一点rebase 过程中你可能会连续解好几次冲突因为每个提交的重放都有可能遇到新的冲突要有心理准备。4.4 用可视化工具解决冲突更高效虽然命令行能解决冲突但当冲突文件很多、冲突区域很大时光标在标记之间来回挪是非常低效的。我用过的几个方案里体验较顺的如下IDEA 自带 Diff Merge冲突文件上右键 → Git → Resolve Conflicts左右两栏分别是三个版本中间是合并结果。可以一键接受左边接受右边也可以手动编辑中间区域。IDEA 还支持逐行对比和检查是目前我用过最顺手的方案之一。小乌龟的 Edit Conflicts右键冲突文件 → Edit Conflicts会调起外部合并工具。小乌龟默认可以用 Beyond Compare、KDiff3 等在设置里配置好路径就能用。VS Code冲突标记会以彩色高亮块展示顶部有Accept Current ChangeAccept Incoming ChangeAccept Both Changes的按钮对轻度冲突来说效率很高。4.5 合并到一半发现不对怎么安全退出执行git merge feature时如果冲突太多或者你发现合并思路有问题可以随时退出git merge --abort这个命令会把工作区恢复到合并之前的状态干净利落。rebase 对应的是git rebase --abort。这个操作对小白很友好很多人不知道合并是可以反悔的硬着头皮解完一堆冲突后才发现自己根本搞不清哪个代码是对的完全是给自己找罪受。记住合并随时可反悔这个原则心态会稳很多。5. 分支管理的工作流约定让多分支协作不再是灾难5.1 命名规范与分支生命周期我现在习惯的命名规则很简单主干分支固定为main永远保持可发布的稳定状态。功能分支feature/订单模块、feature/user-login用斜杠切分类型和描述。修复分支bugfix/登录超时处理线上的紧急问题。发布分支release/1.2.0准备发版的收敛期使用。这套命名规则的好处在于一个仓库里你看到分支名不用点进去就知道它是干什么的、生命周期长还是短。配合git branch --list feature/*这样的通配符还能快速筛选分支非常实用。5.2 合完就删定期清理失效分支很多团队的分支越积越多根因是合并完不删。我在实际项目里的规则是功能分支合并回 main 后本地分支和远程分支一起删。git checkout main git pull git branch -d feature/订单模块 git push origin --delete feature/订单模块如果你发现本地有一堆远程已经删掉了的分支残留可以定期清理git fetch --prune--prune会把本地跟踪的、但远程已不存在的分支引用删掉。这个命令最好固定成每周一跑一次不然时间久了本地分支列表能给人看花眼。5.3 最容易翻车的两个操作未提交就切分支、rebase 公共分支未提交就切分支是我在团队里被问过最多次的翻车场景。比如你在 dev 分支改了一堆代码突然想看看 main 分支上的文件夹结构直接git checkout main然后 Git 就给你一堆报错。正确的做法是git stash git checkout main # 完事后回到 dev git checkout dev git stash popgit stash会把当前工作区的未提交修改临时储藏起来工作区恢复干净。切回 dev 后再stash pop拿出来。还有一次我手滑在公共分支上执行了git rebase结果同事 pull 的时候看到整个分支历史重写了吓得以为代码被黑客改了。从那以后我深刻记住一个原则rebase 只用于你个人的分支绝不用于公共分支。如果团队里确实需要共享分支保持线性历史可以选择合并时用 rebase 方式也就是git pull --rebase这种方式只影响你本地不会改写公共远程分支。6. 踩过的坑与保命技巧我从实际项目中总结的几条经验6.1 工作区有未提交修改时不要盲目 checkout开头我讲过那个切分支后代码消失的恐惧。实际上 Git 如果检测到目标分支与当前分支在同一文件上有不同修改是会拒绝切换的并不会让你丢代码。但如果两边的文件互不冲突Git 会把未提交的修改带过去你切到新分支后会发现工作区还带着这些改动。这个隐性携带行为有时候会让人困惑明明切了分支为什么还残留着上一个分支的改动所以我现在的工作习惯是切分支之前必须git status哪怕只是加了一行日志也要明确它该属于哪个分支。该 stash 的 stash该提交的提交。磨刀不误砍柴工这几秒钟能省掉后续无数排查时间。6.2 误删分支的后悔药reflog 找回你以为git branch -D删掉分支就彻底没救了吗其实 Git 有一个后悔药机制叫 reflog。执行git reflog可以看到 HEAD 所有的历史移动记录包括被删除分支最后一次指向的提交哈希。找到那个 hash 后直接用下面的命令恢复git branch 分支名 提交哈希有一次我不小心删了一个还没合并的功能分支当时吓得手心冒汗后来靠 reflog 三分钟就找回来了。从这之后我虽然还是会删分支但至少心里知道删错了十有八九能捞回来操作心态稳了不少。6.3 团队协作里比命令更重要的沟通约定最后说点不太技术但特别实在的东西。有一次我们两个人同时在一个功能分支上开发互相都有未推送的提交结果每天上班第一件事就是解决各种冲突。后来改成功能分支只属于一个人的约定冲突数量立刻大幅下降。分支管理从来不只是命令的问题更是团队协作边界的问题。如果每个人都独立维护自己的功能分支合并 code review 后及时删分支这个仓库基本不会乱到哪去。反之即使你把所有命令背下来也挡不住混乱的工作流天天产生冲突。我自己用了 Git 快八年最大的体会是合并冲突其实不是坏事它在强制你和其他人做一次有效沟通——你看看我改了什么我看看你改了什么两边对齐之后再交付。这个过程虽然麻烦但比各写各的、最后集成出一堆烂账要舒服得多。把这些基础逻辑搞明白后面再玩工作流、子模块、复杂历史改写都不会再慌。