前述した以外のシステムを使っている場合は、それ用のインポートツールをオンラインで探さなければなりません。CVS、Clear Case、Visual Source Safe、あるいはアーカイブのディレクトリなど、多くのバージョン管理システムについて、品質の高いインポーターが公開されています。
これらのツールがうまく動かなかったり、もっとマイナーなバージョン管理ツールを使っていたり、あるいはインポート処理で特殊な操作をしたりしたい場合は git fast-import
を使います。
このコマンドはシンプルな指示を標準入力から受け取って、特定の Git データを書き出します。
git fast-import
を使えば、生の Git コマンドを使ったり、生のオブジェクトを書きだそうとしたりする(詳細は ch10-git-internals.asc を参照してください)よりは、ずっと簡単に 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
に指示しなければならないのは、コンテンツのスナップショットが何でどのコミットデータがそれを指しているのかということと、コミットデータを取り込む順番だけです。
ここでは、スナップショットをひとつずつたどって各ディレクトリの中身を含むコミットオブジェクトを作り、それらを日付順にリンクさせるものとします。
ch08-customizing-git.asc と同様、ここでも 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 <[email protected]>'
これで、コミットのデータをインポーターに流せるようになりました。 最初の情報では、今定義しているのがコミットオブジェクトであることと、どのブランチにいるのかを示しています。その後に先ほど生成したマークが続き、さらにコミッターの情報とコミットメッセージが続いた後にひとつ前のコミットが (もし存在すれば) 続きます。 コードはこのようになります。
# 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 ですが $stdout.binmode |
これで終わりです。 スクリプト全体を以下に示します。
#!/usr/bin/env ruby
$stdout.binmode
$author = "John Doe <[email protected]>"
$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 <[email protected]> 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 <[email protected]> 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 <[email protected]>
Date: Tue Jul 29 19:39:04 2014 -0700
imported from current
commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <[email protected]>
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
ディレクトリにあります。