Git --distributed-even-if-your-workflow-isnt
Chapters ▾ 2nd Edition

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

Git の属性

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

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

バイナリファイル

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

バイナリファイルの特定

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

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

*.pbxproj binary

これで、Git が CRLF 問題の対応をすることもなくなりますし、git showgit diff を実行したときにもこのファイルの diff を調べることはなくなります。

バイナリファイルの差分

バイナリファイルに対して意味のある差分を取る際にも、Git の属性を使うことができます。 普通の diff でも比較できるよう、バイナリデータをテキストデータに変換する方法をGitに教えればいいのです。

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

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

これでは、2つのバージョンをチェックアウトして、目視で見比べなくては、比較はできませんよね? Gitの属性を使えば、これをうまく解決できます。 `.gitattributes`に次の行を追加して下さい。

*.docx diff=word

これは、指定したパターン (.docx) にマッチした全てのファイルに対して、差分を表示する時には word'' というフィルタを使うよう Git に指示しているのです。 では、 word'' フィルタとは何でしょうか? これは自分で用意しなければなりません。 ここでは、 docx2txt を使ってWord文書をテキストファイルに変換した上で、正しく diff が取れるように設定してみましょう。

まず、 docx2txt をインストールする必要があります。 http://docx2txt.sourceforge.net からダウンロードしたら、 INSTALL ファイルの指示に従って、シェルから見える場所にファイルを置いてください。 次に、出力を Git に合わせて変換するラッパースクリプトを作成します。 パスの通った場所に、 `docx2txt`という名前のファイルを次の内容で作成してください。

#!/bin/bash
docx2txt.pl $1 -

作ったファイルに chmod a+x するのを忘れないでください。 最後に、Git がこのファイルを使うように設定します。

$ git config diff.word.textconv docx2txt

これで、二つのスナップショットの diff を取る際に、ファイル名の末尾が .docx だったら、 word'' フィルタを通す(この word'' フィルタは docx2txt というプログラムとして定義されている)ということが Git に伝わりました。 こうすることで、Wordファイルの差分を取る際に、より効果的なテキストベースでの差分を取ることができるようになります。

例を示しましょう。この本の第1章をWord形式に変換し、Gitリポジトリに登録しました。 さらに、新しい段落を追加しました。 git diff の出力は次のようになります。

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 0b013ca..ba25db5 100644
--- a/chapter1.docx
+++ b/chapter1.docx
@@ -2,6 +2,7 @@
 This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so.
 1.1. About Version Control
 What is "version control", and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. For the examples in this book you will use software source code as the files being version controlled, though in reality you can do this with nearly any type of file on a computer.
+Testing: 1, 2, 3.
 If you are a graphic or web designer and want to keep every version of an image or layout (which you would most certainly want to), a Version Control System (VCS) is a very wise thing to use. It allows you to revert files back to a previous state, revert the entire project back to a previous state, compare changes over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also generally means that if you screw things up or lose files, you can easily recover. In addition, you get all this for very little overhead.
 1.1.1. Local Version Control Systems
 Many people's version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they're clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you're in and accidentally write to the wrong file or copy over files you don't mean to.

Gitは、追加した ``Testing: 1, 2, 3.'' という正しい文字列を首尾よく、かつ、簡潔に知らせてくれました。 これだけでは完璧ではありません(書式の変更はここでは表示されていません)が、確実に動作しています。

その他の興味深い問題としては、画像ファイルの差分があります。 ひとつの方法として、EXIF情報(多くのファイル形式で使用されているメタデータ)を抽出するフィルタを使う方法があります。 exiftool`をダウンロードしてインストールすれば、画像データを、メタデータを表すテキストデータへ変換できます。これによって、 diff では少なくとも、変更内容をテキスト形式で表示できるようになります。 ではここで、以下の行を.gitattributes`に追加してみましょう。

*.png diff=exif

続いて、さきほどインストールしたツールを使うようGitの設定を変更します。

$ 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:21 07:02:45-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 がコミットの最初にファイルのチェックサムを生成するためです。 しかし、ファイルをチェックアウトする際にテキストを挿入し、コミットへ追加する際にそれを削除することは可能です。 Gitの属性はこれを行うための方法を2つ提供します。

ひとつめの方法として、ファイルの $Id$ フィールドへ、 blob の SHA-1 チェックサムを自動的に挿入できます。 あるファイル、もしくはいくつかのファイルに対してこの属性を設定すれば、次にそのブランチをチェックアウトする時、Gitはこの置き換えを行うようになります。 ただし、挿入されるチェックサムはコミットに対するものではなく、対象となるblobのものであるという点に注意して下さい。 ではここで、以下の行を`.gitattributes`に追加してみましょう。

*.txt ident

続いて、`$Id$`への参照をテスト用ファイルに追加します。

$ echo '$Id$' > test.txt

そうすると、次にこのファイルをチェックアウトする時、GitはblobのSHA-1チェックサムを挿入します。

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

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

これには、コミットおよびチェックアウトの時にキーワード展開を行うフィルタを書いてやれば対応できます。 このフィルタは clean'' および smudge'' フィルタと呼ばれます。 .gitattributes ファイルで、特定のパスにフィルタを設定し、チェックアウトの直前( smudge'' 、 チェックアウトする時に ``smudge'' フィルタを実行する を参照)およびステージングの直前( clean'' 、 ステージングする時に ``clean'' フィルタを実行する を参照)に処理を行うスクリプトを設定できます。 これらのフィルタは、色々と面白いことに使えます。

チェックアウトする時に ``smudge'' フィルタを実行する
Figure 143. チェックアウトする時に ``smudge'' フィルタを実行する
ステージングする時に ``clean'' フィルタを実行する
Figure 144. ステージングする時に ``clean'' フィルタを実行する

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

*.c filter=indent

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

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

このケースでは、 *.c にマッチするファイルをコミットした時、Gitはステージング前にindentプログラムにファイルを通し、チェックアウトする前には cat を通すようにします。 cat`は基本的に何もしません。入力されたデータと同じデータを吐き出すだけです。 この組み合わせを使えば、Cのソースコードのコミット前に、効果的に `indent を通せます。

もうひとつの興味深い例として、RCSスタイルの $Date$ キーワード展開があります。 これを正しく行うには、ファイル名を受け取り、プロジェクトの最新のコミットの日付を見て、その日付をファイルに挿入するちょっとしたスクリプトが必要になります。 これを行う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 コマンドの出力から最新のコミットの日付を取得し、標準入力中のすべての $Date$ 文字列にその日付を追加し、結果を出力します。お気に入りのどんな言語で書くにしても、簡単なスクリプトになるでしょう。 このスクリプトファイルに`expand_date`と名前をつけ、実行パスのどこかに置きます。 次に、Git にフィルタ(ここでは dater`とします)を設定し、チェックアウト時に smudge で `expand_date フィルタを使うように指定します。 コミット時に日付を削除するのには、 Perl の正規表現が使えます。

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

このPerlのスニペットは、 $Date$ 文字列の内側にある内容を削除し、日付を挿入する前の状態に戻します。 さて、フィルタの準備ができました。このファイルが新しいフィルタに引っかかるように Git の属性を設定し、ファイルに $Date$ キーワードを追加した上で、テストしてみましょう。

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

これらの変更をコミットして、再度ファイルをチェックアウトすれば、キーワードが正しく置き換えられているのがわかります。

$ 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 ファイルはコミットされてプロジェクト内で共有されますが、ドライバ(このケースで言えば、dater)そうはそうはいきません。そのため、この機能はどこででも働くわけではありません。 フィルタを設計する時には、たとえフィルタが正常に動作しなかったとしても、プロジェクトは適切に動き続けられるようにすべきです。

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

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

export-ignore

アーカイブを生成するとき、特定のファイルやディレクトリをエクスポートしないように設定できます。 プロジェクトにはチェックインしたいが、アーカイブファイルには含めたくないディレクトリやファイルがあるなら、それらに export-ignore 属性を設定することで、分別が行えます。

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

test/ export-ignore

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

export-subst

デプロイ用にファイルをエクスポートする際に、export-subst 属性のついたファイルを指定して git log のログ書式指定機能とキーワード展開機能で生成した内容をファイルに付与できます。 例えば、LAST_COMMIT`という名前のファイルをプロジェクトに追加し、`git archive`を実行した時にそのファイルのメタデータを最新コミットと同じ内容に変換したい場合、.gitattributes`ファイルと`LAST_COMMIT`ファイルを 次のように設定します。

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

git archive を実行すると、 LAST_COMMIT は以下のような内容になっているはずです。

$ git archive HEAD | tar xCf ../deployment-testing -
$ cat ../deployment-testing/LAST_COMMIT
Last commit date: Tue Apr 21 08:38:48 2009 -0700 by Scott Chacon

このような置換に、コミットメッセージや git note を用いることもできます。その際、git log コマンドのワードラップ処理が適用されます。

$ echo '$Format:Last commit: %h by %aN at %cd%n%+w(76,6,9)%B$' > LAST_COMMIT
$ git commit -am 'export-subst uses git log's custom formatter

git archive uses git log's `pretty=format:` processor
directly, and strips the surrounding `$Format:` and `$`
markup from the output.
'
$ git archive @ | tar xfO - LAST_COMMIT
Last commit: 312ccc8 by Jim Hill at Fri May 8 09:14:04 2015 -0700
       export-subst uses git log's custom formatter

         git archive uses git log's `pretty=format:` processor directly, and
         strips the surrounding `$Format:` and `$` markup from the output.

この結果作成されたアーカイブはデプロイするのにぴったりです。一方、いったんエクスポートされてしまったアーカイブで開発を続けるのはおすすめできません。

マージの戦略

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

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

database.xml merge=ours

その上で、ダミーのマージ戦略 ours を次のように定義します。

$ git config --global merge.ours.driver true

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

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

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