svn2git(svn-all-fast-export)でSVNリポジトリーをGitのリポジトリーに転送する


背景: git-svnを使ったらうまくいかなかった

日本語でググるとgit-svnを使用した方法がヒットしたので、

git svn clone <移行元のSVNリポジトリー> -T trunk -b branches -t tags

と言うコマンドでひとまずローカルにcloneしようと試みたものの、なぜか途中で失敗してしまった。
出てきたエラーメッセージで検索したところ、 https://stackoverflow.com/questions/42534064/git-svn-clone-fails-with-error-git-svn-died-of-signal-11 というStackOverflowでの質問が見つかった。
2番目の回答曰く、 https://github.com/svn-all-fast-export/svn2git など、もっと移行に特化したツールを使え、とのことなのでこちらのsvn2gitを使うことにした。

svn-all-fast-export (KDEが作ったsvn2git)を使う

実行した環境

手順1 svn-all-fast-export (svn2git)のビルド

最初に、KDEが作ったsvn2git(どうもsvn2gitという安直な名前のツールは他にもたくさんあるらしいのでここでは「KDEが作った」と明記している)をインストールする。(ややこしいのでここから先はsvn-all-fast-exportと呼ぼう。)
残念ながらバイナリーパッケージは提供されていないらしいので、依存パッケージをインストールしつつビルドする1

sudo apt install qt5-default qt4-qmake # 試行錯誤の過程でインストールしたが、多分実際にはqt5-defaultのみでよい。
sudo apt install libsvn-dev libapr1-dev
git clone https://github.com/svn-all-fast-export/svn2git.git 
cd svn2git
qmake
make

ここまでのコマンドで、cloneしたディレクトリーにsvn-all-fast-exportのバイナリーが作られる。
実際に作られる実行ファイルの名前はsvn2gitではなく svn-all-fast-exportなので、./svn-all-fast-export で実行すればよい。

手順2 svnsyncで手元にSVNリポジトリーのcloneを作る

svn-all-fast-exportはそもそも1つのサーバーの中で、SVNリポジトリーをGitリポジトリーに変換するためのツールなので、ファイルシステム上にあるSVNリポジトリーしか参照できない。
参考: https://github.com/svn-all-fast-export/svn2git/issues/3

つまり、リポジトリーが入ったサーバーの中でsvn-all-fast-exportを実行する必要があるのだが、そんな権限はないしsvn-all-fast-exportをビルドできるかどうかも怪しい2ので、手元のUbuntu上でsvnsyncを使うことで一時的なSVNリポジトリーのクローンを作り、それをsvn-all-fast-exportでGitリポジトリーに変換することにした。

svnsyncの使い方については https://m-tmatma.github.io/svndoc/svn_svnsync.html を参考にした。

sudo apt install subversion # svnsyncはsubversionにバンドルされている
svnadmin create <SVNリポジトリーのクローンの名前。適当でよい。>
cd <SVNリポジトリーのクローンの名前>
# pre-revprop-change というhookを作る必要がある。中身は実質空のスクリプトでよいらしい。
cat <<END > hooks/pre-revprop-change
#!/bin/sh
exit 0
END
chmod +x hooks/pre-revprop-change
svnsync init file://`pwd` <クローン対象のSVNリポジトリーのURL> --source-username <クローン対象のSVNリポジトリーでのユーザー名> # SVNのユーザー名を明示しないと、OSのユーザー名が使われてしまう
# ここでsvnのパスワードを入力する #
svnsync sync file://`pwd` # SVNリポジトリーのクローンを作成開始

せっかくなのでここまでの作業で作ったSVNリポジトリーの中身を覗いてみよう。

svnserve -d --foreground --listen-port 8081 --root .

でSVNのサーバーを起動する。上記の例ではportを8081, 対象のディレクトリーをカレントディレクトリーに設定しているので、

svn://localhost:8081/

にアクセスすれば、正しくリポジトリーが作られたことを確認できる(私の環境ではVirtualBoxのNATを使って設定していたので、8081を転送するようにした)。

手順3 svn-all-fast-exportのrulesの記述・実行

手順2でSVNリポジトリーのクローンを作れたので、手順1でビルドしたsvn-all-fast-exportを使ってGitリポジトリーに変換しよう。
その前に、svn-all-fast-exportのrulesファイルというものを書く必要がある。
rulesファイルは、対象のSVNリポジトリーにおけるtagやbranchやtrunkなどといったフォルダーを、どのGitリポジトリーに転送するか、どのGitのブランチに割り当てるか、と言ったことを定義するファイルである。
下記は我らがチームで実際に使用したrulesファイルを元に、環境固有の情報を削ったり解説を充実させたものである。
実行中にエラーが発生するbranchやtagを回避する方法もメモしてあるので適宜修正してご利用いただきたい。

svn-all-fast-export-rules
# Copied from https://github.com/svn-all-fast-export/svn2git/blob/master/samples/merged-branches-tags.rules

#
# Declare the repositories we know about:
#

create repository git-repository
end repository

#
# Declare the rules
# Note: rules must end in a slash
#

match /trunk/
  repository git-repository
  branch master
end match

# 下記のようなエラーが出るため、問題のtagはスキップする。
# 参考: https://github.com/svn-all-fast-export/svn2git/issues/26
# Exporting revision 91340     /tags/broken_tag1 was copied from /tags rev 91339
# .git-repository : branch broken_tag1 is branching from tags
# "broken_tag1" in repository "git-repository" is branching from branch "tags" but the latter doesn't exist. Can't continue.
# 正規表現でマッチしているようなので、(broken_tag1|broken_tag2|broken_tag3)と書けば複数指定できる。
match /tags/(broken_tag1)/
end match

# Subversion doesn't understand the Git concept of tags
# In Subversion, tags are really branches
#
# Only a post-processing (i.e., after converting to Git) of the tag
# branches can we be sure that a tag wasn't moved or changed from the
# branch it was copied from
#
# So we don't pretend that SVN tags are Git tags and then import
# everything as one

match /(branches|tags)/([^/]+)/
  repository git-repository
  branch \2
end match

# どうもbranchesでもtagsでもないディレクトリーがあるらしいので、ひとまずそういうブランチとして作ってしまう
match /(.*)/
  repository git-repository
  branch \1
end match

なお、上記のファイルを使った方法では、tagもbranchもGitのbranchとして移行されるのでその点はご注意を。
「SVNのtagは実質branchなので、branchにした方がいいよ」みたいな感じでsvn-all-fast-exportが推奨していたためである。
svn-all-fast-exportのrulesファイルのサンプルを読む限り、どうもSVNのtagを単純にGitのtagにマッピングすることはできないらしい。
特にtagとbranchに同じ名前のものがある、みたいな場合はエラーになると思われるのでご注意願いたい。
rulesファイルが書けたら下記のコマンドで同期を開始しよう。

./svn-all-fast-export --rules /path/to/svn-all-fast-export-rules `pwd`

SVNリポジトリーのパスはフルパスでないといけないので注意。
上記のファイルに書いた方法では回避できないエラーも発生することが何度かあったが、再実行すればちゃんと途中から再開された。

手順4 git pushしてGitリポジトリーに転送する

同期ができたら、下記のコマンドを実行して、対象のGitのリモートリポジトリー(予めGitHubやGitLabなどで作っておく必要がある)にpushしよう。

cd /path/to/git/repository/
git remote add origin <対象のGitのリモートリポジトリーのURL>
git push --all origin

以上!


  1. [Ubuntu 16.10ならaptリポジトリーにある]https://launchpad.net/ubuntu/yakkety/+package/svn-all-fast-export)ので、そちらを使ってももいい。 

  2. もちろん、インフラ担当に相談してやってもらうこともできただろう。というか、結構時間がかかったので今思えばそうすべきだったかも...。