CVS レポジトリを Git に変換した手順とか注意点とか

この前、10 年以上前に趣味で作っていたフリーソフトについてメールで質問が来た。もはや完全に記憶から消えているだけでなく、いま使っている PC にソースコードもない。何も分からない、答えられない。

そのままでは古いソースコードも成仏しきれない。供養するために、古い HDD を引っ張り出して探したところ、自宅サーバーをやってた HDD の中に CVS レポジトリーが見つかった。せっかくなので、Git に変換して GitHub で公開してみた (その1, その2)。これで成仏できるだろう。

そこで、この記事では CVS レポジトリーを Git に移行した手順をまとめておく。レガシーな CVS から Git に移行したい人の参考になるとうれしい。

git cvsimport の使い方

Git には git-cvsimport というコマンドがある。CVS の履歴を Git に変換してくれる。

CVS はファイルごとに履歴を保存する構造になってるんだけど、cvsps というツールを組み合わせることで、同時に変更したファイルを 1 コミットとして扱ってくれるようになる。

大きいレポジトリーだと結構時間かかる。cygwin だと超絶に時間がかかったので、UNIX 上で実行した。Git for Windows には git-cvsimport がない。

自分は次のように実行した。

git cvsimport -v -i -R -A author-conv-file.txt \
-d :local:/path/to/CVS <module>

それぞれのパラメータの意味は次の通り。詳しくは git-cvsimport を参照のこと。

  • -v: 出力を verbose にする。
  • -i: インポートだけを行ってチェックアウトしない。
  • -R: 「CVS のリビジョン番号」と「それに対応する Git のコミット」の対応付けを .git/cvs-revisions に出力する。一応生成しておく。
  • -A: ユーザー名の変換テーブル。詳細は後述。
  • -d: CVSROOT のパス。cvs コマンドで -d に指定するやつ。もしくは、環境変数の CVSROOT に指定してるやつ。
  • <module>: モジュール名。サブディレクトリを指定することもできる。

-d で指定したパスに CVSROOT ディレクトリがないと Expected Valid-requests from server, but got: E Cannot access /path/to/CVS というエラーになる。ダミーでよいので mkdir /path/to/CVS/CVSROOT しておけば回避できた。

author-conv-file の書き方

author-conv-file には「CVS のユーザー名」から「Git の Author 名・メールアドレス」への対応を記述していく。

user1=User 1 <user1@example.com>
user2=User 2 <user1@example.com> Asia/Tokyo

1.8.1 からはタイムゾーンも指定できるようになっている。それ以前のバージョンだと、すべて UTC でのコミットとなってしまう。

実行が終わったら git log | grep Author: | sort | uniq を実行して、author-conv-file に漏れがないか確認しておくといいだろう。

cvsps のキャッシュ

個人的に悩まされたのが、cvsps が結果を ~/.cvsps にキャッシュすること。CVS レポジトリーを変更したあとに git cvsimport を実行しても反映されない。そういうときは、rm -rf ~/.cvsps して再実行するとよい。

文字コードの変換

いまなら「全部 UTF-8 でやっちゃえ」となるんだけど、CVS 全盛の時代は Windows は ShiftJIS で、UNIX 上では EUC-JP がまだまだ主流だった。

コミットログやファイル名が UTF-8 でない場合は、ちょいとソースコードを修正する必要がある。

以下は 1.7.3.1 の git-cvsimport に対する修正。

--- /usr/local/libexec/git-core/git-cvsimport.bak
+++ /usr/local/libexec/git-core/git-cvsimport
@@ -722,6 +722,11 @@
 sub update_index (\@\@) {
        my $old = shift;
        my $new = shift;
+
+       # file name
+       use Encode qw/decode encode/;
+       $_ = encode('utf8', decode('shift-jis', $_)) for @$old;
+       $_->[2] = encode('utf8', decode('shift-jis', $_->[2])) for @$new;
+
        open(my $fh, '|-', qw(git update-index -z --index-info))
                or die "unable to open git update-index: $!";
        print $fh
@@ -810,6 +816,11 @@
        substr($logmsg,32767) = "" if length($logmsg) > 32767;
        $logmsg =~ s/[\s\n]+\z//;

+       # commit log
+       use Encode qw/encode decode/;
+       $logmsg = encode('utf8', decode('shift-jis', $logmsg));
+
        if (@skipped) {
            $logmsg .= "\n\n\nSKIPPED:\n\t";
            $logmsg .= join("\n\t", @skipped) . "\n";

ここではコミットログやファイル名が Shift_JIS だという前提で書いてある。

自動判定したい場合は

+       use Encode::Guess qw/euc-jp shiftjis/;
+       $logmsg = encode('utf8', decode('guess', $logmsg));

のように書けばいける。

歴史の書き換え

push する前に、インポート結果を十分に確認しておきたい。次のような点に注意して作業した。

コミットログを書き換えたいなら git rebase -i でやっちゃう。

不要なファイルがあったら git filter-branch --tree-filter 'rm -f passwords.txt' HEAD のようにして消しておく (参照: Git - 歴史の書き換え)。

一度でも歴史を書き換えたら、commit date が実行時刻になってしまうので、git filter-branch --env-filter 'GIT_COMMITTER_DATE=$GIT_AUTHOR_DATE; export GIT_COMMITTER_DATE' で author date にそろえておく (参照: git rebase without changing commit timestamps - Stack Overflow)。

このあたりは git-cvsimport の Tips というよりも、Git での歴史書き換えの Tips。

まとめ

負の遺産 CVS を捨てて、健全な Git ライフを送ろう!