Skip to content

Latest commit

 

History

History
487 lines (422 loc) · 24.1 KB

import-custom.asc

File metadata and controls

487 lines (422 loc) · 24.1 KB

A Custom Importer

前述した以外のシステムを使っている場合は、それ用のインポートツールをオンラインで探さなければなりません。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 ですが git fast-import は LF にしか対応していません。この問題に対応して git fast-import をうまく動作させるには、CRLF ではなく LF を使うよう ruby に指示しなければなりません。

$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 ディレクトリにあります。