进公司第一天,leader丢给你一个SVN地址;回工位坐下,你习惯性地敲下git init,然后开始纠结一个问题:这两套版本控制工具,到底能不能在一台电脑上和平共处?
答案是不仅能,而且对很多人来说,这是日常工作的标准操作。我见过大量团队,公司主仓库是SVN,程序员自己私下维护Git仓库做本地版本管理;也见过团队从SVN迁移到Git,但在过渡期必须双轨并行好几个月;还有人要同时维护公司SVN项目和开源Git项目,电脑上两种工具整天换来换去。这篇文章就把这种“同时使用SVN和Git”的完整玩法讲透,包括安装配置、日常协作流程、git-svn桥接、仓库迁移,以及一堆我踩过坑之后才总结出来的注意事项。
1. 为什么需要同时使用SVN和Git:几个真实工作场景
先说个结论:SVN和Git并不是“有你没我”的对手关系,它们是两套独立的版本控制实现,底层数据结构完全不同,安装在同一台机器上根本不会冲突。真正需要想清楚的,是你到底处于哪种工作场景,以及两种工具各负责哪一段流程。
1.1 三种最典型的“双仓库”需求
第一种,公司强制SVN,个人习惯Git。这种在传统企业、银行、外包项目里太常见了。服务端的代码仓库由运维统一管理,权限、审计都要走SVN,你没有选择权。但你习惯用Git做本地提交、随时回滚、开分支实验,那就完全可以在SVN工作副本里再初始化一个Git仓库,让Git当本地的“后悔药”。
第二种,团队正处于SVN向Git迁移的过渡期。规划好了新仓库地址,但业务还在跑,不可能停下手头工作“搬迁”一天。常见做法是:代码继续提交到SVN,同时用git-svn把SVN仓库实时镜像到本地Git仓库,等团队成员陆续切换过来,最后彻底关掉SVN。这个过程可能持续几周到几个月,双轨是唯一选择。
第三种,多个项目本身就用了不同的版本控制工具。开源项目用Git,客户要求源码托管在公司的SVN服务器;自己接的私活用Git,公司项目用SVN。这种情况没有技术包袱,就是两种工具都要熟练操作,切换项目时别把命令记混就行。
1.2 两个工具能共存的前提:底层数据结构不冲突
很多人担心“同一个目录里既有.svn又有.git会不会打架”,答案是基本不会,前提是你别乱改对方的元数据目录。SVN在工作副本里每个目录都会生成一个.svn文件夹,Git则是在仓库根目录生成一个.git文件夹,两者是独立存在的。
但从数据模型上,两者的设计哲学完全不同,这也决定了它们适合的协作模式:
| 维度 | SVN | Git |
|---|---|---|
| 架构 | 集中式,所有历史在服务器 | 分布式,每个克隆都是完整仓库 |
| 版本号 | 全局递增数字(r1、r2…) | 40位哈希值 |
| 提交体验 | 大多数操作需要联网 | 本地提交毫秒级完成 |
| 分支/标签 | 目录拷贝成本高,合并靠svn merge | 分支是轻量指针,合并有完整三方合并算法 |
| 历史记录 | 单线为主,改名/移动后日志易断裂 | 有向无环图,任何操作可追溯 |
| 空目录 | 可以纳入版本控制 | 默认不跟踪空目录 |
| 离线操作 | 基本不能 | 可以完整使用 |
理解这层差异,你就知道为什么太多人“用着SVN却不甘心”——不是SVN不好用,而是它解决不了本地快速迭代、安全回滚、离线工作这些需求。而Git又恰恰在落地时碰到了企业服务器的管理习惯问题。两者共存,本质上是各取所长:SVN负责与团队同步,Git负责个人代码管理。
2. Windows环境下的安装与配置:让SVN和Git和平共处
如果要在一个干净的环境里把两套工具都装好,并且让IDEA、VSCode、Visual Studio都能正确识别,有几个安装细节必须提前注意,否则后面会遇到一堆莫名其妙的问题。
2.1 安装TortoiseSVN时最容易漏掉的一项
TortoiseSVN(俗称“小乌龟”)是Windows上最主流的SVN客户端。下载地址是官网TortoiseSVN.net,安装包本身不大,一路Next即可。
容易漏掉的一步:安装进行到“Select Additional Tasks”页面时,默认没有勾选command line client tools这项。如果你只把TortoiseSVN当右键菜单工具用,不勾选也没关系;但如果你要在IDEA里配置SVN、或者想用svn命令行,就必须勾选。IDEA、Visual Studio等IDE集成的SVN本质上调用的是svn.exe,而不是TortoiseSVN的右键菜单。
安装完成后如果发现没装命令行工具,无需卸载重装,直接重新运行安装包,选择“Modify”,勾上该组件即可。装完以后,svn.exe位于C:\Program Files\TortoiseSVN\bin\svn.exe,这个路径在IDE配置时会用到。
小乌龟的汉化包(中文语言包)建议同步安装,安装后在Settings -> Language里切换语言。特别注意:语言包版本必须和主程序版本完全一致,否则无法识别。
2.2 Git安装:PATH、换行符、中文路径一个都不能少
Git for Windows的安装包从git-scm.com下载,安装过程中的几个选项很多新手会直接点下一步,但我建议按下图思路处理:
- Select Components:勾选
Git Bash Here、Git GUI Here,这两个右键菜单入口平时太好用了。 - Default branch name:建议选
main,新仓库默认分支名是main,现在各大Git平台都支持。 - Adjusting your PATH environment:务必选
Git from the command line and also from 3rd-party software,这个选项会把git.exe自动加到系统PATH,否则在CMD、PowerShell里敲git会提示“无法将git识别为cmdlet”之类的错误。 - Checkout Windows-style, commit Unix-style line endings:这是默认选项,一般保持默认即可。但对SVN和Git混用的目录,我后面会专门讲行尾符的坑。
装完后打开Git Bash验证一下,运行:
git --version git config --global user.name "你的名字" git config --global user.email "你的邮箱"用户信息不配置的话,Git提交会报黄字警告,而且历史里的作者信息会乱。这一点和SVN不同,SVN通常直接使用系统登录名,Git必须显式配置。
顺手做两件事:一是执行git config --global core.quotepath false,解决中文文件名在Git log里显示成八进制转义序列的问题——如果你搜过“git -c core.quotepath=false”,应该明白我在说什么;二是确认凭据管理器可用,这个放到第5章讲免密时详细说。
2.3 IDE里的SVN与Git协调配置
现在主流IDE几乎都内置了Git支持,SVN则要分情况处理。
IntelliJ IDEA(包含Android Studio):Settings -> Version Control -> Subversion,在Path to svn executable里手动指定C:\Program Files\TortoiseSVN\bin\svn.exe,然后点Test确认。Git配置在Settings -> Version Control -> Git,系统通常会自动检测到git.exe。IDEA支持一个项目里同时存在多个VCS,你可以在Settings -> Version Control里看到当前被识别的目录映射,比如某个子目录是SVN,另一个子目录是Git,IDE会自动切换对应的菜单。
Visual Studio:VS的Git支持是原生集成的,SVN则需要安装扩展(比如VisualSVN)。如果你用的是较新的VS,Git的按钮就在解决方案管理器旁边;SVN仓库则建议直接用TortoiseSVN右键操作,或者装扩展后在解决方案上右键,会出来SVN相关的Commit/Update菜单。
VSCode:Git是内置的,SVN要靠扩展。在扩展市场搜“SVN”,安装下载量最高的那个(一般叫SVN,作者是JohnstonCode),它会调用本地的svn.exe。打开SVN工作副本目录时,左侧源代码管理面板会变成SVN操作面板,可以查看改动、提交、更新。VSCode的多根工作区里,不同子文件夹分别属于SVN和Git也能正常识别。
IDE配置这块的核心经验是:只要能在一台机器上同时找到git.exe和svn.exe,并且两个命令行工具都能正常工作,IDE层面就不会出大问题。
3. 双轨并行的三种日常工作流(直接照抄)
这一章是整篇文章的核心。工具装好了,IDE也配好了,那同一个项目里到底怎么同时用两套版本控制?我给出三种方案,覆盖从纯命令行到纯图形界面的全诉求。
3.1 方案A:SVN做主仓库,Git做本地后悔药
这是我最推荐的非迁移场景方案,适合“公司仓库是SVN,但你想要本地版本管理能力”的情况。
原理:在SVN的工作副本里执行git init,启动一个不关联任何远端的纯本地Git仓库。SVN负责和团队同步,Git负责你个人每天的文件快照管理。
初始化步骤:
# 第一步,迁出SVN工作副本 svn checkout http://svn.example.com/project/trunk project cd project # 第二步,创建.gitignore,把.svn目录排除掉 echo ".svn/" > .gitignore # 第三步,初始化本地Git仓库 git init git add . git commit -m "init local repo"这里最关键的就是.gitignore必须排除.svn/,否则Git会把每个目录下的.svn文件全部记录下来,一次commit里全是乱七八糟的元数据。我见过有人忘了这一条,Git仓库里攒了几千个.svn文件,后来清理起来极其痛苦。
日常提交节奏,我强烈建议按这个固定顺序:
svn update——先拉取服务器上别人提交的代码;- 改代码;
svn diff——检查自己的改动是否符合预期;svn commit——把改动推送给团队;git add .+git commit -m "..."——给本地Git留一个快照。
为什么要把git commit放在最后?因为Git快照记录的是“服务器上已经存在的最终状态”,如果你先git commit再svn commit,万一svn commit因为冲突失败,你的本地Git快照就记录了一个“团队里别人都看不到的状态”,之后想回滚就会和服务器状态对不上。
这个方案能干什么:
- 改坏了代码,在SVN提交前你可以用
git diff看自己改了什么,git checkout -- file丢弃文件改动; - 想开分支做个大改动,
git checkout -b experiment开Git分支,随便折腾,SVN工作副本完全不受影响,实验成功再git checkout master+git merge experiment; - 每次
svn update后,Git status能直观告诉你工作区哪些文件发生了变化,比svn status的信息密度高不少。
注意:这套方案里,Git仓库不要添加任何remote,不要执行push。因为Git本地仓库只是你监控版本、做快照的辅助工具。一旦你哪天加了remote并push到某个Git平台,那另一套代码历史就和SVN完全分叉了,最终只会把自己搞晕。
3.2 方案B:用git-svn把SVN仓库当成Git仓库来用
如果你不想看到SVN的命令行、不想装TortoiseSVN,只想纯粹用Git操作,那就用Git自带的git-svn桥接工具。它是Git官方提供的SVN兼容层,你日常用Git命令,它自动翻译成SVN协议操作。
首次克隆:
# 标准SVN布局(trunk/branches/tags)加 -s 参数 git svn clone -s http://svn.example.com/project my-local-repo # 如果仓库不是标准布局,手动指定 # git svn clone -T trunk -b branches -t tags http://svn.example.com/project repo首次克隆会把SVN服务器上的全部历史拉到本地,如果服务器历史很长,会很慢。可以从指定版本开始加速:
git svn clone -s -r 1000:HEAD http://svn.example.com/project repo上面的命令只从r1000开始拉取,之后用增量命令补齐。
日常开发流程:
# 拉取SVN服务器上别人的提交 git svn rebase # 正常用git做本地提交 git add . git commit -m "commit message" # 把本地提交推送到SVN服务器 git svn dcommit注意这里不是git push,是git svn dcommit。它会把你本地的一段Git提交逐个转换成SVN提交,每个提交对应一个SVN版本号。执行后打开SVN Log,会看到你那条Git commit的message和git-svn-id信息。
这个方案的坑,比方案A多,重点讲一下:
- git-svn只工作在它克隆的那个SVN分支上。你在本地可以开Git分支(体验分支的便利),但合并多个Git分支后直接dcommit到SVN,官方不支持,会因为merge的父子关系无法映射到SVN而报错。正确做法是在SVN仓库里正儿八经地创建分支,clone到本地后切到对应的remote分支上操作;
- 千万不要在dcommit前用
git rebase -i改写已提交的历史。SVN历史上已经出现了这些提交记录,你再改本地哈希,dcommit时会出现大量冲突和诡异错误。本地随便改,但改完要赶在推送前确认; - git svn rebase和git pull --rebase不一样,前者是“把SVN上新增的提交拉到本地,并把你的提交rebase到它们之上”,是整个桥接机制的自动处理流程。不要试图用standalone的git pull来替代,会搞乱git-svn的remote状态。
方案B适合什么场景?适合你个人有能力绕开SVN客户端,且团队里其他人都还在用SVN做主流协作。你是“少数派”,但通过git-svn,你能保留完整的Git体验。
3.3 方案C:图形界面党怎么用TortoiseSVN + TortoiseGit
装一个TortoiseGit(Git的小乌龟),它会和TortoiseSVN住进同一个右键菜单里。两个小乌龟的版本控制操作如下:
- SVN小乌龟管
SVN Update、SVN Commit、SVN Checkout; - Git小乌龟管
Git Sync、Git Pull、Git Push、Git Commit。
右键菜单里,两类操作会同时出现,这时候最容易犯的错就是“心态混乱”。我的建议是:同一个目录里,认准一条主线。比如你处于方案A(SVN主仓库+Git本地快照),那就在菜单上养成习惯:提交给团队一定是点SVN Commit,记录本地版本一定用Git Commit。两个小乌龟的图标外观很相似,新手真的会点错。
另外提醒一句:TortoiseGit默认的忽略规则和TortoiseSVN的“全局忽略”是各自独立的。SVN仓库里维护的svn:ignore属性、svn:global-ignores属性,不会自动同步到Git的.gitignore。如果你同时用两套工具,就要在两个地方都维护忽略规则,漏一个都会导致该忽略的文件被误提交。
4. 从SVN到Git的迁移实操:历史、作者和分支一个都不能丢
工作流说完了,再讲一个更进阶的用法:整个团队决定从SVN迁到Git,怎么把历史仓库完整“挪”过去。这一步很多人喜欢手动把代码复制过去,只保留最新代码,但丢失历史会让日后排查问题变得非常困难。正确做法是用git-svn做一次完整迁移。
4.1 迁移前要准备的三样东西
第一,SVN仓库的目录结构。先确认是标准布局还是非标准布局。标准布局指根目录下有trunk、branches、tags三个目录,迁移最方便;非标准布局需要额外指定参数,否则无法正确识别主干和分支。
第二,作者映射文件。SVN记录里存的是用户名,Git提交需要“用户名 + 邮箱”。要生成一个映射文件,把每个SVN用户的用户名映射成Git身份。Git Bash里执行:
svn log -q | awk -F '|' '/^r/ {sub("^ ", "", $2); sub(" $", "", $2); print $2" = "$2" <"$2"@example.com>"}' | sort -u > authors.txt执行完打开authors.txt,手动检查一遍,把占位邮箱改成真实邮箱,例如:
zhangsan = Zhang San <zhangsan@example.com> lisi = Li Si <lisi@example.com>第三,目标Git仓库。在你的Gitee、GitHub或GitLab上建一个空仓库,拿到仓库地址。建议不要直接push到团队正在使用的Git仓库,先在本地迁移验证无误后再推送。
4.2 用git svn clone完成仓库转换
假设SVN地址是http://svn.example.com/project,执行:
git svn clone -s --authors-file=authors.txt -r 1000:HEAD http://svn.example.com/project project-git参数说明:
-s:自动识别标准布局(trunk/branches/tags);--authors-file=authors.txt:指定作者映射文件,这样每个Git提交的author信息才是规范的;-r 1000:HEAD:只迁移从r1000开始到最新版本的历史。首次迁移建议限制范围,先跑通流程再补全,否则SVN仓库若有两三千个版本,clone时间会非常久。
如果SVN仓库历史很长,你可能需要分步增量拉取。第一次clone完成后,用如下命令继续拉取之后的新提交:
git svn fetch git svn rebasegit svn fetch负责抓取SVN上新提交的对象,git svn rebase负责将这些提交整合进当前分支。
4.3 分支、标签与远端推送的收尾工作
迁移完之后,本地看起来是一个完整的Git仓库,但你执行git branch -a看一下,会发现SVN上的分支以remotes/origin/xxx的形式存在,标签也都在refs/remotes/origin/tags/xxx下。需要将它们转换成本地分支和真正的Git标签,然后推送到新Git平台。
把SVN分支转成Git分支,进入project-git目录:
for ref in $(git for-each-ref --format='%(refname:strip=2)' refs/remotes/origin | grep -v '^tags/'); do git branch "$ref" "refs/remotes/origin/$ref" done把SVN标签转成Git标签:
for ref in $(git for-each-ref --format='%(refname:strip=3)' refs/remotes/origin/tags); do git tag "$ref" "refs/remotes/origin/tags/$ref" done然后推送全部内容:
git remote add origin git@gitee.com:yourname/project.git git push -u origin --all git push -u origin --tags推送完成后,用浏览器打开新仓库的“标签”页和“分支”页确认数量是否符合预期。一旦确认无误,SVN老仓库不必急着删除,可以让团队成员先读新仓库,过一两个月再关停。
迁移过程中有个很常见的坑:SVN的空目录不迁移过来。Git默认不跟踪空目录,而SVN是可以跟踪空目录的。如果某个模块目录在SVN下是有意保留的空目录,迁移后需要手动加个.gitkeep文件再提交。怎么发现?在SVN仓库里执行svn list,对比每个目录在Git里是否存在,数量不多就手动处理;目录很多就用脚本生成占位文件。
5. 常见问题与排查技巧实录
最后把我在实际工作中遇到频率最高的问题和解决方案整理出来,直接照着排查就行。
5.1 命令行工具找不到?先查这三处
问题一:“无法将‘git’项识别为 cmdlet、函数、脚本文件或可运行程序的名称。”这是Windows下Git没加到PATH的典型报错,常见于IDEA内置终端或PowerShell窗口。解决:重新运行Git安装包,选择Modify,在“Adjusting your PATH environment”步骤确保选中了“Git from the command line”,装完重开终端。
问题二:“svn: command not found”在纯命令行环境里敲svn报错,说明TortoiseSVN命令行组件没装。重新运行TortoiseSVN安装包,Modify,勾上command line client tools。
问题三:IDEA里SVN操作报E170013、E230001。先确认Settings -> Version Control -> Subversion里的svn.exe路径正确,并点击Test。如果路径正确还报错,通常是服务器地址无权限或证书问题,用TortoiseSVN先正常访问一次,保存认证信息后再回到IDEA。
5.2 SVN和Git免密配置的两种思路
SVN免密:首次连接SVN时,认证弹窗里勾选“Save authentication”;Windows凭据管理器会记录下来,之后更新、提交不再弹密码。如果没弹窗,可以用svn auth命令查看当前保存的认证记录。清理凭据则进入控制面板 -> 凭据管理器 -> Windows凭据,找到对应条目删除。
Git免密,分HTTPS和SSH两种方式:
- HTTPS方式:Git for Windows自带的Git Credential Manager会帮你保存账号密码。强制开启:
git config --global credential.helper manager; - SSH方式:生成密钥
ssh-keygen -t ed25519 -C "your_email",公钥(.pub文件内容)添加到Gitee/GitHub的SSH公钥列表,私钥留在本地。克隆地址选SSH,推送时就不用输入密码。
5.3 混合使用中最容易踩的四个坑
坑一:.svn被误纳入Git仓库。一旦.svn/被git add进去,每次SVN更新导致.svn目录内容变化,Git status就是一坨红色。已经误提交的,执行:
git rm -r --cached .svn git commit -m "remove .svn from git"然后在.gitignore里补上.svn/。
坑二:行尾符问题导致所有文件都显示为modified。Windows下SVN默认把文件以CRLF保存在工作副本,Git配置了core.autocrlf=true时也会做CRLF/LF转换。两套机制叠加,极容易出现“我明明没改文件,git status却显示一堆改动”的情况。处理:在混用目录下执行git config core.autocrlf false,然后git checkout .重新刷新工作区。或者更简单,根目录的.gitattributes里统一声明文本类型。
坑三:文件名大小写不一致。Windows文件系统默认不区分大小写,但SVN和Git都认大小写。假如你新增foo.js、删除Foo.js,在SVN上提交成功了,Git里会出现两个文件并存;反过来在Git上操作也可能让SVN状态混乱。解决:涉及改文件名时成对操作,先删旧文件再add新文件,避免同一个文件在仓库里出现两个“大小写不同”的记录。
坑四:git-svn克隆时报“Unable to determine upstream SVN information”。这是SVN仓库不是标准布局导致的。用-T -t -b显式指定三个目录,或重新确认仓库结构。
5.4 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| git命令找不到 | PATH未配置 | 重装并勾选Git from the command line |
| svn命令找不到 | TortoiseSVN未装命令行组件 | 重装勾选command line client tools |
| IDEA无法连接SVN | svn.exe路径错误 | 在IDEA的Subversion设置里手动指定并Test |
| git status大量显示修改 | core.autocrlf与SVN换行冲突 | 设置core.autocrlf false后checkout |
| .svn目录出现在git里 | 缺少.gitignore规则 | git rm -r --cached .svn,补充忽略 |
| git svn dcommit报Merge冲突 | 本地多个分支合并后推送SVN | 避免在git-svn里做Git merge,回退后重新提交 |
| 迁移后标签消失 | SVN标签未转换 | 用for-each-ref循环生成本地git tag |
| 中文文件名显示转义 | core.quotepath默认开启 | 设置core.quotepath false |
| 每次git push都要密码 | 未配置凭据或SSH密钥 | credential.helper或ssh-keygen配置公钥 |
最后说点实在的
如果你也打算在同一台电脑上把SVN和Git都跑起来,我个人的建议是:不要一开始就想着用git-svn这种骚操作,先老老实实用“SVN做远端 + Git做本地快照”的双轨模式跑两周。这种模式最稳,出错也就是本地Git仓库乱了,不影响团队其他人,等SVN和Git两套思维在你脑子里切换自如了,再去碰git svn rebase和dcommit,胜率高得多。
还有一个我踩过几次坑后养成的习惯:不管用哪种双轨方案,每天下班前检查一次“远端状态”。SVN就是svn status、svn log看有没有未提交;Git就是git status看有没有未push的分支。两套工具同时用,最怕的不是技术报错,是你自己忘了哪个改动提交到哪边了。定点检查,习惯养成,这比任何配置技巧都管用。