ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Git迁移SVN全记录:提交历史保全与Log失败排查

Git迁移SVN全记录:提交历史保全与Log失败排查 上篇写完SVN服务器的搭建这篇记录一次从git仓库迁移到SVN仓库的实操。迁移本身不算罕见但这里要保留完整提交记录而且实际执行时让我在提交log上翻了车反复折腾才弄清原因。这篇笔记把整个迁移思路、工具选型、具体步骤和后面那个“迁移提交log失败”的坑都串起来希望能帮到同样在折腾这两套版本控制系统的同学。如果你有git仓库的历史包袱又必须切到SVN环境这篇应该能少走不少弯路。先交代一下背景手上有十几个git仓库团队协作方式要改成SVN客户侧也只认SVN的版本号和权限体系。直接丢文件过去当然不行提交历史是项目的重要资产审计、追溯、回滚都靠它。所以核心需求就是把git仓库完整迁移到SVN仓库提交记录一条不能少。听起来简单做起来才发现git和SVN的提交模型完全是两个物种强行搬运会丢信息工具用错了还会得到一堆垃圾提交。1. Git与SVN提交记录模型的本质差异1.1 为什么不能直接把文件拷进SVNGit是分布式版本控制每个开发者的本地都是一个完整仓库提交记录通过commit的哈希串成一条有向无环图DAG。分支、合并、变基都是家常便饭提交内容还包含作者、邮箱、提交时间、注释、父提交指针这些元数据。SVN则是集中式版本控制服务器端只有一个中心仓库所有提交编号是按顺序递增的整数比如r1、r2、r3。SVN没有本地仓库的概念分支和标签本质上就是目录拷贝合并信息靠svn:mergeinfo属性记录跟git的分支拓扑完全不是一回事。这两套模型放在一起迁移的本质就不是“复制文件”而是要把git历史上每一次提交转换成SVN仓库里的一次commit同时尽量保留作者信息和注释。如果只把最新代码svn import进去那SVN上就只有r1一条记录之前几百次commit全部消失这在实际项目中是不可接受的。1.2 SVN的线性版本号与Git的DAG冲突SVN的版本号是全局递增的r10一定是在r9之后而且一个目录路径下不可能出现两个平行的历史分支。Git则允许develop分支和feature分支各自分叉之后还能合并。把git的DAG映射到SVN的线性序列必然要做折衷。常用的办法是拿git的一个主干分支如master/main作为SVN的trunk其它分支分别映射成SVN的branches下的目录标签映射成tags目录。git svn工具就是这么干的它会在SVN仓库里重建分支结构但合并关系通常会丢失SVN侧看到的是一串按时间排序的提交。我一开始没意识到这个模型差异天真地以为git svn dcommit会把所有历史平铺过去。实际操作后才发现如果没有认真准备作者映射文件SVN会直接拒收提交或者提交进去但log记录全是乱的。这也是后面“迁移提交log失败”案例的直接导火索。2. 迁移工具选型与前端准备2.1 git svn、svn2git和手工方案的取舍网上讲迁移方案最常见的三个选择是git自带桥接工具git svn、Ruby写的svn2git、以及手工svn add svn commit。我推荐优先试git svn理由很直接git安装自带不需要额外装Ruby和一堆gem对trunk/branches/tags的标准SVN布局支持成熟社区资料多遇到问题好搜。svn2git优势在于对复杂分支合并的处理更好能把git的merge commit转成SVN的合并跟踪但它依赖Ruby环境在老服务器上装依赖容易出幺蛾子。手工方案不推荐只适合仓库很小、提交次数少于50次的场景因为每一条提交都要手动操作而且很难做到定时批量处理。选型的时候还有一点要考虑SVN服务器端装了VisualSVN还是Apache Subversion。VisualSVN自带管理界面和用户权限设置但命令行工具是svn.exeApache版则需要自己配svnserve或httpd模块。我这边用的是Linux下的svnserve直接svnadmin create建仓库命令行操作最顺手后续设置权限也简单。2.2 初始化SVN仓库与标准目录结构不管用什么工具迁移第一步都是先把SVN仓库准备好。我这里用命令svnadmin create /data/svn/repos/myproject接着要建标准目录结构。SVN本身没有强制的目录布局但git svn和svn2git都默认按trunk、branches、tags三层来识别。不建这三层的话后续clone会报“Unable to determine upstream SVN information from working tree history”之类的错误。用svn mkdir建立svn mkdir --parents svn://localhost/myproject/trunk -m Init trunk svn mkdir --parents svn://localhost/myproject/branches -m Init branches svn mkdir --parents svn://localhost/myproject/tags -m Init tags注意目录名称必须严格小写Git svn识别SVN布局时区分大小写把Trunk写成大写会直接导致clone失败。如果你用的是VisualSVN的图形界面点“Create Standard Structure”就行效果一样。3. 核心实操git svn clone与dcommit迁移流程3.1 生成SVN作者映射文件这是整个迁移里最容易忽略、也最容易导致log丢失的环节。git提交的作者字段是“名字邮箱”的组合比如zhangsan zhangsanexample.com但SVN的用户名只是一个短字符串比如zhangsan。git svn在把git提交转换成SVN提交时必须知道git作者对应哪个SVN用户否则它没法给SVN提交分配author。如果没有映射文件git svn会直接把git作者字符串截断塞进SVN结果就是SVN log里显示一堆乱七八糟的“zhangsan zhangsanexample.com ”或者干脆报错。生成映射文件的方法是先导出git仓库里所有作者git log --prettyformat:%an %ae | sort -u拿到输出后手工整理成如下格式每行一项zhangsan zhangsanexample.com zhangsan lisi lisicorp.com lisi格式严格是“git作者信息 svn用户名”。注意等号两边要有空格svn用户名必须是SVN服务器上真实存在的账号否则后面dcommit时会因为找不到用户而被拒绝。我以前踩过一个坑SVN服务器上用户是john_smith但映射文件里写的john结果所有john的提交全部失败log一条都没进。3.2 初始化git-svn工作副本并拉取远程作者映射文件准备好后先从空的SVN仓库初始化一个git-svn工作目录git svn clone --authors-fileauthors.txt svn://localhost/myproject workdir cd workdir这个动作会在本机创建一个git工作副本同时把SVN仓库的trunk、branches、tags结构识别进来。SVN那边目前还是空的所以clone出来只有一个空目录。接着把原来的git仓库拉进来git remote add origin /path/to/original.git git fetch origin此时工作目录里有两个不相关的历史一个是SVN的初始空状态master分支一个是原git仓库的所有提交origin/master。要做的是把git历史快进到master上。如果git仓库的分支比较多还要处理分支映射这里先用最简单的主干迁移git checkout -b master origin/master git svn dcommitgit svn dcommit是真正往SVN服务器推送提交的命令。它会从当前分支的第一条提交开始逐条转换成SVN提交并按提交时间排序。注意不要在git仓库里commit后再dcommit而是先让当前分支指向原git历史再执行推送。3.3 分支与标签的处理方式如果原git仓库有多个分支建议用git svn branch指令先把SVN分支建好再推送到对应分支git branch -r git svn branch feature/xxx git checkout -b feature/xxx origin/feature/xxx git svn dcommit标签处理稍微特殊。git的tag是轻量或附注对象SVN的tag只是tags目录下的拷贝。用git svn迁移标签时要把tag记录转成一次只添加文件不修改内容的提交。比较省事的做法是git tag -l | while read t; do git svn tag $t done但实测下来如果tag很多这个命令经常卡住或者报“tag not found”。我后来改用svn copy手动处理直接把原git里某个commit对应的代码拷贝到tags目录svn copy svn://localhost/myproject/trunkN svn://localhost/myproject/tags/1.0.0 -m Tag 1.0.0实战提醒分支和标签的迁移不要太贪心。先保证主干历史完整迁移再处理关键分支。如果分支结构特别复杂git svn的合并记录会失真这时候就要考虑svn2git或者干脆接受部分信息丢失。4. 迁移提交log失败的根因排查与修复记录4.1 现象SVN log里看不到历史提交我第一次执行完上面的流程后发现SVN仓库里文件确实齐全但是log显示只有两条提交一条是我手动建的trunk目录另一条是dcommit推上去的合并提交原来的几百条详细提交记录全部消失。git log里的状态树和提交信息在SVN侧只剩了个空壳这显然不是我想要的。排查的第一步是看dcommit输出。git svn dcommit在执行时会在每个提交前打印对应的SVN版本号如果中途出现“Use of uninitialized value”或者“revision ... mismatched”之类的提示基本就是作者映射或者上游分支对不上。我当时看到的现象是git svn dcommit成功退出了没有报错但SVN log里只有一条r2内容还是自动生成的“git-svn-id”补丁这说明dcommit根本没把git提交真正转换成SVN提交而是做了一个类似快照的提交。4.2 原因一作者映射文件缺失或格式错误这是最常见的原因。上面提到没有作者映射文件的话git svn会尝试猜测SVN用户猜不对就拒绝提交。但如果你写了作者映射却漏掉了某位作者git svn会在dcommit时输出类似“Author: xxx not defined in authors.txt”的提示。我那次失败是因为映射文件里有个别作者邮箱写错了导致工具直接跳过了该提交后续提交全部建立在缺失节点上最终log链断掉。解决办法重新导出所有作者比对映射文件与git log输出的差异一个都不能少。可以用脚本自动校验comm -23 (git log --prettyformat:%an %ae | sort -u) (awk -F {print $1} authors.txt | sort -u)输出为空的才是完整的映射文件。4.3 原因二SVN用户权限不足导致部分commit被拒另一个隐蔽的原因和SVN服务器权限有关。有些团队用了VisualSVN Server默认全局用户只能读不能写。git svn dcommit需要对trunk目录有写权限如果某个用户只配置了read权限那这个用户对应的git提交就会被服务器拒绝。表面看起来是log失败实际是权限问题。检查SVN仓库根目录的access.conf或者VisualSVN的权限对话框确保映射到的每个svn用户名都至少有trunk的read-write权限。我当时用svnssh协议权限是通过authorized_keys认证的。如果映射文件里的用户不存在于SVN服务器提交会被拒绝并且不产生任何版本号。这种情况下log自然就没记录。手动测试方法很简单svn log svn://localhost/myproject/trunk看是否能正常访问如果提示“svn: E220001: Access denied”那就是权限问题。4.4 原因三commit message编码问题导致log乱码还有一个容易被忽略的坑是编码。原git仓库里如果有中文注释而SVN服务器的locale不是UTF-8那提交log会出现乱码。git本身以UTF-8存储commit信息但svn log输出时会按服务器的系统编码显示。解决方法是设置SVN服务器的locale为UTF-8或者在git仓库里设置git config i18n.logoutputencoding UTF-8另外dcommit时还可以加参数--encodingUTF-8强制转换git svn dcommit --encodingUTF-8这个参数能解决大部分乱码问题但不是所有版本都支持建议先git svn --help确认。我迁移时就是因为没加这个参数导致中英文混排的提交信息在SVN log里变成了“锟斤拷”看着像失败实际是编码问题。4.5 修复策略推倒重来还是增量修补一旦发现log链不完整最稳妥的办法是推倒重来不要试图在部分迁移的基础上修补。我第一次失败后就尝试在原有SVN仓库里继续dcommit结果SVN版本号乱跳有些提交重复出现有些又丢失。正确的做法是把SVN仓库删掉重新svnadmin create然后从头执行一次干净的迁移。完整重来前记下以下检查项svnadmin verify仓库完整性git fsck检查原git仓库是否完整确认映射文件和编码参数确认SVN用户与权限我第二次迁移时严格按上面流程加了--encodingUTF-8保留了全部作者映射最终SVN log里2000多条提交一条不差。这说明整个过程是可控的问题基本都在前置准备上。5. 常见报错速查与客户端使用避坑5.1 典型报错与解决方案报错信息主要原因解决办法Author: xxx not defined in authors.txt作者映射缺失补齐映射文件svn: E170013: Unable to connect to a repository at URL服务器地址或认证错误检查svn://或svnssh配置确认svnserve已启动svn: E155007: x is not a working copy目录不是svn checkout出来的svn update或者重新checkoutgit: svn is not a git commandgit-svn未安装Linux下apt install git-svnUnable to determine upstream SVN information from working tree history未遵循标准trunk/branches/tags结构重新初始化仓库目录结构Repository has not been enabled for SVN路径错误确保clone时使用仓库根URL这些都是我在迁移过程中至少碰到过一次的真实报错。特别提一下“svn: E155007”经常出现在你直接svn add一个文件但该文件并不在版本控制工作副本里的时候注意先svn checkout或svn update。5.2 迁移后客户端使用细节IDE与VSCode仓库迁到SVN后团队侧需要调整工具链。如果你之前用IntelliJ IDEA管理gitIDEA里已经内置SVN支持直接VCS - Enable Version Control Integration选择Subversion即可。但有个细节IDEA的SVN插件对git-svn迁移过来的“svn:mergeinfo”属性支持很差合并提交会显示成普通修改不要因此误判log丢失。用VSCode的话装一个“SVN”扩展它会自动识别工作副本里的标记。但注意VSCode的SVN扩展默认只显示修改和更新状态不会帮你自动清理那些从git带过来的.gitignore文件。迁移后一定要检查项目里是否还有.git目录如果有建议删除否则影响SVN文件图标显示比如“绿勾”图标不出现。我见过有人把.git目录留在工作副本里结果svn status永远混乱怎么cleanup都没用。5.3 迁移中的性能与批量提交心得最后分享几个实操心得。第一迁移几百次commit的仓库git svn dcommit非常慢因为它每提交一条就要和服务器做一次交互。5000条提交可能要跑几个小时。建议分批次推送比如先把最早的100条提交推上去确认无误再继续。可以用git svn dcommit --interactive逐条确认。第二迁移过程中千万不要在SVN仓库里做任何其他提交否则版本号会对不上。git svn的dcommit要求你的本地版本和SVN服务器版本一致一旦服务器有外来提交你就得git svn rebase再重来非常痛苦。第三SVN服务器上尽量开svnserve的日志记录路径一般是/var/log/svnserve.log。排查权限问题时看这个日志比猜快得多。VisualSVN则是在事件查看器里看Application日志。第四关于“svn用户权限”和“仓库不存在”这类报错大部分时候是路径配置问题。svnserve启动时指定的根目录和客户端访问的URL要对应比如svnserve -r /data/svn那客户端应该访问svn://host/myproject而不是svn://host/data/svn/myproject。我个人在实际操作中最深刻的体会是迁移git到SVN真正难的从来不是命令本身而是你能否提前把git、SVN、权限、编码、分支模型这五个环节全部对齐。一旦哪一环没对上最后体现出来的就是log失败或者历史丢失。我最后又试了一个小技巧迁移完成后在SVN仓库里跑一次svnadmin dump和svnadmin load做完整性校验确认dump过程没有报错基本就能判断提交记录是完整的。这个动作虽然多花了点时间但能让你在交付给团队前心里有底。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进