
前两天有个同事跑过来问我SVN 怎么建分支网上教程全是讲 Git 的SVN 是不是快没人用了 我听完还挺感慨的。虽然这几年新项目基本都上了 Git但存量项目里 SVN 的使用量依然很大尤其是传统企业、外包项目、老系统维护SVN 至今仍是很多团队唯一的代码管理工具。而分支branch和标签tag恰恰是 SVN 使用中最容易混乱、也最值得搞清楚的一块。这篇东西我就用这些年维护 SVN 仓库的实操经验把分支和标签的创建、命名、合并、切换、删除这些事一次讲透不管你是刚接手 SVN 项目的新人还是在老项目里被 merge 搞到头大的老手应该都能找到能直接落地的办法。1. 先说清楚SVN 的分支和标签到底是什么1.1 SVN 的分支本质上是一次廉价的目录复制很多从 Git 转过来的同学会很不自觉地拿 Git 分支的概念去套 SVN结果一脸懵。Git 里分支只是一个指向 commit 的指针创建成本几乎为零SVN 里没有指针这个概念SVN 的分支是真实存在于仓库目录结构中的一份代码快照。创建分支这个动作在 SVN 里对应的操作就是 svn copy把 trunk 或者某个分支、标签的整体目录复制一份到 branches 下面的新目录里。但这里要重点说一句SVN 的这个复制非常便宜。它并不是把几千个文件逐个拷贝一遍而是在仓库层面做一个带完整历史的目录快照。SVN 底层会记录复制来源实际存储时共享大部分历史数据所以哪怕项目里有几万个文件创建分支也就是一瞬间的事占用的空间也远比你想象的小。你可以把它理解成给文件夹拍了一张带完整历史的照片而不是把文件夹重新抄写了一份。复制之后新分支就拥有了一条独立的版本线。你在分支上提交代码提交会记录在这个分支自己的路径下不会影响 trunk同样在 trunk 上的提交也不会自动进入分支。两个目录从此各自演进直到某天你决定用 svn merge 把两边重新对接起来。1.2 为什么约定俗成要用 trunk / branches / tags 三件套严格来说SVN 并没有强制要求你必须用这三个目录你完全可以在仓库根目录随便放代码然后在任意位置 svn copy。但是所有 SVN 教科书、所有团队经验最终都指向同一个约定仓库根目录下划分 trunk、branches、tags 三个平级目录。有的仓库还会额外加 releases 或者 archive但三件套是基础。trunk主干也就是当前的主开发线团队绝大多数日常提交都发生在这里。branches分支区。功能开发、版本发布、紧急修复、实验性尝试都从这里拉一条独立的线。tags标签区。用来给某个重要的历史版本做快照比如 v1.0.0、v2.3.1、某个上线点。为什么一定要这么分因为 SVN 的访问是基于 URL 路径的路径设计合理权限控制、备份策略、代码评审、发布拉取都会变得非常直观。比如很多公司的发布系统就是固定从 tags/ 下某个标签路径拉代码打包版本回溯、增量发布都有据可查。反过来如果团队里每个人想建分支就建在任意路径过两个月仓库就会变成一个谁都找不到北的乱摊子。另外tags 目录在语义上代表只读快照。SVN 本身没有硬性阻止你在 tags 里提交代码但你一旦在 v1.0.0 这个标签上改了代码标签就失去了可复现的意义。这也是很多团队会用 pre-commit hook 脚本强制禁止 tags 目录提交的原因后面我会专门说这个。1.3 分支、标签与版本号的关系用 SVN 的时候经常有同学被版本号搞晕为什么 trunk 还是 r123创建分支后就变成 r124 了分支提交一下又变 r125因为 SVN 的版本号是全局递增的整个仓库共享同一个版本序列任何路径的提交都会让仓库版本号 1。这意味着trunk 的某个历史版本可能是 r100而 branches/feature/xxx 里的某个文件在同一时刻也是 r100同一版本号下每个路径都有自己的快照。你在分支上提交影响的只是分支路径下的内容但整个仓库版本号会推进。打标签本质上就是把某个路径在某版本号的快照复制成 tags/vX.X.X后续所有操作都能用这个版本号精确定位。理解这一点你在合并代码、对比差异的时候就不会对着版本号犯糊涂。很多合并问题追根溯源都是因为我以为这个版本号是 trunk 的其实是整个仓库的。2. 创建分支和标签图形化与命令行实操2.1 用 TortoiseSVN 创建分支或标签TortoiseSVN 也就是大家常说的小乌龟是在 Windows 下用 SVN 最顺手的图形化工具。创建分支的路径是在工作副本目录上右键 → TortoiseSVN → Branch/tag...。弹出的窗口里有几个关键选项我逐个说一下。首先是 To path这里要填分支的目标 URL。TortoiseSVN 会根据你当前工作副本的仓库地址自动拼接一个默认路径比如当前在 trunk它会默认填 http://svn.example.com/project/branches/你在后面补上分支名就行。然后是 Create copy in the repository from这一步有单选一个是从 Working copy 复制一个是从 Repository URL 复制。这里我的建议是创建分支和标签一律选 Repository URL 方式从服务器端直接复制不要用工作副本。为什么因为工作副本里可能有一堆未提交的改动、未版本化的文件、临时编译产物虽然 TortoiseSVN 会让你提交或者忽略但这个流程特别容易把脏数据带进新分支。直接从仓库 URL 复制复制的是服务器上某个版本号的干净快照可控性高得多。窗口底部还有一个复选框Switch working copy to new branch/tag。这个就是创建后自动把当前工作副本切换到新分支。我的习惯是拉功能分支时勾选它省得再手动 switch打标签时绝对不勾选因为标签是只读的切换过去纯属给自己挖坑。2.2 用命令行创建分支和标签命令行看起来没有图形界面直观但在自动化、脚本化场景里它是唯一的方案。而且用熟了以后命令行创建分支和标签反而更快、更不容易点错。创建功能分支svn copy ^/trunk ^/branches/feature/user-center -m open feature branch for user center这里有个关键符号^/ 表示仓库根目录。在 SVN 1.5 之后的命令行客户端里这个写法在工作副本内可以直接使用省去敲一长串 http:// 地址的麻烦。执行完成后服务器上就有了一个新分支。打标签的命令几乎一样svn copy ^/trunk ^/tags/v1.0.0 -m tag v1.0.0 for release切换工作副本到分支svn switch ^/branches/feature/user-center切换之后用 svn info 看一眼URL 应该已经变成分支路径版本号也会指向分支最新的快照。这里我特别提醒一句svn switch 不是 svn checkout。switch 是在同一个工作副本里切换路径本地未提交的修改会被保留并尽可能带过去checkout 是重新拉一份全新的工作副本。日常开发应该依赖 switch而不是反复 checkout否则本地改了一半的代码很容易丢。2.3 分支与标签的命名规范这一小节看起来不起眼但分支管理乱不乱八成取决于命名规范。我的经验是在仓库建好第一天就要定规矩宁可多写几个字不要省事。比较通用的一套规则是这样的功能分支branches/feature/功能名比如 branches/feature/wechat-login。缺陷修复分支branches/bugfix/问题单号-简述比如 branches/bugfix/CRM-1234-order-price。紧急修复分支branches/hotfix/问题简述这种分支一般从发布标签拉出来修完立刻合并回主干和当前发布分支。发布分支branches/release/版本号比如 branches/release/2.3.0。标签tags/版本号推荐用语义化版本。比如 tags/v1.0.0、tags/v2.3.1。命名里尽量避免空格、中文、特殊符号路径中一旦出现这些字符脚本处理起来非常痛苦。也不要用纯日期当标签比如 tags/20240601这种名字完全看不出版本含义两个发布撞在同一天时更是灾难。更不要用 test、tmp、new 这种无意义的名字过三个月你自己都分不清里面是什么。2.4 打标签前一定要确认干净打标签这个动作看起来就是一次 svn copy但实际踩坑的人特别多。最大的坑就是用工作副本打标签时把未提交的局部修改或者未版本化文件一并带进了标签。虽然从功能上 svn copy 工作副本到 URL 需要你提交一个操作但很多人会在这种随手操作里忽略文件差异最后发布的代码和当时测试的代码根本不是同一份。所以我给自己定了一条死规矩打标签之前先 svn update 到目标版本然后 svn status 确认工作副本干净最后再用 Repository URL 方式复制。发布流程里标签必须对应服务器上某个确切的版本号而不是对应某个开发者的本地目录。还有一个细节打标签的 commit message 里建议写上来源路径和版本号比如 tag v1.0.0 from trunk r1234。这样将来任何人翻日志都能立刻知道这个标签是从哪里来的。3. 分支开发与合并回主干完整流程3.1 一个功能分支的典型生命周期真正到实际项目里分支不是建完就完事了而是要跑完一整个生命周期。拿一个比较典型的功能分支举例流程大概是这样的从最新主干拉分支保证起点是干净且最新的svn update ^/trunk svn copy ^/trunk ^/branches/feature/user-center -m open user center feature branch svn switch ^/branches/feature/user-center在分支上开发提交代码。这个阶段要注意如果开发周期超过一两天要定期把主干的最新修改同步到分支上避免分支和主干偏差太大最后合并时冲突多到怀疑人生。同步命令很简单svn merge ^/trunk这条命令会以分支当前版本为基础把 trunk 上还没合并进来的修改应用到分支工作副本然后你需要 review、解决冲突、提交。同步合并不是可选项而是分支开发里最容易被忽视的一环。功能开发完成测试通过后把分支合并回主干svn switch ^/trunk svn merge --reintegrate ^/branches/feature/user-center老版本 SVN 里从分支合并回主干推荐用 --reintegrate 参数因为它会智能计算分支上相对于主干的所有修改只把这些修改合并进主干避免把主干的历史重复应用一遍。SVN 1.8 以后多数情况下直接 svn merge 也能借助 merge tracking 做对但 --reintegrate 这个习惯并不算过时。合并提交后清理分支svn delete ^/branches/feature/user-center -m feature merged to trunk, remove branch已经合并完的分支没必要继续留在仓库里及时删除能让 branches 目录保持清爽。删除并不代表丢历史SVN 会完整保留这个分支的所有版本记录需要时仍然可以恢复。3.2 同步合并与回合并方向千万别搞反SVN 开发里有两个合并方向方向搞反是最常见的事故源头。第一个方向是主干合并到分支这叫同步合并。目的就是把主干上的新修改同步到分支里让分支保持较新的代码基线。这是开发期高频操作一般从分支创建的第二天就可以开始了。第二个方向是分支合并到主干这叫回合并。功能做完了把分支上的成果带回到主干。这一步通常发生在分支维护的末期频率低但风险最高因为它可能把分支上积累的大量修改在一个提交里灌进主干。这两个方向不能混。有些同学在分支上开发到合并那天直接在主干上执行 svn merge ^/trunk结果就是把主干合并到主干完全没作用还有人在主干上 svn merge ^/branches/feature/xxx 没问题但换到分支上又习惯性执行同一句结果把分支又合并到了分支浪费大半天。我的建议是合并操作执行前先 svn info 看清当前 URL再手指敲命令。宁可慢一分钟也不要在一个错误目录里 merge 完才发现提交错了地方。3.3 merge tracking 与 svn:mergeinfo 属性SVN 1.5 引入了一个非常重要的功能merge tracking合并跟踪。它通过在目标路径上维护一个 svn:mergeinfo 属性记录哪个来源路径的哪些版本已经被合并过来了。这个属性是 SVN 能自动避免重复合并、能智能计算差异的基础。你可以用下面的命令查看一个路径的 mergeinfosvn propget svn:mergeinfo .输出会显示类似这样的内容/branches/feature/user-center:r1200-1350 /branches/feature/wechat-login:r1400-1420意思是当前路径已经合并了 user-center 分支的 r1200 到 r1350以及 wechat-login 分支的 r1400 到 r1420。下次再执行 svn merge 时SVN 会参考这个属性只合并尚未合入的版本范围。mergeinfo 是个好东西但它也带来了一个著名的坑一旦有人手动删除了 svn:mergeinfo 属性或者从主干复制成分支后把 mergeinfo 清掉SVN 就失去了合并跟踪的依据后续合并可能把已经合过的修改再次应用一遍产生大量重复冲突。这种问题排查起来非常痛苦因为表面上看代码没问题但一步合并就全是冲突。我的经验是别轻易手动修改 mergeinfo如果发现 mergeinfo 确实坏了优先考虑通过反向合并修正而不是直接删除属性。具体情况后面第 5 节我再展开。3.4 多分支并行时的合并节奏项目大了以后往往不会只有一个分支。最常见的组合是主干、一到两个功能分支、一个发布分支。这种情况下合并节奏要尽量规律化。我比较习惯的节奏是每天上班先 svn update 主干然后切到自己的功能分支执行一次 svn merge ^/trunk把主干最新代码拉到分支。这样主干和分支的差异始终控制在一定范围内合并日的冲突量会小很多。发布分支则遵循另一个逻辑功能分支稳定后先合主干从主干拉发布分支测试通过打 tag修完紧急 bug 后把修复合并回主干。这种多分支并行模式里最重要的原则是保持单一事实来源。主干是当前主开发线发布分支只接收必要的 bug 修复功能分支只做自己的功能。如果让发布分支同时承载新功能开发那合并关系会迅速乱成蜘蛛网。4. 在 IDEA 里操作 SVN 分支4.1 IDEA 集成 SVN 的配置IDEA 自带 Subversion 集成但前提是本地已经安装了 SVN 命令行客户端。很多人遇到IDEA 里找不到 SVN 选项的问题原因基本都是没装命令行工具。我一般推荐装 SlikSVN 或者 VisualSVN装好之后打开 IDEA 的 Settings → Version Control → Subversion在 Path to svn executable 里指定 svn.exe 的路径测试一下连接即可。配置完之后IDEA 会在 VCS 菜单里出现 Subversion 相关选项右键项目根目录也能看到 Subversion 的子菜单。这个集成基本上覆盖了日常 90% 的操作包括更新、提交、查看历史、切换、合并。4.2 切换分支与标签在 IDEA 里切换分支有几种方式。最直接的是右键项目根目录 → Subversion → Update Directory在弹出的对话框里把 URL 改成目标分支或标签的地址。这个操作本质上就是 svn switch执行前 IDEA 会检查本地是否有未提交的修改。这里我要强调一个非常重要的事项切换分支前本地未提交的修改要先处理。IDEA 的 switch 并不是拒绝执行而是会尝试把本地修改带到目标路径上。如果两个路径对同一个文件做了不同修改switch 可能直接把工作副本搞到冲突状态那比提交后再切换麻烦得多。我的习惯是有未提交修改时先 commit或者用 IDEA 的 Shelf 功能暂存切换后如果有必要再恢复。还有一个操作很多人不熟IDEA 的 VCS → Subversion → Branches 面板可以集中查看 trunk、branches、tags 下的所有路径右键可以执行创建分支、切换、合并等操作。这个面板在操作频繁的时候效率很高尤其是浏览分支列表时。4.3 在 IDEA 中执行合并IDEA 的合并操作路径是右键项目根目录 → Subversion → Merge from...弹窗里填写来源 URL 和要合并的版本范围。IDEA 会把合并结果也放在同一个对话框里有冲突时可以直接在 Three-way Merge 界面里逐文件处理。合并后的代码要特别注意 Review。IDEA 会把所有改动文件显示在本地变更列表里我建议在提交前逐个 diff 一遍确认没有把不该合过来的东西带进来。合并完成但还没提交时如果发现合并方向错了可以用 IDEA 的 Local History 或者直接 revert 掉改动然后重新合并。还有一个小技巧IDEA 的日志视图里能查看文章级的版本历史配合 Revision Graph 可以非常直观地看到分支、标签、合并之间的关系。平时做 SVN 分支操作之前我会先打开 Revision Graph 看一眼整体拓扑确认自己接下来要动的分支确实是从正确的节点拉出来的。5. 常见问题排查与避坑实录5.1 合并时冲突糊脸怎么办SVN 合并最让人头皮发麻的就是冲突。文件状态变成 C意味着左右两个版本都改了同一处地方SVN 无法自动决定保留哪个。处理冲突的大原则只有一个先看清楚两边的差异再动手。命令行下svn status 会列出所有冲突文件你需要一个个打开手工选择保留我的还是他们的或者直接编辑成想要的最终内容然后用以下命令标记为已解决svn resolve --accept working path/to/conflict/fileTortoiseSVN 里可以右键冲突文件 → Edit conflicts在三方合并视图里看 主干版本、分支版本、合并结果 三栏逐条解决。IDEA 里类似Merge 对话框里可以直接在左右版本之间选择。这里分享几个实际经验第一合并前先把当前工作副本的未提交修改全部提交掉避免冲突出现时把新旧修改混在一起到时候连哪个是哪次改的都分不清。第二冲突解决不是把所有他们的代码保留就算完而是要理解两边的意图特别是当主干和分支都在同一个模块里加过功能时两个功能往往需要同时保留不是简单的二选一。第三冲突文件多的时候别硬扛先把文件列表截图发到群里问问团队最近谁动过这块代码往往更快。5.2 mergeinfo 混乱导致重复合并、漏合并mergeinfo 混乱是 SVN 老用户最常见的疑难杂症。症状通常是明明这个分支已经合并过了再合并时又把老代码重新应用一遍或者反过来该合并的改动始终不出现。排查的第一步是看 mergeinfosvn propget svn:mergeinfo .如果这个属性包含了奇怪的路径或者路径范围明显不合理那就是它出了问题。最常见的情况是有人从主干拉了一个分支然后把分支里的 svn:mergeinfo 属性手动删除了。之后合并时会彻底失去方向感因为 SVN 不再知道主干哪些改动已经带过来了。处理方式上如果 mergeinfo 只是局部错误可以用 svn merge -x --ignore-ancestry 强制忽略祖先关系来完成某一次合并但这是治标不治本。如果混乱严重我个人的做法是把分支的 svn:mergeinfo 属性重新设置成正确的基线然后立刻做一次完整的同步合并把主干所有应合未合的改动一次性拉齐再提交。这样后续的合并跟踪就恢复正常了。当然这一套操作越到后期越复杂。最好的策略其实是预防要求所有人在分支合并后不要手动修改 mergeinfo 属性也不要为了省事加 --ignore-ancestry。5.3 删除分支、恢复分支与清理工作副本分支合并完成后要删除这个前面说过了svn delete ^/branches/feature/user-center -m feature merged但如果删错了怎么办不要慌因为 SVN 是保留历史的。你可以通过两个方式找回一种是根据删除前的版本号直接复制回来svn copy -r 版本号 ^/branches/feature/user-center版本号 ^/branches/feature/user-center -m restore deleted branch另一种是直接用 TortoiseSVN 的 Show log 找到删除提交这个分支的那条记录然后 Revert 这条删除操作。找回之后注意这个分支的 mergeinfo 还是删除前的状态要继续使用前建议先和主干做一次同步合并更新到最新状态。还有一个特别常见的问题工作副本出现 working copy locked 之类的提示。这通常是上次 SVN 操作中途被杀掉或者异常退出导致的。解决办法是执行 svn cleanup让它清理掉残留的锁和临时状态。如果 cleanup 都没效果那就只能把出问题的目录重新 checkout 一份手动把本地修改备份出来再覆盖回去。这种手段要谨慎但确实是最后一道保险。5.4 分支合并后主干还是旧代码这个问题的发生频率高得离谱而且大部分时候不是 SVN 出问题而是操作人的问题。合并是一系列动作不是单个动作。svn merge 只是把差异应用到工作副本之后必须显式提交改动才会真正进入主干。很多人在分支上合并完主干就直接切回主干看代码发现主干还是旧代码以为合并失败了其实只是没有 commit。另一个可能原因是合并方向选错了。比如在分支工作副本上执行了 svn merge ^/branches/feature/xxx合了半天还是在分支自己身上合自己自然主干不会变。这种错误我见过不止一次解决方案只有一个执行之前先 svn info看清自己站在哪条路径上。5.5 切换分支时本地修改被带过去svn switch 的行为和 Git checkout 不太一样。Git 在没有处理干净本地修改时会直接拒绝切换SVN 则会把本地未提交的修改尽力带到新目标上。这个特性在某些场景下是便利但在更多场景下是陷阱。举个例子你在分支 A 上改了 a.java此时切换到分支 B如果分支 B 的 a.java 和分支 A 的 a.java 相同那么你的修改会被直接沿用如果两边对 a.java 的改动产生冲突switch 会报错告诉你本地某个文件状态冲突。处理起来很麻烦。所以我的建议是切换分支前要么把未提交修改先提交到当前分支要么用 IDEA 的 Shelf 功能或者手动备份文件反正不要带着一堆本地修改来回切。SVN 不是做不了是这么做特别容易被带过去的修改误导最后把不该提交的文件提交到错误的分支上。6. 我这么多年用下来的几条心得SVN 分支和标签管理说到底不是什么高深技术它的原理一遍就能讲清楚。但为什么很多团队还是用得一塌糊涂我觉得问题出在约定和习惯上而不是工具本身。我的第一个心得是分支命名必须规范而且要从第一天定死。你可以不理解 merge tracking 的底层实现但你一定不能连 branches 下面哪个分支是干嘛的都说不清楚。命名规范不是形式主义它是降低协作成本最廉价的手段。第二个心得是标签永远只读。凡是发现有人往 tags 里提交过代码我建议第一时间把这个提交 revert 掉并清查一遍发布流程因为这说明发布环节存在漏洞标签已经无法保证可复现了。与其事后补救不如在 pre-commit hook 里直接禁止对 tags 目录的提交一劳永逸。第三个心得是关于合并粒度的。一次合并只做一件事不要把多个功能分支或者多个版本的改动混在一次 merge 里完成。合并粒度越粗出问题时排查的成本越高。而且每次合并的 commit message 里我都会习惯性地写明来源分支和版本范围比如 merge from branches/feature/user-center r1200-1350。几个月后再翻日志这份记录能救命。第四个心得是关于自动化。SVN 虽然老但它的命令行接口非常稳定非常适合脚本化。我现在维护的老项目已经把从主干拉发布分支、打标签、合并回主干这些常规操作全部写成了 shell 脚本。新人来了不需要死记命令跑脚本就能完成全套发布动作出错率大大降低。如果你所在团队经常发布我强烈建议也做一套类似的脚本。最后再说一个我踩过好几次的坑工具层面的操作可以练熟但团队约定才是真正的核心。你可以在 TortoiseSVN 和 IDEA 里把各种操作玩出花但如果仓库里 branches 乱成一团、tags 被乱改最后受苦的还是全体开发者。SVN 确实不如 Git 时髦但把分支和标签这套逻辑吃透你在任何版本管理工具上都会受益。