Nov 01, 2010
SubversionからGitに移行した
アドオンの開発にはずっと須藤さんに用意してもらったSubversionのリポジトリを使ってたんだけど、
- インターネットに繋がらない状態でコミットできないのが辛い
- 他の人からの変更を受け取りづらい(そんな事があればの話なんだけど)
と思っていて、Git(あるいは他の分散型バージョン管理システム)ならそれが解消されると期待してて、でもずっと移行できていなかった。
- コマンドライン操作が嫌いなので、Subversionを使うのにWindowsではTortoiseSVNを、LinuxではRabbitVCS(旧称:NautilusSVN)を使ってるけど、GitではWindowsにはTortoiseGitがあってもLinuxでは相当する物が無いみたい。
- バージョン管理システムを移行するとそれまでのコミットの履歴も全部なくなっちゃうんじゃないの?
- git-svnっていうのを使うといいと聞いたけど、試しても空のディレクトリしかできないんですけど……
というのがその理由だった。でも
- git-svnは時々動作がうまくいかない事があるみたいで、Windows上のmsysgitが特に高確率で失敗するだけで、VirtualPC上のUbuntu 10.04LTSのgit-svnだとそれほどでもなかった。
- それで成功したケースだとコミット履歴が完全に引き継がれてた。なんだ、ちゃんと動けばちゃんとやってくれるんじゃん!!!
- 最近、システムモニターの開発でGitをずっと使ってたから、まあLinux上ではコマンドラインでも別にいいかな……という気がしてきた。
という事で、思い切って移行してみる事にしました。具体的な手順はSourceForge.JP のプロジェクトを Subversion から Git へ移行するに従いました。
SubversionとGit
大まかに言うとこういう事だと僕は理解してる。
- Subversionでは、中央に1つのリポジトリがあって、それを各人がチェックアウトしてワーキングコピーを作り、ワーキングコピーに対して行った変更をリポジトリにコミットする、という形で「バージョンの管理」と「成果の共有」を実現する。
- Gitでは、リポジトリのコピーを誰でも簡単に手元に作る事ができて(Gitではこれをcloneと言う)、Subversionでワーキングコピーからリポジトリに変更点をコミットするのと同じように、Gitではリポジトリとリポジトリの間で変更点をコミットできる(Gitではこれをpushとかpullとか言う)。
- どれか1つのリポジトリを「中央のリポジトリ」として運用すれば、Subversionでのバージョン管理と同じような運用もできる。
- 手元にできるのは「履歴の情報を持たないワーキングコピー」ではなく「元のリポジトリを複製したリポジトリそのもの」なので、インターネットに繋がってない状態でもローカルのリポジトリに対してコミットできる。
- Subversionではtrunkと呼ばれていた物は、Gitではmasterと呼ばれる。
- git-svnは、以下のような事をする。
- Subversionのリポジトリのコミット履歴を全部辿る。
- ローカルに新しいGitリポジトリを作って、Subversionのコミット履歴に対応するコミットを順番にそのGitリポジトリにコミットする。
- なので、git-svnでGitリポジトリ化したSubversionリポジトリは、元のリポジトリの変更履歴を完全に引き継いだ物になる。
やってみる
ツリー型タブのSubversionリポジトリをgithub上の同名のリポジトリに移行する手順を振り返ってみる。
- Subversionのリポジトリをgit-svnでGitリポジトリに変換する。
$ git svn clone --prefix svn/ -s https://www.cozmixng.org/repos/piro/treestyletab
これでローカルにtreestyletabという名前のディレクトリができて、これがGitリポジトリになってる。コミットを1件1件取り込むので、コミット数が多いとメチャメチャ時間がかかるけど、黙って待つ。たまに失敗するので、そういう時はできたゴミディレクトリを消してもう一度やり直す。 - githubに「treestyletab」という名前でリポジトリを作る。(分かりやすくするためにSubversionの物と同じ名前にするといい。リポジトリ名は後でgithub上で好きなように変更できるので。)これで、読み書き両用のURLとして「git@github.com:piroor/treestyletab.git」みたいなのが使えるようになる。
- ローカルにできたtreestyletabのGitリポジトリの「元」として、githubのリポジトリを登録する。
$ git remote add origin git@github.com:piroor/treestyletab.git
これによって、「このリポジトリはgithubのリポジトリをcloneした物ですよ」「このリポジトリに行われた変更を元のリポジトリにpushする時は、githubにpushしますよ」ということになる。 - pushする。
$ git push origin master
これで、Subversionのリポジトリから持ってきたコミット履歴が全部github上のリポジトリに反映される。 - Subversionのtagsの内容をGitのtagに変換する。Subversionでtags以下に作ったタグはブランチとして取り込まれているので、これをGitのタグにしてやる。
$ git branch -r
これでブランチの一覧を見れるので、$ git checkout svn/tags/0.10.2010102501
という風にしてブランチを切り替えて$ git tag 0.10.2010102501
でタグを打って$ git push --tags origin
でタグの情報をgithubのリポジトリにpushする。タグの数だけこの操作を繰り返す。 - Subversionのbranchesにブランチがある場合、Gitのbranchに変換する。Gitだとローカルにあるブランチをgit push origin ローカルリポジトリのブランチ名:リモートリポジトリのブランチ名でリモートリポジトリにpushできるんだけど、git−svnでcloneしたローカルリポジトリにあるブランチはそのままだとpushできない(不可視のブランチになってる?)ようなので、
$ git checkout svn/my-branch
でブランチを切り替えて$ git branch my-branch
で普通のブランチとして切り直して$ git push origin my-branch:my-branch
でgithubにpushする。
自分はアドオンの数自体20個以上あるし、それぞれアホみたいに何度もリリースしててタグの数がハンパないことになってるので、全部手動でやることを考えたら気が遠くなりました。なのでこういうスクリプトをRubyで書いてみました。
#!/usr/bin/ruby
SVN_REPOSITORY_PATH = "https://www.cozmixng.org/repos/piro/<%= src_project_name %>"
GIT_REPOSITORY_PATH = "git@github.com:piroor/<%= dest_project_name %>.git"
$LOAD_PATH.unshift(File.dirname(__FILE__))
require "fileutils"
require "erb"
require "shellwords"
def main
ARGV.each do |arg|
p "process #{arg}"
args = arg.split(":")
src_project_name = args[0]
dest_project_name = args.size > 1 ? args[1] : args[0]
svn_to_git(src_project_name, dest_project_name)
end
end
def svn_to_git(src_project_name, dest_project_name)
p "svn:#{src_project_name}, git:#{dest_project_name}"
clone(src_project_name)
push(src_project_name, dest_project_name)
push_branches_and_tags(src_project_name)
rescue Exception
p $!
end
def clone(src_project_name)
result = run("git", "svn", "clone",
"--prefix", "svn/",
"-s", ERB.new(SVN_REPOSITORY_PATH).result(binding).chomp)
p result.to_s
raise Exception.new("clone of #{src_project_name}, #{result.to_s}") unless result.to_s.include?("Checked out HEAD:")
end
def push(src_project_name, dest_project_name)
FileUtils.cd(src_project_name) do
result = run("git", "remote", "add",
"origin", ERB.new(GIT_REPOSITORY_PATH).result(binding).chomp)
p result.to_s
result = run("git", "push", "origin", "master")
p result.to_s
raise Exception.new("push of #{src_project_name}, #{result.to_s}") unless result.to_s.include?("master -> master")
end
end
def push_branches_and_tags(src_project_name)
FileUtils.cd(src_project_name) do
branches = run("git", "branch", "-r")
branches = branches.to_s.split("\n")
branches.each do |branch|
next unless branch.include?("svn/")
branch.strip!
name = branch.split("svn/")[1]
next if name == "trunk"
if name.include?("tags/")
tag = branch.split("tags/")[1]
p run("git", "checkout", branch)
p run("git", "tag", tag)
p run("git", "push", "--tags", "origin")
else
p run("git", "checkout", branch)
p run("git", "branch", name)
p run("git", "push", "origin", "#{name}:#{name}")
end
end
end
end
def run(*args)
command_line = Shellwords.shelljoin(args)
result = `#{command_line} 2>&1`
result
end
main
ファイル名は svn-to-git.rb として、
$ ./svn-to-git.rb treestyletab
とやると、ここまでの手順のうちgithubのサイト上でリポジトリを作る所以外を全部自動でやってくれるという物です(ということは、スクリプトの実行前にあらかじめgithubのサイト上でリポジトリを作っておかないといけない)。これでなんとか全部のリポジトリをgithubに持ってくることができました。XUL/Migemoの辞書をSQLiteにしてみようとかそういうブランチを切ってた物が取り込めてなかったり、svn:externalsで参照してた物が入ってなかったり、Subversionに突っ込んでから1回もコミットしてないプロジェクトをまだgithubに持ってきてなかったり(必要あるの? 無いよね?)という課題は残っていますが。→ブランチの取り込み方が分かったので追記しました。→svn:externalsの移行は諦めてsubmoduleにすることにしました。TortoiseGitでメニューから「Submodule Add」を選んでリポジトリにgit@github.com:piroor/makexpi.gitを、パスにbuildscriptを指定する、という手順でだいたい同じような結果になるみたい。更新の時はTortoiseGitだと「Git Sync」から「Submodule Sync」しないといけないようだ。コマンドラインなら一発で更新できるようなんだけど……
今後はgit-svn駆け込み寺あたりを熟読して頑張っていきたいと思っております。あと、今後具体的にコードを提供してくれるような人がもしいれば、githubの方にpull requestっていうんですか?するようにしてもらえたら幸いです。
……というまとめエントリを書こうとしてもうちょっと調べ直してたら、git-svnでタグが自動で取り込まれないとかの問題を解消するラッパーのsvn2gitという物があるということを今更知りました。ギャフン!!!!!
……さらに後から気がついたけど、HTTPでアクセスできる公開のSubversionリポジトリをgithubに移行するだけならtagやbranchの変換も含めてgithubのWebインターフェース上の機能だけでサクッとできてしまうことが分かりました。ギャフンギャフン!!!!!!!!! 手順は以下の通りです。
- githubで新しいリポジトリを作る。
- 空のリポジトリができたら、最初に表示されてる説明の下の方にある「Subversionのリポジトリを取り込みたい? ここをクリック」というリンク(日本語のUIにしてる場合。英語でも多分似たような文言があると思う。)を辿る。
- インポート元のSubversionリポジトリのURLを入力する。
- githubのユーザ名( 「piroor <piro.outsider.reflex@gmail.com>」のような、githubのユーザ名と登録済みのメールアドレスの組み合わせ)を作者として入力する。
- しばらく待つ。
最初githubのUIを英語で使ってたのと、下までスクロールしてなかったから気がついてなかった。なんということでしょう。丸1日以上を無駄にしてしまいました。
wikieditish message: Ready to edit this entry.