1.前言
最近组里要把之前在 SVN 上管理的项目迁移到 SVN 上去,所以花了一个晚上研究了一下 SVN 向 Git 迁移的方法。这里首先给出一种能保存提交记录的方案。
2.实施准备
再实施迁移之前,有几件事情是必须做的:
- 获取 SVN 路径权限
- 将本机的公钥提交到 Git 服务器上
- 在 Git 服务器上建立仓库
3.实施过程
实施的过程分为以下几个步骤:
- 将 SVN checkout 到本地
- 获取用户名列表
- 用 git svn clone 创建 Git 仓库
- 导出 .gitignore 文件
- 调整分支和标签
- 推送到远程
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 的序列解包使得多个函数返回值变成可能,减少了封装也提高了使用的便利性。