Git
Chapters ▾ 2nd Edition

9.2 Gitとその他のシステムの連携 - Git へ移行する

Git へ移行する

Git 以外のバージョン管理システムで管理しているコードベースがあるけれど、Git を使いはじめることにした、という場合、どうにかしてプロジェクトを移行する必要があります。 この節では、主要なバージョン管理システム用のインポーターについて触れた後、独自のインポーターを自前で開発する方法を実際に見ていきます。 ここでは、いくつかの大きくてプロ仕様のソースコード管理システムからデータをインポートする方法を学びます。これは、移行するユーザの多くがそういったシステムのユーザであるのと、そういったシステムでは高品質なツールが簡単に手に入るためです。

Subversion

先ほどの節で git svn の使い方を読んでいれば、話は簡単です。まず git svn clone でリポジトリを作り、そして Subversion サーバーを使うのをやめ、新しい Git サーバーにプッシュし、あとはそれを使い始めればいいのです。これまでの歴史が欲しいのなら、それも Subversion サーバーからプルすることができます (多少時間がかかります)。

しかし、インポートは完全ではありません。また時間もかかるので、正しくやるのがいいでしょう。まず最初に問題になるのが作者 (author) の情報です。Subversion ではコミットした人すべてがシステム上にユーザーを持っており、それがコミット情報として記録されます。たとえば先ほどの節のサンプルで言うと schacon がそれで、blame の出力や git svn log の出力に含まれています。これをうまく Git の作者データとしてマップするには、Subversion のユーザーと Git の作者のマッピングが必要です。users.txt という名前のファイルを作り、このような書式でマッピングを記述します。

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

SVN で使っている作者の一覧を取得するには、このようにします。

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

これは、まずログを XML フォーマットで生成します。その中から作者の情報を含む行だけを抽出し、重複を削除して、XML タグを除去します。 (ちょっと見ればわかりますが、これは grepsort、そして perl といったコマンドが使える環境でないと動きません) この出力を users.txt にリダイレクトし、そこに Git のユーザーデータを書き足していきます。

このファイルを git svn に渡せば、作者のデータをより正確にマッピングできるようになります。また、Subversion が通常インポートするメタデータを含めないよう git svn に指示することもできます。そのためには --no-metadataclone コマンドあるいは init コマンドに渡します。そうすると、 import コマンドは次のようになります。

$ git svn clone http://my-project.googlecode.com/svn/ \
      --authors-file=users.txt --no-metadata -s my_project

これで、Subversion をちょっとマシにインポートした my_project ディレクトリができあがりました。コミットがこんなふうに記録されるのではなく、

commit 37efa680e8473b615de980fa935944215428a35a
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

    git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de-
    be05-5f7a86268029

次のように記録されています。

commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2
Author: Scott Chacon <schacon@geemail.com>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

Author フィールドの見た目がずっとよくなっただけではなく、git-svn-id もなくなっています。

インポートした後には、ちょっとした後始末も行ったほうがよいでしょう。 たとえば、git svn が作成した変な参照は削除しておくべきです。 まずはタグを移動して、奇妙なリモートブランチではなくちゃんとしたタグとして扱えるようにします。そして、残りのブランチを移動してローカルで扱えるようにします。

タグを Git のタグとして扱うには、次のコマンドを実行します。

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

これは、リモートブランチのうち remotes/origin/tags/ で始まる名前のものを、実際の (軽量な) タグに変えます。

次に、refs/remotes 以下にあるそれ以外の参照をローカルブランチに移動します。

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

このとき、Subversionではブランチが1つだったのにもかかわらず、名前が`@xxx`(xxxは数字)で終わる余分なブランチがいくつか出来てしまうことがあります。Subversionの「ペグ・リビジョン」という機能が原因なのですが、Gitにはこれと同等の機能は存在しません。よって、`git svn`コマンドはブランチ名にsvnのバージョン番号をそのまま追加します。svnでペグ・リビジョンをブランチに設定するときとまさに同じやり方です。もうペグ・リビジョンがいらないのであれば、`git branch -d`コマンドで削除してしまいましょう。

インポートが終わり、過去のブランチはGitのブランチへ、過去のタグはGitのタグへと変換できました。

最後に後始末についてです。残念なことに、`git svn`は`trunk`という名前の余計なブランチを生成してしまいます。Subversionにおけるデフォルトブランチではあるのですが、`trunk`の参照が指す場所は`master`と同じです。`master`のほうが用語としてもGitらしいですから、余分なブランチは削除してしまいましょう。

$ git branch -d trunk

これで、今まであった古いブランチはすべて Git のブランチとなり、古いタグもすべて Git のタグになりました。最後に残る作業は、新しい Git サーバーをリモートに追加してプッシュすることです。自分のサーバーをリモートとして追加するには以下のようにします。

$ git remote add origin git@my-git-server:myrepository.git

すべてのブランチやタグを一緒にプッシュするには、このようにします。

$ git push origin --all
$ git push origin --tags

これで、ブランチやタグも含めたすべてを、新しい Git サーバーにきれいにインポートできました。

Mercurial

Mercurial と Git は、バージョンの表現方法がよく似ており、また Git の方が少し柔軟性が高いので、Mercurial から Git へのリポジトリの変換は非常に素直に行えます。変換には "hg-fast-export" というツールを使用します。このツールは次のコマンドで取得できます。

$ git clone http://repo.or.cz/r/fast-export.git /tmp/fast-export

変換の最初のステップとして、変換の対象となる Mercurial リポジトリのクローンを取得します。

$ hg clone <remote repo URL> /tmp/hg-repo

次のステップでは、author マッピングファイルを作成します。 チェンジセットの author フィールドへ指定できる内容は、Git より Mercurial の方が制限がゆるいので、これを機に内容を見直すのがよいでしょう。 author マッピングファイルは、bash シェルなら次のワンライナーで生成できます。

$ cd /tmp/hg-repo
$ hg log | grep user: | sort | uniq | sed 's/user: *//' > ../authors

プロジェクトの歴史の長さによりますが、このコマンドの実行には数秒かかります。実行後には、 /tmp/authors ファイルが次のような内容で作成されているはずです。

bob
bob@localhost
bob <bob@company.com>
bob jones <bob <AT> company <DOT> com>
Bob Jones <bob@company.com>
Joe Smith <joe@company.com>

この例では、同じ人(Bob)がチェンジセットを作成したときの名前が4パターンあり、そのうち1つだけが標準に合った書き方で、また別の1つは Git のコミットとしては完全に無効なように見えます。 hg-fast-export では、このような状態を修正する場合、修正したい行の末尾に ={修正後の氏名とメールアドレス} を追加し、変更したくないユーザ名の行はすべて削除します。 すべてのユーザ名が正しいなら、このファイルは必要ありません。 この例では、ファイルの内容を次のようにします。

bob=Bob Jones <bob@company.com>
bob@localhost=Bob Jones <bob@company.com>
bob <bob@company.com>=Bob Jones <bob@company.com>
bob jones <bob <AT> company <DOT> com>=Bob Jones <bob@company.com>

次のステップでは、新しい Git リポジトリを作成して、エクスポート用スクリプトを実行します。

$ git init /tmp/converted
$ cd /tmp/converted
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors

hg-fast-export に対して、 -r フラグで、変換の対象となる Mercurial リポジトリの場所を指定しています。また、 -A フラグで、author マッピングファイルの場所を指定しています。 このスクリプトは、Mercurial のチェンジセットを解析して、Git の "fast-import" 機能(詳細はまた後で説明します)用のスクリプトへ変換します。 これには少し時間がかかります(ネットワーク経由の場合と比べれば かなり 速いですが)。出力は非常に長くなります。

$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
Loaded 4 authors
master: Exporting full revision 1/22208 with 13/0/0 added/changed/removed files
master: Exporting simple delta revision 2/22208 with 1/1/0 added/changed/removed files
master: Exporting simple delta revision 3/22208 with 0/1/0 added/changed/removed files
[…]
master: Exporting simple delta revision 22206/22208 with 0/4/0 added/changed/removed files
master: Exporting simple delta revision 22207/22208 with 0/2/0 added/changed/removed files
master: Exporting thorough delta revision 22208/22208 with 3/213/0 added/changed/removed files
Exporting tag [0.4c] at [hg r9] [git :10]
Exporting tag [0.4d] at [hg r16] [git :17]
[…]
Exporting tag [3.1-rc] at [hg r21926] [git :21927]
Exporting tag [3.1] at [hg r21973] [git :21974]
Issued 22315 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:     120000
Total objects:       115032 (    208171 duplicates                  )
      blobs  :        40504 (    205320 duplicates      26117 deltas of      39602 attempts)
      trees  :        52320 (      2851 duplicates      47467 deltas of      47599 attempts)
      commits:        22208 (         0 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:         109 (         2 loads     )
      marks:        1048576 (     22208 unique    )
      atoms:           1952
Memory total:          7860 KiB
       pools:          2235 KiB
     objects:          5625 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =      90430
pack_report: pack_mmap_calls          =      46771
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =  340852700 /  340852700
---------------------------------------------------------------------

$ git shortlog -sn
   369  Bob Jones
   365  Joe Smith

作業はこれだけです。 すべての Mercurial のタグは Git のタグに変換され、 Mercurial のブランチとブックマークは Git のブランチに変換されています。 これで、リポジトリを新しいサーバ側へプッシュする準備が整いました。

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

Perforce

次のインポート元としてとりあげるのは Perforce です。 前述の通り、 Git と Perforce を相互接続するには2つの方法があります。 git-p4 と Perforce Git Fusion です。

Perforce Git Fusion

Git Fusion を使えば、移行のプロセスに労力はほぼかかりません。( Git Fusion で述べた通り)設定ファイルで、プロジェクトの設定、ユーザのマッピング、およびブランチの設定を行った後、リポジトリをクローンすれば完了です。 Git Fusion がネイティブな Git リポジトリと類似の環境を提供してくれるので、お望みとあればいつでも、本物のネイティブな Git リポジトリへプッシュする準備はできているというわけです。 また、お望みなら、 Perforce を Git のホストとして使用することもできます。

git-p4

git-p4 はインポート用ツールとしても使えます。 例として、 Perforce Public Depot から Jam プロジェクトをインポートしてみましょう。 クライアントをセットアップするには、環境変数 P4PORT をエクスポートして Perforce ディポの場所を指すようにしなければなりません。

$ export P4PORT=public.perforce.com:1666
Note

以降の手順に従うには、アクセスできる Perforce のディポが必要です。 この例では public.perforce.com にある公開ディポを使用していますが、アクセス権があればどんなディポでも使用できます。

git p4 clone コマンドを実行して、 Perforce サーバから Jam プロジェクトをインポートします。ディポとプロジェクトのパス、およびプロジェクトのインポート先のパスを指定します。

$ git-p4 clone //guest/perforce_software/jam@all p4import
Importing from //guest/perforce_software/jam@all into p4import
Initialized empty Git repository in /private/tmp/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 9957 (100%)

このプロジェクトにはブランチがひとつしかありませんが、ブランチビューで設定されたブランチ(またはディレクトリ)があるなら、 git p4 clone--detect-branches フラグを指定すれば、プロジェクトのブランチすべてをインポートできます。 この詳細については ブランチ を参照してください。

この時点で作業はおおむね完了です。 p4import ディレクトリへ移動して git log を実行すると、インポートした成果物を確認できます。

$ git log -2
commit e5da1c909e5db3036475419f6379f2c73710c4e6
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

    [git-p4: depot-paths = "//public/jam/src/": change = 8068]

commit aa21359a0a135dda85c50a7f7cf249e4f7b8fd98
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

    [git-p4: depot-paths = "//public/jam/src/": change = 7304]

git-p4 が各コミットメッセージに識別子を追加しているのが分かると思います。 この識別子はそのままにしておいてもかまいません。後で万一 Perforce のチェンジ番号を参照しなければならなくなったときのために使えます。しかし、もし削除したいのなら、新しいリポジトリ上で何か作業を始める前の、この段階で消しておきましょう。 git filter-branch を使えば、この識別子を一括削除することができます。

$ git filter-branch --msg-filter 'sed -e "/^\[git-p4:/d"'
Rewrite e5da1c909e5db3036475419f6379f2c73710c4e6 (125/125)
Ref 'refs/heads/master' was rewritten

git log を実行すると、コミットの SHA-1 チェックサムは変わりましたが、 git-p4 という文字列がコミットメッセージから消えたことが分かると思います。

$ git log -2
commit b17341801ed838d97f7800a54a6f9b95750839b7
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

commit 3e68c2e26cd89cb983eb52c024ecdfba1d6b3fff
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

これで、インポート結果を新しい Git サーバへプッシュする準備ができました。

TFS

あなたのチームで、ソース管理を TFVC から Git へ移行したいということになった場合、できる限り最高の忠実度で変換を行いたいことと思います。 そのため、相互運用についてのセクションでは git-tfs と git-tf の両方を取り上げましたが、本セクションでは git-tfs のみを取り上げます。これは git-tfs はブランチをサポートしている一方、 git-tf ではブランチの使用が禁止されており、対応が難しいためです。

Note

以下で述べるのは、一方通行の変換です。 できあがった Git リポジトリを、元の TFVC プロジェクトと接続することはできません。

最初に行うのはユーザ名のマッピングです。 TFVC ではチェンジセットの author フィールドの内容をかなり自由に設定できますが、 Git では人間に読める形式の名前とメールアドレスが必要です。 この情報は、 tf コマンドラインクライアントで次のようにして取得できます。

PS> tf history $/myproject -recursive > AUTHORS_TMP

このコマンドは、プロジェクトの歴史からすべてのチェンジセットの情報を取ってきて、 AUTHORS_TMP ファイルへ出力します。このファイルは、 User カラム(2番目のカラム)のデータを抽出する際に使用します。 AUTHORS_TMP ファイルを開いて、2番目のカラムの開始位置と終了位置を確認したら、次のコマンドラインの、 cut コマンドの引数 11-20 を、それぞれ開始位置と終了位置で置き換えてください。

PS> cat AUTHORS_TMP | cut -b 11-20 | tail -n+3 | uniq | sort > AUTHORS

この cut コマンドは、各行の11文字目から20文字目だけを抽出します。 また、この tail コマンドは、最初の2行(フィールドヘッダと、下線のアスキーアート)を読み飛ばします。 この処理の結果は、重複を排除するためパイプで uniq コマンドへ送られた上で、 AUTHORS ファイルへ保存されます。 次のステップは手作業です。git-tfs でこのファイルを利用するには、各行は次のフォーマットに従っている必要があります。

DOMAIN\username = User Name <email@address.com>

イコール記号の左側にあるのは TFVC の “User” フィールドの内容、右側にあるのは Git のコミットで使用されるユーザ名です。

このファイルを作りおえたら、次は、対象となる TFVC プロジェクト全体のクローンを作成します。

PS> git tfs clone --with-branches --authors=AUTHORS https://username.visualstudio.com/DefaultCollection $/project/Trunk project_git

次は、コミットメッセージの末尾にある git-tfs-id セクションを消去したいことと思います。 これは、次のコマンドで行えます。

PS> git filter-branch -f --msg-filter 'sed "s/^git-tfs-id:.*$//g"' '--' --all

これは、Git-bash から sed コマンドを使用して、 “git-tfs-id:” で始まる行を空文字列で置き換えます。 Git は空文字列を無視します。

これらをすべて実施したら、新しいリモートを追加したり、ブランチをプッシュしたり、チームが Git で作業を始めたりする準備はこれで完了です。

A Custom Importer

前述した以外のシステムを使っている場合は、それ用のインポートツールをオンラインで探さなければなりません。CVS、Clear Case、Visual Source Safe、あるいはアーカイブのディレクトリなど、多くのバージョン管理システムについて、品質の高いインポーターが公開されています。 これらのツールがうまく動かなかったり、もっとマイナーなバージョン管理ツールを使っていたり、あるいはインポート処理で特殊な操作をしたりしたい場合は git fast-import を使います。 このコマンドはシンプルな指示を標準入力から受け取って、特定の Git データを書き出します。 git fast-import を使えば、生の Git コマンドを使ったり、生のオブジェクトを書きだそうとしたりする(詳細は Gitの内側 を参照してください)よりは、ずっと簡単に Git オブジェクトを作ることができます。 この方法を使えばインポートスクリプトを自作することができます。必要な情報を元のシステムから読み込み、単純な指示を標準出力に出せばよいのです。 そして、このスクリプトの出力をパイプで git fast-import に送ります。

手軽に試してみるために、シンプルなインポーターを書いてみましょう。 current で作業をしており、プロジェクトのバックアップは時々ディレクトリまるごとのコピーで行っているものとします。バックアップディレクトリの名前は、タイムスタンプをもとに back_YYYY_MM_DD としています。これらを Git にインポートしてみましょう。 ディレクトリの構造は、このようになっています。

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Git のディレクトリにインポートするため、まず Git がどのようにデータを格納しているかをおさらいしましょう。 覚えているかもしれませんが、 Git は基本的にはコミットオブジェクトのリンクリストであり、コミットオブジェクトがコンテンツのスナップショットを指しています。 fast-import に指示しなければならないのは、コンテンツのスナップショットが何でどのコミットデータがそれを指しているのかということと、コミットデータを取り込む順番だけです。 ここでは、スナップショットをひとつずつたどって各ディレクトリの中身を含むコミットオブジェクトを作り、それらを日付順にリンクさせるものとします。

Git ポリシーの実施例 と同様、ここでも Ruby を使って書きます。Ruby を使うのは、我々が普段使っている言語であり、読みやすくしやすいためです。 このサンプルをあなたの使いなれた言語で書き換えるのも簡単でしょう。単に適切な情報を標準出力に送るだけなのだから。 また、Windows を使っている場合は、行末にキャリッジリターンを含めないように注意が必要です。git fast-import が想定している行末は LF だけであり、Windows で使われている CRLF は想定していません。

まず最初に対象ディレクトリに移動し、そのサブディレクトリを認識させます。各サブディレクトリがコミットとしてインポートすべきスナップショットとなります。 続いて各サブディレクトリへ移動し、そのサブディレクトリをエクスポートするためのコマンドを出力します。 基本的なメインループは、このようになります。

last_mark = nil

# loop through the directories
Dir.chdir(ARGV[0]) do
  Dir.glob("*").each do |dir|
    next if File.file?(dir)

    # move into the target directory
    Dir.chdir(dir) do
      last_mark = print_export(dir, last_mark)
    end
  end
end

各ディレクトリ内で実行している print_export は、前のスナップショットの内容一覧とマークを受け取って、このディレクトリの内容一覧とマークを返します。このようにして、それぞれを適切にリンクさせます。 “マーク” とは fast-import 用語で、コミットに対する識別子を意味します。コミットを作成するときにマークをつけ、それを使って他のコミットとリンクさせます。 つまり、print_export メソッドで最初にやることは、ディレクトリ名からマークを生成することです。

mark = convert_dir_to_mark(dir)

これを行うには、まずディレクトリの配列を作り、そのインデックスの値をマークとして使います。マークは整数値でなければならないからです。 メソッドの中身はこのようになります。

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end

これで各コミットを表す整数値が取得できました。次に必要なのは、コミットのメタデータ用の日付です。 日付はディレクトリ名に現れているので、ここから取得します。print_export ファイルで次にすることは、これです。

date = convert_dir_to_date(dir)

convert_dir_to_date の定義は次のようになります。

def convert_dir_to_date(dir)
  if dir == 'current'
    return Time.now().to_i
  else
    dir = dir.gsub('back_', '')
    (year, month, day) = dir.split('_')
    return Time.local(year, month, day).to_i
  end
end

これは、各ディレクトリの日付に対応する整数値を返します。 コミットのメタ情報として必要な最後の情報はコミッターのデータで、これはグローバル変数にハードコードします。

$author = 'John Doe <john@example.com>'

これで、コミットのデータをインポーターに流せるようになりました。 最初の情報では、今定義しているのがコミットオブジェクトであることと、どのブランチにいるのかを示しています。その後に先ほど生成したマークが続き、さらにコミッターの情報とコミットメッセージが続いた後にひとつ前のコミットが (もし存在すれば) 続きます。 コードはこのようになります。

# print the import information
puts 'commit refs/heads/master'
puts 'mark :' + mark
puts "committer #{$author} #{date} -0700"
export_data('imported from ' + dir)
puts 'from :' + last_mark if last_mark

タイムゾーン (-0700) をハードコードしているのは、そのほうがお手軽だったからです。 別のシステムからインポートする場合は、タイムゾーンをオフセットとして指定しなければなりません。 コミットメッセージは、次のような特殊な書式にする必要があります。

data (size)\n(contents)

まず最初に「data」という単語、そして読み込むデータのサイズ、改行、最後にデータがきます。 同じ書式は後でファイルのコンテンツを指定するときにも使うので、ヘルパーメソッド export_data を作ります。

def export_data(string)
  print "data #{string.size}\n#{string}"
end

残っているのは、各スナップショットが持つファイルのコンテンツを指定することです。 今回の場合はどれも一つのディレクトリにまとまっているので簡単です。deleteall コマンドを出力し、それに続けてディレクトリ内の各ファイルの中身を出力すればよいのです。 そうすれば、Git が各スナップショットを適切に記録します。

puts 'deleteall'
Dir.glob("**/*").each do |file|
  next if !File.file?(file)
  inline_data(file)
end

注意:多くのシステムではリビジョンを「あるコミットと別のコミットの差分」と考えているので、fast-importでもその形式でコマンドを受け取ることができます。つまりコミットを指定するときに、追加/削除/変更されたファイルと新しいコンテンツの中身で指定できるということです。 各スナップショットの差分を算出してそのデータだけを渡すこともできますが、処理が複雑になります。すべてのデータを渡して、Git に差分を算出させたほうがよいでしょう。 もし差分を渡すほうが手元のデータに適しているようなら、fast-import のマニュアルで詳細な方法を調べましょう。

新しいファイルの内容、あるいは変更されたファイルと変更後の内容を表す書式は次のようになります。

M 644 inline path/to/file
data (size)
(file contents)

この 644 はモード (実行可能ファイルがある場合は、そのファイルについては 755 を指定する必要があります) を表し、inline とはファイルの内容をこの次の行に続けて指定するという意味です。inline_data メソッドは、このようになります。

def inline_data(file, code = 'M', mode = '644')
  content = File.read(file)
  puts "#{code} #{mode} inline #{file}"
  export_data(content)
end

先ほど定義した export_data メソッドを再利用することができます。この書式はコミットメッセージの書式と同じだからです。

最後に必要となるのは、現在のマークを返して次の処理に渡せるようにすることです。

return mark
Note

Windows 上で動かす場合はさらにもう一手間必要です。 先述したように、Windows の改行文字は CRLF ですが git fast-import は LF にしか対応していません。この問題に対応して git fast-import をうまく動作させるには、CRLF ではなく LF を使うよう ruby に指示しなければなりません。

$stdout.binmode

これで終わりです。 スクリプト全体を以下に示します。

#!/usr/bin/env ruby

$stdout.binmode
$author = "John Doe <john@example.com>"

$marks = []
def convert_dir_to_mark(dir)
    if !$marks.include?(dir)
        $marks << dir
    end
    ($marks.index(dir)+1).to_s
end

def convert_dir_to_date(dir)
    if dir == 'current'
        return Time.now().to_i
    else
        dir = dir.gsub('back_', '')
        (year, month, day) = dir.split('_')
        return Time.local(year, month, day).to_i
    end
end

def export_data(string)
    print "data #{string.size}\n#{string}"
end

def inline_data(file, code='M', mode='644')
    content = File.read(file)
    puts "#{code} #{mode} inline #{file}"
    export_data(content)
end

def print_export(dir, last_mark)
    date = convert_dir_to_date(dir)
    mark = convert_dir_to_mark(dir)

    puts 'commit refs/heads/master'
    puts "mark :#{mark}"
    puts "committer #{$author} #{date} -0700"
    export_data("imported from #{dir}")
    puts "from :#{last_mark}" if last_mark

    puts 'deleteall'
    Dir.glob("**/*").each do |file|
        next if !File.file?(file)
        inline_data(file)
    end
    mark
end


# Loop through the directories
last_mark = nil
Dir.chdir(ARGV[0]) do
    Dir.glob("*").each do |dir|
        next if File.file?(dir)

        # move into the target directory
        Dir.chdir(dir) do
            last_mark = print_export(dir, last_mark)
        end
    end
end

このスクリプトを実行すれば、次のような結果が得られます。

$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer John Doe <john@example.com> 1388649600 -0700
data 29
imported from back_2014_01_02deleteall
M 644 inline README.md
data 28
# Hello

This is my readme.
commit refs/heads/master
mark :2
committer John Doe <john@example.com> 1388822400 -0700
data 29
imported from back_2014_01_04from :1
deleteall
M 644 inline main.rb
data 34
#!/bin/env ruby

puts "Hey there"
M 644 inline README.md
(...)

インポーターを動かすには、インポート先の Git レポジトリにおいて、インポーターの出力をパイプで git fast-import に渡す必要があります。 インポート先に新しいディレクトリを作成したら、以下のように git init を実行し、そしてスクリプトを実行してみましょう。

$ git init
Initialized empty Git repository in /opt/import_to/.git/
$ ruby import.rb /opt/import_from | git fast-import
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:           13 (         6 duplicates                  )
      blobs  :            5 (         4 duplicates          3 deltas of          5 attempts)
      trees  :            4 (         1 duplicates          0 deltas of          4 attempts)
      commits:            4 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              2
Memory total:          2344 KiB
       pools:          2110 KiB
     objects:           234 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =         10
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          2 /          2
pack_report: pack_mapped              =       1457 /       1457
---------------------------------------------------------------------

ご覧のとおり、処理が正常に完了すると、処理内容に関する統計情報が表示されます。 この場合は、全部で 13 のオブジェクトからなる 4 つのコミットが 1 つのブランチにインポートされたことがわかります。 では、git log で新しい歴史を確認しましょう。

$ git log -2
commit 3caa046d4aac682a55867132ccdfbe0d3fdee498
Author: John Doe <john@example.com>
Date:   Tue Jul 29 19:39:04 2014 -0700

    imported from current

commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <john@example.com>
Date:   Mon Feb 3 01:00:00 2014 -0700

    imported from back_2014_02_03

きれいな Git リポジトリができていますね。 ここで重要なのは、この時点ではまだ何もチェックアウトされていないということです。作業ディレクトリには何もファイルがありません。 ファイルを取得するには、ブランチをリセットして master の現在の状態にしなければなりません。

$ ls
$ git reset --hard master
HEAD is now at 3caa046 imported from current
$ ls
README.md main.rb

fast-import ツールにはさらに多くの機能があります。さまざまなモードを処理したりバイナリデータを扱ったり、複数のブランチやそのマージ、タグ、進捗状況表示などです。 より複雑なシナリオのサンプルは Git のソースコードの contrib/fast-import ディレクトリにあります。