将 SVN 仓库迁移到 Git

本文介绍如何将SVN仓库转换为Git作为版本控制系统。

背景说明

SVN(Subversion)是一个不太主流的版本控制工具,与Git相比,其劣势明显:

  • SVN的集中式管理限制了广泛参与,仅少数特权用户能协同编辑,难以满足开源社区的开放需求。

  • SVN的提交须经服务器,网络依赖重、效率低,且缺失代码审核机制,质量难以保证。

参考方案

Git已成为主流的版本控制系统,转向Git前,建议您先明确迁移范围,根据需求挑选最佳的迁移方案。

方案一:仅需要迁移SVN的最新数据

如果仅需要迁移SVN的最新数据,不需要迁移SVN的提交历史记录,那么迁移操作非常简单。

只需要 svngit两个工具即可完成迁移,操作如下。

  • 使用 svn checkout 命令从SVN仓库检出最新文件到本地工作区。

  • 在本地工作区执行 git init初始化,完成本地Git仓库的创建。

  • 编辑 .git/info/exclude文件,在其中添加 .svn等条目以便忽略工作区中的SVN管理目录。

  • 执行git add -Agit commit命令,创建Git提交。

  • 在远程Git服务器上创建代码仓库,假设仓库地址的示例地址为<URL>

  • 执行 git remote add origin <URL> 命令,将本地工作区和远程仓库地址相关联。

  • 执行 git push -u origin HEAD 将本地Git仓库的分支推送到远程仓库中。

该迁移方式和普通Git仓库推送服务端一致,不再赘述。

方案二:需要迁移 SVN 的全部历史

如果需要迁移SVN的历史提交,或者需要迁移多个分支和标签,则需要安装和使用 git-svn 实现 SVN仓库到Git仓库的迁移。

image.png

作为Git的一条子命令,git-svn可以桥接远端的SVN仓库和本地的Git仓库,允许用户使用Git操作远端的SVN仓库。git-svn支持如下操作:

  • 使用 git svn clone 命令将远程的SVN代码仓库克隆到本地Git仓库。

  • 使用 Git 命令在本地工作区中创建提交。

  • 使用 git svn dcommit命令将本地的Git提交推送到SVN仓库上,创建SVN提交。

  • 使用 git svn fetch命令持续从SVN仓库同步提交到本地仓库。

借助 git-svn,SVN仓库的完整历史将无损转为Git仓库,让迁移与协作更加流畅。

针对方案二,迁移操作具体流程如下:

  1. 安装 git-svn

    Git的内置子命令 git-svn和其他的子命令不同,不是编译后的原生可执行程序,而是一个Perl脚本,在运行时依赖svn的perl模块。

    1. 以Ubuntu操作系统为例,首先安装Git和svn,示例如下:

       apt-get install git subversion
    2. 单独安装 git-svn和svn的perl模块,示例如下:

       apt-get install git-svn libsvn-perl

      执行 git svn,如果输出正常的帮助信息,则安装成功。反之,可能会输出如下的错误信息。

      git svn
      Can't locate SVN/Core.pm in @INC (you may need to install the SVN::Core module) (@INC contains: /opt/git/dev/share/perl5 /Library/Perl/5.30/darwin-thread-multi-2level ... ) at /opt/git/dev/share/perl5/Git/SVN/Utils.pm line 6.

      这可能是因为用户手动编译安装的svn、git,在运行 git-svn时无法定位svn的perl模组。解决方案应是将git和svn安装目录中的perl模块目录添加到环境变量GITPERLLIB ,git-svn即可正确运行,示例如下:

      GITPERLLIB=/opt/git/dev/share/perl5:/usr/local/Cellar/subversion/1.14.2_1/lib/perl5/site_perl/5.30.3/darwin-thread-multi-2level
      export GITPERLLIB
      git svn
  2. 探测 SVN 仓库布局

    SVN通过简单目录拷贝即可创建分支,灵活性极高,连不同版本或不完整的文件树也能变身分支。但这种混合管理策略,让自动化识别仓库中的分支变得棘手,极具挑战性。

    要从SVN仓库的目录结构布局推测其分支和标签设置,以及主干分支和其他分支、标签的映射方式,关键在于理解SVN的标准目录结构及其含义。

    • SVN 仓库标准目录结构

      SVN 仓库通常采用以下标准目录结构:

      /项目根目录
      ├── trunk
      ├── branches
      │   ├── feature-1
      │   ├── feature-2
      │   └── ...
      ├── tags
      │   ├── v1.0.0
      │   ├── v1.1.0
      │   └── ...
      • trunk:主干分支,存放项目的主开发线。

      • branches:存放各个功能分支或开发分支。

      • tags:存放各个版本的标签,通常用于标记发布版本。

    • 查看 SVN 仓库的根路径

      使用svn info命令可以查看给定地址的SVN仓库的根路径和其他信息。

      svn info https://example.com/svn/repo/trunk

      以下是一个示例脚本,用于查看 SVN 仓库的根路径,并根据目录结构猜测分支和标签的设置。

      #!/bin/bash
      
      # 输入 SVN 仓库 URL:将 SVN_URL 变量设置为你要检查的 SVN 仓库 URL。
      SVN_URL="https://example.com/svn/repo"
      
      # 获取 SVN 仓库信息:使用 svn info 命令获取仓库信息,并存储在 svn_info 变量中。
      svn_info=$(svn info $SVN_URL)
      
      # 提取根路径:从 svn_info 中提取仓库的根路径。
      root_path=$(echo "$svn_info" | grep '^Repository Root:' | awk '{print $3}')
      
      # 输出根路径。
      echo "SVN 仓库根路径: $root_path"
      
      # 检查 trunk 目录:检查 trunk 目录是否存在,并输出其路径。
      trunk_url="$root_path/trunk"
      if svn info $trunk_url &> /dev/null; then
        echo "主干分支 (trunk) 存在于: $trunk_url"
      else
        echo "未找到主干分支 (trunk)"
      fi
      
      # 检查 branches 目录:检查 branches 目录是否存在,并输出其路径。
      branches_url="$root_path/branches"
      if svn info $branches_url &> /dev/null; then
        echo "分支目录 (branches) 存在于: $branches_url"
      else
        echo "未找到分支目录 (branches)"
      fi
      
      # 检查 tags 目录:检查 tags 目录是否存在,并输出其路径。
      tags_url="$root_path/tags"
      if svn info $tags_url &> /dev/null; then
        echo "标签目录 (tags) 存在于: $tags_url"
      else
        echo "未找到标签目录 (tags)"
      fi

      执行 svn ls命令查看仓库的根路径及目录结构,示例命令及输出如下:

      svn ls https://example.com/svn/repo/
      #  输出说明
      #  trunk/:主干分支,存放项目的主开发线。
      trunk/
      #  branches/:存放各个功能分支或开发分支。
      branches/
      #  tags/:存放各个版本的标签,通常用于标记发布版本。
      tags/

    在SVN转换Git仓库时,要通过参数将仓库布局提供给 git-svn,以便将分支和标签正确导出。

    其他的布局方式如下:

    • 无分支、无标签的主干模式:SVN仓库的根目录即为项目主干。

    • 多项目模式:SVN仓库的一级目录作为项目名,每个项目有自己特定的仓库布局。

  1. 建立 SVN 和 Git 之间的作者名称映射

    SVN使用服务端ID作为提交者,而Git允许客户端自定义提交者,含全名和邮箱。

    • git-svn转换时,建议提供SVN到Git用户的映射文件,每行记录映射关系,示例如下:

      # 格式:SVN用户名 = Git用户名 <邮箱>
      loginname = Joe User <user@example.com>
    • 执行 svn log命令可以获取SVN仓库全部提交的用户名列表。

      svn log -q https://example.com/svn/repo/

      从中提取SVN用户名,请手动编辑生成如上所述的SVN到Git用户的映射文件备用。

迁移 SVN 仓库

初始化本地 Git 仓库

创建一个本地工作区目录,进入到目录中,示例如下:

mkdir workdir
cd workdir

执行 git svn init命令将本地工作区初始化为一个Git仓库。

  • 对于无分支的SVN仓库,初始化命令示例如下:

    git svn init https://example.com/svn/repo
  • 对于标准布局的SVN仓库,初始化命令示例如下:

    git svn init -s https://example.com/svn/repo
  • 对于非标准布局的SVN仓库(例如目录名大小写与标准布局有差异),初始化命令示例如下:

    git svn init -t Trunk -b Branches -t Tags https://example.com/svn/repo

拉取远程 SVN 仓库同步

执行 git svn fetch命令与远程SVN仓库数据同步,将远程仓库获取到本地并转换为Git提交。如果有SVN/Git 作者映射文件,如文件authors.map,需在命令行中提供。

  • 同步全部历史提交,命令示例如下:

    git svn fetch -A /path/of/authors.map
  • 同步某个范围的提交,命令示例如下:

    git svn fetch -A /path/of/authors.map -r 0:100 

推送到远程仓库

在云效的服务端创建Git仓库,用于保存转换后的结果。

  1. 创建仓库操作,请参见新建第一个代码库

  2. 在服务端页面获取Git库地址。

    高的 (19).png

    说明

    请不要在新库上创建任何分支、标签以及文件,确保其为空仓库,否则可能因为强制推送问题导致迁移失败。

    示例Git仓库地址:https://codeup.aliyun.com/$group/repo.git,快速添加远程源使用 git remote add target命令,将上述地址设为新源,避免与git-svn源重名,示例如下:

    git remote add target https://codeup.aliyun.com/$group/repo.git

    迁移后,SVN仓库变身Git仓库,分支标签一键转换。迁移完成,SVN分支标签即转为Git跟踪分支和标签。推送时,SVN布局决定推送方式。

    1. 对于无分支标签的SVN仓库,仓库的主干分支映射为 refs/remotes/git-svn,将其推送到目标 Git 仓库的 master分支(或 main分支),示例如下:

      git push target refs/remotes/git-svn:refs/heads/master
    2. 对于标准布局的SVN仓库,其分支和标签混杂在一起,用如下命令重命名标签,以便本地跟踪分支和标签有不同前缀,便于区分。

      git for-each-ref --format="%(objectname) %(refname:lstrip=4)" \
            "refs/remotes/origin/tags/*" |
        while read oid tag; do
            git update-ref "refs/tags/$tag" $oid &&
            git update-ref -d "refs/remotes/origin/tags/$tag"
        done

      然后执行如下命令将本地仓库的分支和标签推送到目标Git仓库。

      git push target \
            --tags \
            refs/remotes/origin/trunk:refs/heads/master \
            "refs/remotes/origin/*:refs/heads/*"

相关文档