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 show
や git 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を実行した時、アーカイブには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は元々のバージョンのまま、書き変わりません。