Git --distributed-is-the-new-centralized
Chapters ▾

7.2 Git のカスタマイズ - Git の属性

Git の属性

設定項目の中には、パスにも指定できるものがあります。Git はその設定を、指定したパスのサブディレクトリやファイルにのみ適用するのです。これらのパス固有の設定は Git の属性と呼ばれ、あるディレクトリ (通常はプロジェクトのルートディレクトリ) の直下の .gitattributes か、あるいはそのファイルをプロジェクトとともにコミットしたくない場合は .git/info/attributes に設定します。

属性を使うと、ファイルやディレクトリ単位で個別のマージ戦略を指定したりテキストファイル以外での diff の取得方法を指示したり、あるいはチェックインやチェックアウトの前に Git にフィルタリングさせたりすることができます。このセクションでは、Git プロジェクトでパスに設定できる属性のいくつかについて学び、実際にその機能を使う例を見ていきます。

バイナリファイル

Git の属性を使ってできるちょっとした技として、どのファイルがバイナリファイルなのかを (その他の方法で判別できない場合のために) 指定して Git に対してバイナリファイルの扱い方を指示するというものがあります。たとえば、機械で生成したテキストファイルの中には diff が取得できないものがありますし、バイナリファイルであっても diff が取得できるものもあります。それを Git に指示する方法を紹介します。

バイナリファイルの特定

テキストファイルのように見えるファイルであっても、何らかの目的のために意図的にバイナリデータとして扱いたいこともあります。たとえば、Mac の Xcode プロジェクトの中には .pbxproj で終わる名前のファイルがあります。これは JSON (プレーンテキスト形式の javascript のデータフォーマット) のデータセットで、IDE がビルド設定などをディスクに書き出したものです。すべて ASCII で構成されるので、理論上はこれはテキストファイルです。しかしこのファイルをテキストファイルとして扱いたくはありません。実際のところ、このファイルは軽量なデータベースとして使われているからです。他の人が変更した内容をマージすることはできませんし、diff をとってもあまり意味がありません。このファイルは、基本的に機械が処理するものなのです。要するに、バイナリファイルと同じように扱いたいということです。

すべての pbxproj ファイルをバイナリデータとして扱うよう Git に指定するには、次の行を .gitattributes ファイルに追加します。

*.pbxproj -crlf -diff

これで、Git が CRLF 問題の対応をすることもなくなりますし、git showgit diff を実行したときにもこのファイルの diff を調べることはなくなります。また、次のようなマクロbinaryを使うこともできます。これは -crlf -diff と同じ意味です。

*.pbxproj binary

バイナリファイルの差分

Gitでは、バイナリファイルの差分を効果的に扱うためにGitの属性機能を使うことができます。通常のdiff機能を使って比較を行うことができるように、バイナリデータをテキストデータに変換する方法をGitに教えればいいのです。ただし問題があります。バイナリデータをどうやってテキストに変換するか?ということです。この場合、一番いい方法はバイナリファイル形式ごとに専用の変換ツールを使うことです。とはいえ、判読可能なテキストに変換可能なバイナリファイル形式はそう多くありません(音声データをテキスト形式に変換?うまくいかなさそうです...)。ただ、仮にそういった事例に出くわしデータをテキスト形式にできなかったとしても、ファイルの内容についての説明、もしくはメタデータを取得することはそれほど難しくないでしょう。もちろん、そのファイルについての全てがメタデータから読み取れるわけではありませんが、何もないよりはよっぽどよいはずです。

これから、上述の2手法を用い、よく使われてるバイナリファイル形式から有用な差分を取得する方法を説明します。

補足: バイナリファイル形式で、かつデータがテキストで記述されているけれど、テキスト形式に変換するためのツールがないケースがよくあります。そういった場合、strings プログラムを使ってそのファイルからテキストを抽出できるかどうか、試してみるとよいでしょう。UTF-16 などのエンコーディングで記述されている場合だと、strings プログラムではうまくいかないかもしれません。どこまで変換できるかはケースバイケースでしょう。とはいえ、strings プログラムは 大半の Mac と Linux で使えるので、バイナリファイル形式を取り扱う最初の一手としては十分でしょう。

MS Word ファイル

あなたはまずこれらのテクニックを使って、人類にとって最も厄介な問題のひとつ、Wordで作成した文書のバージョン管理を解決したいと思うでしょう。奇妙なことに、Wordは最悪のエディタだと全ての人が知っているにも係わらず、皆がWordを使っています。Word文書をバージョン管理したいと思ったなら、Gitのリポジトリにそれらを追加して、まとめてコミットすればいいのです。しかし、それでいいのでしょうか? あなたが git diff をいつも通りに実行すると、次のように表示されるだけです。

$ git diff
diff --git a/chapter1.doc b/chapter1.doc
index 88839c4..4afcb7c 100644
Binary files a/chapter1.doc and b/chapter1.doc differ

これでは2つのバージョンをcheckoutしてそれらを自分で見比べてみない限り、比較することは出来ませんよね? Gitの属性を使えば、うまく解決できます。.gitattributesに次の行を追加して下さい。

*.doc diff=word

これは、指定したパターン(.doc)にマッチした全てのファイルに対して、差分を表示する時には"word"というフィルタを使うべきであるとGitに教えているのです。"word"フィルタとは何でしょうか? それはあなたが用意しなければなりません。Word文書をテキストファイルに変換するプログラムとして catdoc を使うように次のようにGitを設定してみましょう。なお、catdoc とは、差分を正しく表示するために、Word文書からテキストを取り出す専用のツール( http://www.wagner.pp.ru/~vitus/software/catdoc/ からダウンロードできます。)です。

$ git config diff.word.textconv catdoc

このコマンドは、.git/config に次のようなセクションを追加します。

[diff "word"]
    textconv = catdoc

これで、.doc という拡張子をもったファイルはそれぞれのファイルに catdoc というプログラムとして定義された"word"フィルタを通してからdiffを取るべきだということをGitは知っていることになります。こうすることで、Wordファイルに対して直接差分を取るのではなく、より効果的なテキストベースでの差分を取ることができるようになります。

例を示しましょう。この本の第1章をGitリポジトリに登録した後、ある段落にいくつかの文章を追加して保存し、それから、変更箇所を確認するためにgit diffを実行しました。

$ git diff
diff --git a/chapter1.doc b/chapter1.doc
index c1c8a0a..b93c9e4 100644
--- a/chapter1.doc
+++ b/chapter1.doc
@@ -128,7 +128,7 @@ and data size)
 Since its birth in 2005, Git has evolved and matured to be easy to use
 and yet retain these initial qualities. It’s incredibly fast, it’s
 very efficient with large projects, and it has an incredible branching
-system for non-linear development.
+system for non-linear development (See Chapter 3).

Gitは、追加した"(See Chapter 3)"という文字列を首尾よく、かつ、簡潔に知らせてくれました。正確で、申し分のない動作です!

OpenDocument Text ファイル

MS Word ファイル (*.doc) と同じ考えかたで、OpenOffice.org の OpenDocument Text ファイル (*.odt) も扱えます。

次の行を .gitattributes ファイルに追加しましょう。

*.odt diff=odt

そして、odt diff フィルタを .git/config に追加します。

[diff "odt"]
    binary = true
    textconv = /usr/local/bin/odt-to-txt

OpenDocument ファイルの正体は zip で、複数のファイル (XML 形式のコンテンツやスタイルシート、画像など) を含むディレクトリをまとめたものです。このコンテンツを展開し、プレーンテキストとして返すスクリプトが必要です。/usr/local/bin/odt-to-txt というファイルを作って (ディレクトリはどこでもかまいません)、次のような内容を書きましょう。

#! /usr/bin/env perl
# Simplistic OpenDocument Text (.odt) to plain text converter.
# Author: Philipp Kempgen

if (! defined($ARGV[0])) {
    print STDERR "No filename given!\n";
    print STDERR "Usage: $0 filename\n";
    exit 1;
}

my $content = '';
open my $fh, '-|', 'unzip', '-qq', '-p', $ARGV[0], 'content.xml' or die $!;
{
    local $/ = undef;  # slurp mode
    $content = <$fh>;
}
close $fh;
$_ = $content;
s/<text:span\b[^>]*>//g;           # remove spans
s/<text:h\b[^>]*>/\n\n*****  /g;   # headers
s/<text:list-item\b[^>]*>\s*<text:p\b[^>]*>/\n    --  /g;  # list items
s/<text:list\b[^>]*>/\n\n/g;       # lists
s/<text:p\b[^>]*>/\n  /g;          # paragraphs
s/<[^>]+>//g;                      # remove all XML tags
s/\n{2,}/\n\n/g;                   # remove multiple blank lines
s/\A\n+//;                         # remove leading blank lines
print "\n", $_, "\n\n";

そして実行権限をつけます。

chmod +x /usr/local/bin/odt-to-txt

これで、git diff.odt ファイルの変更点を確認できるようになりました。

画像ファイル

その他の興味深い問題としては画像ファイルの差分があります。PNGファイルに対するひとつの方法としては、EXIF情報(多くのファイルでメタデータとして使われています)を抽出するフィルタを使う方法です。exiftoolをダウンロードしインストールすれば、画像データをメタデータの形でテキストデータとして扱うことができます。従って、次のように設定すれば、画像データの差分をメタデータの差分という形で表示することができます。

$ echo '*.png diff=exif' >> .gitattributes
$ git config diff.exif.textconv exiftool

上記の設定をしてからプロジェクトで画像データを置き換えてgit diffと実行すれば、次のように表示されることになるでしょう。

diff --git a/image.png b/image.png
index 88839c4..4afcb7c 100644
--- a/image.png
+++ b/image.png
@@ -1,12 +1,12 @@
 ExifTool Version Number         : 7.74
-File Size                       : 70 kB
-File Modification Date/Time     : 2009:04:17 10:12:35-07:00
+File Size                       : 94 kB
+File Modification Date/Time     : 2009:04:21 07:02:43-07:00
 File Type                       : PNG
 MIME Type                       : image/png
-Image Width                     : 1058
-Image Height                    : 889
+Image Width                     : 1056
+Image Height                    : 827
 Bit Depth                       : 8
 Color Type                      : RGB with Alpha

ファイルのサイズと画像のサイズが変更されたことが簡単に見て取れるでしょう。

キーワード展開

SubversionやCVSを使っていた開発者から、キーワード展開機能をリクエストされることがよくあります。これについてGitにおける主な問題は、Gitはまずファイルのチェックサムを生成するためにcommitした後にファイルに関する情報を変更できないという点です。しかし、commitするためにaddする前にファイルをcheckoutしremoveするという手順を踏めば、その時にファイルにテキストを追加することが可能です。Gitの属性はそうするための方法を2つ提供します。

ひとつめの方法として、ファイルの$Id$フィールドを自動的にblobのSHA-1 checksumを挿入するようにできます。あるファイル、もしくはいくつかのファイルに対してこの属性を設定すれば、次にcheckoutする時、Gitはこの置き換えを行うようになるでしょう。ただし、挿入されるチェックサムはcommitに対するものではなく、対象となるblobものであるという点に注意して下さい。

$ echo '*.txt ident' >> .gitattributes
$ echo '$Id$' > test.txt

次にtest.txtをcheckoutする時、GitはSHA-1チェックサムを挿入します。

$ rm test.txt
$ git checkout -- test.txt
$ cat test.txt
$Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

しかし、このやりかたには制限があります。CVSやSubversionのキーワード展開ではタイムスタンプを含めることができます。対して、SHA-1チェックサムは完全にランダムな値ですから、2つの値の新旧を知るための助けにはなりません。

これには、commit/checkout時にキーワード展開を行うためのフィルタを書いてやることで対応できます。このために"clean"と"smudge"フィルタがあります。特定のファイルに対して使用するフィルタを設定し、checkoutされる前("smudge" 図7-2参照)もしくはcommitされる前("clean" 図7-3参照)に指定したスクリプトが実行させるよう、.gitattributesファイルで設定できます。これらのフィルタはあらゆる種類の面白い内容を実行するように設定できます。


図7-2. checkoutする時に"smudge"フィルタを実行する


図7-3. ステージする時に"clean"フィルタを実行する。

この機能に対してオリジナルのcommitメッセージは簡単な例を与えてくれています。それはcommit前にあなたのCのソースコードをindentプログラムに通すというものです。*.cファイルに対して"indent"フィルタを実行するように、.gitattributesファイルにfilter属性を設定することができます。

*.c     filter=indent

それから、smudgeとcleanで"indent"フィルタが何を行えばいいのかをGitに教えます。

$ git config --global filter.indent.clean indent
$ git config --global filter.indent.smudge cat

このケースでは、*.cにマッチするファイルをcommitした時、Gitはcommit前にindentプログラムにファイルを通し、checkoutする前にはcatを通すようにします。catは基本的に何もしません。入力されたデータと同じデータを吐き出すだけです。この組み合わせでCのソースコードに対してcommit前にindentを通すことが効果的に行えます。

RCSスタイルの$Date$キーワード展開もまた別の興味深い例です。満足のいく形でこれを行うには、ファイル名を受け取って、プロジェクトの最新のcommitの日付を見付けだし、その日付をファイルに挿入するちょっとしたスクリプトが必要になります。そのようなRubyスクリプトが以下です。

#! /usr/bin/env ruby
data = STDIN.read
last_date = `git log --pretty=format:"%ad" -1`
puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')

このスクリプトは、git logコマンドの出力から最新のcommitの日付を取得し、標準入力からの入力中のすべての$Date$文字列にその日付を追加し、結果を表示します。あなたのお気に入りのどのような言語でスクリプトを書くにしても、簡潔にすべきです。このスクリプトファイルにexpand_dateと名前をつけ、実行パスのどこかに置きます。次に、Gitが使うフィルタ(daterと呼びましょうか)を設定し、checkout時にexpand_dateが実行されるようにGitに教えてあげましょう。

$ git config filter.dater.smudge expand_date
$ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'

このPerlのスクリプトは、開始点に戻るために$Date$文字列内の他の文字列を削除します。さあ、フィルタの準備ができました。ファイルに$Date$キーワードを追加して新しいフィルタに仕事をさせるためにGitの属性を設定して、テストしてみましょう。

$ echo '# $Date$' > date_test.txt
$ echo 'date*.txt filter=dater' >> .gitattributes

これらの変更をcommitして再度ファイルをcheckoutすれば、キーワード展開が正しく行われているのがわかります。

$ git add date_test.txt .gitattributes
$ git commit -m "Testing date expansion in Git"
$ rm date_test.txt
$ git checkout date_test.txt
$ cat date_test.txt
# $Date: Tue Apr 21 07:26:52 2009 -0700$

アプリケーションをカスタマイズするためのこのテクニックがどれほど強力か、おわかりいただけたと思います。しかし、注意が必要です。.gitattributesファイルはcommitされ、プロジェクト内で共有されますが、ドライバ(このケースで言えば、dater)はそうはいきません。そう、すべての環境で動くとは限らないのです。あなたがこうしたフィルタをデザインする時、たとえフィルタが正常に動作しなかったとしても、プロジェクトは適切に動き続けられるようにすべきです。

リポジトリをエクスポートする

あなたのプロジェクトのアーカイブをエクスポートする時には、Gitの属性データを使って興味深いことを行うことができます。

export-ignore

アーカイヴを生成するとき、あるファイルやディレクトリをエクスポートしないように設定することができます。プロジェクトにはcheckinしたいがアーカイブファイルには含めたくないディレクトリやファイルがあるなら、それらにexport-ignoreを設定してやることができます。

例えば、test/ディレクトリ以下にいくつかのテストファイルがあって、それらをプロジェクトのtarballには含めたくないとしましょう。その場合、次の1行をGitの属性ファイルに追加します。

test/ export-ignore

これで、プロジェクトのtarballを作成するためにgit archiveを実行した時、アーカイブにはtest/ディレクトリが含まれないようになります。

export-subst

アーカイブ作成時にできる別のこととして、いくつかの簡単なキーワード展開があります。第2章で紹介した--pretty=formatで指定できるフォーマット指定子とともに$Format:$文字列をファイルに追加することができます。例えば、LAST_COMMITという名前のファイルをプロジェクトに追加し、git archiveを実行した時にそれを最新のcommitの日付に変換したい場合、次のように設定します。

$ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT
$ echo "LAST_COMMIT export-subst" >> .gitattributes
$ git add LAST_COMMIT .gitattributes
$ git commit -am 'adding LAST_COMMIT file for archives'

git archiveを実行したあと、アーカイブを展開すると、LAST_COMMITは以下のような内容になっているでしょう。

$ cat LAST_COMMIT
Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$

マージの戦略

Git属性を使えば、プロジェクトにある指定したファイルに対して異なるマージ戦略を使うようにすることができます。とても有効なオプションのひとつは、指定したファイルで競合が発生した場合に、マージを行わずにあなたの変更内容で他の誰かの変更を上書きするように設定するというものです。

これはブランチを分岐させ特別な作業をしている時、そのブランチでの変更をマージさせたいが、いくつかのファイルの変更はなかったことにしたいというような時に助けになります。例えば、database.xmlというデータベースの設定ファイルがあり、ふたつのブランチでその内容が異なっているとしましょう。そして、そのデータベースファイルを台無しすることなしに、一方のブランチへとマージしたいとします。これは、次のように属性を設定すれば実現できます。

database.xml merge=ours

マージを実行すると、database.xmlに関する競合は発生せず、次のような結果になります。

$ git merge topic
Auto-merging database.xml
Merge made by recursive.

この場合、database.xmlは元々のバージョンのまま、書き変わりません。