SVN迁移到Git:保持提交记录

1.前言

最近组里要把之前在 SVN 上管理的项目迁移到 SVN 上去,所以花了一个晚上研究了一下 SVN 向 Git 迁移的方法。这里首先给出一种能保存提交记录的方案。

2.实施准备

再实施迁移之前,有几件事情是必须做的:

  1. 获取 SVN 路径权限
  2. 将本机的公钥提交到 Git 服务器上
  3. 在 Git 服务器上建立仓库

3.实施过程

实施的过程分为以下几个步骤:

  1. 将 SVN checkout 到本地
  2. 获取用户名列表
  3. 用 git svn clone 创建 Git 仓库
  4. 导出 .gitignore 文件
  5. 调整分支和标签
  6. 推送到远程

a. 将 SVN checkout 到本地

svn checkout --username USER_NAME --password PASSWORD https://svn.xxx/dir_name  dir_name

b. 获取用户名列表

通过如下命令获得用户列表:

svn log --xml | grep -P "^<author" | sort -u | perl -pe 's/<author>(.*?)<\/author>/$1 = /'

然后按照如下格式放入一个文件中(比如 user.txt):

schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>

c. 用 git svn clone 创建 Git 仓库

git svn clone --no-metadata -A users.txt https://svn.xxx/dir_name  dir_name

d. 导出 .gitignore 文件

git svn show-ignore > .gitignore
git add .gitignore
git commit -m 'Convert svn:ignore properties to .gitignore.'

e. 调整分支和标签

首先要移动标签,把它们从奇怪的远程分支变成实际的标签,然后把剩下的分支移动到本地。

cp -Rf .git/refs/remotes/tags/* .git/refs/tags/
rm -Rf .git/refs/remotes/tags

接下来,把 refs/remotes 下面剩下的索引变成本地分支:

cp -Rf .git/refs/remotes/* .git/refs/heads/
rm -Rf .git/refs/remotes

f. 推送到远程

git remote add origin git@my-git-server:myrepository.git
git push origin --all

4. Python 脚本

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import sys
import commands

if len(sys.argv) < 3:
    print 'Usage: python %s USERNAME PASSWORD' % sys.argv[0]
    exit(255)

user_name = sys.argv[1]
user_pwd  = sys.argv[2]

svn_repos = [
        svn_path,
        ......
        ]

git_repos = [
        git_path,
        ......
        ]

users = {}

def init():
    run_cmd('.', 'mkdir git')
    run_cmd('.', 'mkdir svn')

def run_cmd(dir_name, cmd):
    cmd_str = 'cd %s && %s' % (dir_name, cmd)
    print cmd_str
    status,output = commands.getstatusoutput(cmd_str)
    print 'status: %d' % status
    print 'output: \n%s' % output
    print '=========================================================\n'

    return status,output

def clone_svn_to_local(svn_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    run_cmd('svn', "svn checkout --username %s --password %s %s %s" % (user_name, user_pwd, svn_addr, repo_dir_name))
    _,output = run_cmd('svn/' + repo_dir_name, "svn log --xml | grep -P '^<author' | sort -u | perl -pe 's/<author>(.*?)<\/author>/$1 = /'")
    lines = output.split("\n")
    for line in lines:
        name = line.split('=')[0].strip()
        users[name] = ('%s = %s <%s@didichuxing.com>' % (name, name, name))

def write_users():
    with open('git/users.txt', 'w+') as f:
        for name in users.values():
            f.write('%s\n' % name)

def git_svn_clone(svn_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    run_cmd('git', 'git svn clone --no-metadata -A users.txt %s %s' % (svn_addr, repo_dir_name))
    run_cmd('git/' + repo_dir_name, 'git svn show-ignore > .gitignore')
    run_cmd('git/' + repo_dir_name, 'git add .gitignore')
    run_cmd('git/' + repo_dir_name, "git commit -m 'Convert svn:ignore properties to .gitignore.'")
    run_cmd('git/' + repo_dir_name, 'cp -Rf .git/refs/remotes/tags/* .git/refs/tags/')
    run_cmd('git/' + repo_dir_name, 'rm -Rf .git/refs/remotes/tags')
    run_cmd('git/' + repo_dir_name, 'cp -Rf .git/refs/remotes/* .git/refs/heads/')
    run_cmd('git/' + repo_dir_name, 'rm -Rf .git/refs/remotes')

def diff_git_and_svn_repos(svn_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    git_repo_path = 'git/' + repo_dir_name
    svn_repo_path = 'svn/' + repo_dir_name
    _,output = run_cmd('.', 'diff -r %s %s' % (git_repo_path, svn_repo_path))
    expected = 'Only in git/%s: .git\nOnly in git/%s: .gitignore\nOnly in svn/%s: .svn' % (repo_dir_name, repo_dir_name, repo_dir_name)
    if expected != output:
        raise Exception('迁移后内容不一致:\n%s' % output)

def push_to_remote(svn_addr, git_addr):
    repo_dir_name = get_repo_dir_name(svn_addr)
    run_cmd('git/' + repo_dir_name, 'git checkout -b svn_migrate_branch')
    run_cmd('git/' + repo_dir_name, 'git remote add origin %s' % git_addr)
    run_cmd('git/' + repo_dir_name, 'git push origin --all')

def get_repo_dir_name(svn_addr):
    return svn_addr.split('/')[-1]

def main():
    init()

    for svn_addr in svn_repos:
        clone_svn_to_local(svn_addr)

    write_users()

    repo_nums = len(git_repos)
    for i in range(0,repo_nums):
        git_svn_clone(svn_repos[i])
        diff_git_and_svn_repos(svn_repos[i])
        push_to_remote(svn_repos[i], git_repos[i])

if __name__ == '__main__':
    main()

4. 参考文献

5. 一点想法

其实大家都能看出来这个脚本大部分都是对 shell 命令的调用。为什么舍近求远呢?
因为本人一直觉得 shell 的内部命令,关于流程控制以及字符串处理等实在不好用。我更倾向于 Python 和 Ruby 这样强大的脚本语言。
其实我对 Python 用的也不是很多,在写脚本的过程中查阅了一些 Python 调用 shell 的资料,也和 Ruby 中类似的代码做个比较。
在上面的脚本中我使用了 commands 包的 getstatusoutput('cmd') 方法 。该函数返回了一个元组,通过序列解包可以同时获得返回值和标准输出流的输出。一般调用方法为:

(status, output) = commands.getstatusoutput('shell 命令')
print status, output

而 Ruby 中也有类似的方法,即 Kernel 模块中的 `cmd` 方法。但是该方法直接返回的只有标准输出,返回值还要靠 $? 来获取。

irb(main):014:0> `echo hello && exit 99`
=> "hello\n"
irb(main):015:0> $?.exitstatus
=> 99

在便利性上,Python 的序列解包使得多个函数返回值变成可能,减少了封装也提高了使用的便利性。