从SVN搬到GitHub,是很多团队迟早要面对的一件事。SVN用了多年,里面存着好多提交记录,这些记录里每一条带上的是谁的提交、大概改了什么。要是一般迁过去,历史丢了或者提交人全变成同一个“Anonymous”,那后面翻代码的时候就很难受。好消息是,Git本身提供了比较完整的历史重写工具,配合一个作者映射文件,就能把SVN的提交者身份一对一变到Git里。整个过程没有想象中那么麻烦,但有几个细节得提前搞清楚。
一、为什么要把SVN仓库搬到GitHub
SVN是早期很流行的集中式版本控制系统,所有代码都在一台服务器上,提交历史也是集中管理的。后来Git火了,每个人都有本地仓库,分支合并很轻量,GitHub又提供了强大的在线协作、Pull Request、Code Review和自动检查功能。很多团队想迁过去,但因为历史包袱太重,一直拖着。最大的顾虑之一就是历史提交记录。SVN里每个提交只记录了一个用户名,比如wangwei,没有邮箱。而Git的提交者信息通常由名字和邮箱组成,而且这个信息会一直留在历史里。如果迁移时不处理,GitHub上就看不到谁在贡献,提交记录会显得特别苍白。所以我们需要在迁移过程中把SVN用户名映射成GitHub能识别的姓名和邮箱。
二、迁移前要准备的东西
在动手之前,先确认你手上有这几样东西:
- 一个能读的SVN仓库地址,本地能访问到
- GitHub上已经创建好的新仓库(可以是空的)
- 一台装了Git和svn的电脑,最好在Linux或macOS上操作
- 一份作者映射文件,用来把SVN用户名转成Git的“姓名+邮箱”
作者映射文件是整个迁移里最需要花心思的东西。因为SVN服务器上只知道用户名,比如lucy,但GitHub上需要的是Lucy Zhang <lucy.zhang@example.com>这样的完整信息。如果公司内部有规范的最好,没有的话需要向同事一个一个问。别嫌麻烦,这一步做不好,后面改来改去更累。
三、实操步骤:克隆、映射、重写、推送
下面用一个具体例子走一遍完整流程。假设我们要迁移的SVN仓库地址是http://svn.example.com/company/project,标准布局是trunk、branches、tags。所有命令都在bash终端里执行。
3.1 生成作者映射文件
先创建一个文本文件,名字可以叫authors.txt。每一行代表一个SVN用户名到Git作者信息的映射,格式是固定的:
# 作者映射文件 authors.txt
# 格式:SVN用户名 = 显示名 <邮箱>
# 注意等号两边最好有一个空格,后面脚本解析起来方便
wangwei = 王伟 <wang.wei@example.com>
lucy = Lucy Zhang <lucy.zhang@example.com>
haojie = 李浩杰 <li.haojie@example.com>
dev-01 = chongming <chongming@example.com>
这个文件不需要提交到Git仓库,它只是我们做迁移时临时用的工具。文件里每一行的名字和邮箱都必须是GitHub上活跃的,这样推上去才能看到正确的头像和账号关联。
3.2 用git svn克隆SVN仓库
Git自带一个git svn子命令,可以把SVN仓库完整拉下来,并转换成Git仓库历史。先执行克隆命令,同时告诉它我们准备了作者映射文件:
# 克隆SVN仓库到本地新目录
# --no-metadata可以去掉SVN特有的元数据,让历史更干净
# --authors-file让git svn在导入时直接使用刚才的映射文件
# 如果仓库比较大,可以在前面加个GIT_SVN_PROGRESS=1来看进度
git svn clone http://svn.example.com/company/project \
--authors-file=authors.txt \
--trunk=trunk \
--branches=branches \
--tags=tags \
--no-metadata \
project-git
执行完后目录里就是一个带有完整历史的Git仓库了。--trunk、--branches、--tags告诉git svnSVN仓库的目录结构。如果你的SVN仓库没有用标准布局,比如只有一个根目录,那可以省略这些参数,直接写地址。
克隆完成以后,可以检查一下历史记录里作者是不是已经变成映射后的信息了:
# 进入新生成的Git仓库
cd project-git
# 查看最近5条提交的作者和邮箱
git log --pretty=format:"%h %an <%ae> %s" -5
输出看起来会是这样:
a2f31d2 王伟 <wang.wei@example.com> 修复登录接口空指针
b81fea9 Lucy Zhang <lucy.zhang@example.com> 调整前端颜色变量
ff08e91 李浩杰 <li.haojie@example.com> 新增订单导出功能
如果这里已经正确显示,说明git svn的--authors-file帮我们完成了大部分工作。但有时候SVN里存在一些特殊提交,比如没有作者的提交,或者同一个SVN用户名在不同分支大小写不一致,这种情况就需要filter-branch再兜底清洗一下。
3.3 用filter-branch重写提交者
git filter-branch是Git自带的历史重写工具,可以批量修改历史提交里的元数据。我们用它根据authors.txt重新处理一遍所有提交,确保任何漏网之鱼都能被纠正。
在运行之前,先备份这个仓库,可以拷贝一份目录,或者用git clone备份一个bare仓库。历史重写是危险操作,备份是必须的。
然后执行下面的重写命令。这个命令有点长,但每条代码都有注释:
# 在仓库根目录下执行
# -f 是强制重写,因为第一次运行后refs/original会留下备份
git filter-branch -f --env-filter '
# 读取authors.txt,寻找匹配当前作者名的行
matched_line=$(grep "^$GIT_AUTHOR_NAME *=" authors.txt | head -n1)
if [ -n "$matched_line" ]; then
# 去掉“SVN用户名 = ”部分,得到“显示名 <邮箱>”
mapped=$(echo "$matched_line" | sed "s/^[^=]*= *//")
# 从映射结果里抽出显示名,比如“王伟”
new_name=$(echo "$mapped" | sed "s/ *<.*//")
# 从映射结果里抽出邮箱,比如“wang.wei@example.com”
new_email=$(echo "$mapped" | sed "s/.*<\(.*\)>.*/\1/")
# 覆盖Git提交的作者名与邮箱
export GIT_AUTHOR_NAME="$new_name"
export GIT_AUTHOR_EMAIL="$new_email"
fi
# 对提交者同样处理,保证提交者身份也统一
matched_line2=$(grep "^$GIT_COMMITTER_NAME *=" authors.txt | head -n1)
if [ -n "$matched_line2" ]; then
mapped2=$(echo "$matched_line2" | sed "s/^[^=]*= *//")
new_name2=$(echo "$mapped2" | sed "s/ *<.*//")
new_email2=$(echo "$mapped2" | sed "s/.*<\(.*\)>.*/\1/")
export GIT_COMMITTER_NAME="$new_name2"
export GIT_COMMITTER_EMAIL="$new_email2"
fi
' -- --all
这段脚本在每次提交上都会跑一遍。GIT_AUTHOR_NAME和GIT_COMMITTER_NAME是Git在执行filter-branch时传入的环境变量,分别代表提交作者和提交者。我们通过修改这两个变量的值来改变提交的归属。注意脚本必须放在单引号里,因为里面有很多字符串,不能让bash在外部提前展开。
执行完之后,再检查一次历史:
# 重新查看最近3条提交,注意hash已经变了
git log --pretty=format:"%h %an <%ae> %s" -3
如果所有作者都正确了,说明重写成功。
3.4 推送到GitHub
本地历史已经干净了,接下来就是推送到GitHub。先把远程仓库地址加上,然后强制推送,因为我们要覆盖远端的所有分支和tag。
# 在本地仓库目录下
# 添加GitHub远程地址,替换成你自己的仓库URL
git remote add origin https://github.com/your-company/project.git
# 推送所有分支
# 因为历史重写了,必须用 --force,否则会被拒绝
git push origin --all --force
# 推送所有标签
git push origin --tags --force
推送完成后,打开GitHub仓库,点开“Commits”页面,就能看到带名字和邮箱的提交记录了。只要邮箱和GitHub账号绑定,提交还会自动关联到对应的用户头像。
四、filter-branch到底做了什么
很多人会好奇,filter-branch为什么能把历史改了。简单说,Git的提交模型就像一串链表,每个提交里面保存了作者信息、提交信息、父提交的引用,以及当时代码快照的hash。filter-branch会遍历这串链表,每次拿到一个提交,设置好对应的环境变量,然后让它执行你给的脚本。你的脚本修改了环境变量之后,Git会用新的环境变量重新生成一个提交对象。关键点是,只要提交的内容或者元数据有变化,这个提交的hash就会变,它后面所有子孙提交的hash也会跟着变。所以重写之后,整个仓库的历史完全变成了一条新的时间线,旧的时间线被保留在refs/original里。这也是为什么推送时必须用--force,因为远端跟本地已经完全是两条不同的历史了。
filter-branch不仅仅能改作者邮箱,它还能改提交信息、删掉大文件、修改路径等。不过我们今天只关心作者身份,所以只演示了--env-filter。如果你以后想清理历史里的敏感文件,可以看看--tree-filter或--index-filter。
五、应用场景分析
这种老仓库迁移到GitHub的场景,最常见的有这几种:
- 公司内部原本用SVN管理代码,现在想统一到GitHub,让研发流程更现代化。
- 开源项目早期用SVN,现在转移到GitHub进行社区协作,历史记录希望原封不动带过去。
- 多个部门使用同一个SVN服务器,但各自有独立目录,迁移时想拆分成多个Git仓库,历史作者信息要按目录分开映射。
在这些场景里,作者映射文件的准确性直接决定了迁移质量。如果只是小团队,可能手动写几行就够。但如果是几百人的大仓库,建议用脚本从SVN的日志里抽取所有出现过的人名,然后批量做成映射文件,再进行人工补充。
六、技术优缺点
filter-branch是Git自带的老牌重写工具,它的优点很突出:支持几乎所有历史重写场景,不要求安装额外工具,而且与Git核心逻辑紧密结合。通过环境变量修改作者信息,逻辑清晰,容易上手。对于中小型仓库,它完全够用。
但它的缺点也很明显。第一是性能差,它是纯Python脚本,每处理一个提交都要启动一个子进程,仓库一大,耗时几分钟甚至几十分钟都很正常。第二是容易踩坑,比如路径中有中文、用户名包含特殊字符时,需要处理各种转义问题。第三是Git官方已经明确不推荐使用它了,Git 2.38以后会打印警告,推荐用git-filter-repo替代。filter-repo在性能和安全性方面好很多,但它需要单独安装,而且不是内置命令。如果你只是做一次性迁移,并且想在老版本Git上顺利执行,filter-branch依然是很多教程里的选择。
七、注意事项
整个迁移过程有几个地方要特别留意。
第一,迁移之前一定要把SVN仓库完整备份一份。历史重写是不可逆的操作,万一写错了,还能从备份里恢复。可以简单压缩一份SVN目录,或者用svnadmin dump做一个备份。
第二,作者映射文件要一次性补齐。如果漏掉某个用户名,filter-branch脚本会跳过它,那这个提交的作者就停留在默认状态,后面还得再来一次。建议先执行下面这条命令,提取SVN仓库里所有出现过的作者名:
# 从克隆后的Git历史里提取所有作者名
git log --format='%an' | sort -u
然后对照authors.txt看看有没有遗漏。
第三,重写历史过程中,仓库内不要有任何人员操作,避免提交产生冲突。而且整个操作应该在本地完成,不要直接在GitHub的Web端操作。
第四,推送完之后,如果有其他同事已经在新仓库里做了开发,他们需要重新克隆一次,或者用git rebase处理兼容。强制推送会覆盖远端历史,所有基于旧历史的本地分支都会失效。所以最好和团队约定好一个时间点,大家先暂停提交,等推送完成后再统一切换。
第五,filter-branch执行后会生成一个refs/original备份,可以用下面的命令删除它,避免残留引用影响后续操作:
# 删除filter-branch留下的备份引用
git for-each-ref --format='%(refname)' refs/original | xargs -n1 git update-ref -d
八、总结
从SVN迁移到GitHub,核心就是两条:一是用git svn把SVN历史完整拉下来,二是用作者映射文件保证每个提交都能找到对应的主人。filter-branch负责做最后的兜底清洗,把历史里所有作者信息统一规范成GitHub认识的格式。整个过程不需要花里胡哨的工具,只要会写基本的bash命令就够了。虽然filter-branch如今已经算是老古董,但理解它的工作原理对理解Git提交模型非常有帮助。如果以后再碰上大规模历史重写,可以直接换成filter-repo,理解了这个过程,用起来会更容易。迁移完成后,一个干干净净、历史完整的新仓库就正式在GitHub上安家了。
评论
围绕“从SVN迁移至GitHub保留历史提交者身份,作者映射文件与filter-branch重写实操指南”参与讨论