チャプター ▾ 第2版

8.2 Gitのカスタマイズ - Git Attributes

Git Attributes

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

属性を使用すると、プロジェクト内の個々のファイルまたはディレクトリに対して異なるマージ戦略を指定したり、非テキストファイルの差分をGitに指示したり、Gitにファイルをチェックインまたはチェックアウトする前にコンテンツをフィルターさせたりすることができます。このセクションでは、Gitプロジェクトのパスに設定できる属性の一部を学び、この機能の実践的な使用例をいくつか見ていきます。

バイナリファイル

Git属性を使用できるクールなトリックの1つは、どのファイルがバイナリであるか (Gitがそれ以外では判別できない場合) をGitに伝え、これらのファイルの処理方法についてGitに特別な指示を与えることです。たとえば、一部のテキストファイルは機械生成されたもので、差分を取ることができませんが、一部のバイナリファイルは差分を取ることができます。Gitにどちらがどちらであるかを伝える方法を説明します。

バイナリファイルの識別

一部のファイルはテキストファイルのように見えますが、あらゆる目的でバイナリデータとして扱われます。たとえば、macOSのXcodeプロジェクトには、.pbxprojで終わるファイルが含まれています。これは基本的に、IDEによってディスクに書き出されるJSON (プレーンテキストのJavaScriptデータ形式) データセットであり、ビルド設定などを記録します。技術的にはテキストファイル (すべてUTF-8であるため) ですが、軽量のデータベースであるため、そのように扱うべきではありません。2人が変更した場合、内容をマージすることはできませんし、差分は一般的に役に立ちません。このファイルは機械によって消費されることを意図しています。本質的には、バイナリファイルのように扱いたいのです。

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

*.pbxproj binary

これで、GitはCRLFの問題を変換したり修正しようとしません。また、プロジェクトでgit showまたはgit diffを実行しても、このファイルの変更の差分を計算または出力しようとしません。

バイナリファイルの差分

Git属性機能を使用して、バイナリファイルの差分を効果的に取得することもできます。これを行うには、バイナリデータを通常の差分で比較できるテキスト形式に変換する方法をGitに伝えます。

まず、このテクニックを使って、人類に知られている最も煩わしい問題の1つ、つまりMicrosoft 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

これはGitに、このパターン(.docx)に一致するファイルは、変更を含む差分を表示しようとすると「word」フィルターを使用するように指示します。「word」フィルターとは何でしょうか?設定する必要があります。ここでは、Word文書を読み取り可能なテキストファイルに変換するためにdocx2txtプログラムを使用するようにGitを構成し、その後、Gitは適切に差分を取ります。

まず、docx2txtをインストールする必要があります。これはhttps://sourceforge.net/projects/docx2txtからダウンロードできます。INSTALLファイルの指示に従って、シェルが見つけられる場所に配置してください。次に、出力をGitが期待する形式に変換するためのラッパースクリプトを作成します。パス上のどこかにdocx2txtというファイルを作成し、次の内容を追加します。

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

そのファイルをchmod a+xするのを忘れないでください。最後に、Gitがこのスクリプトを使用するように設定できます。

$ git config diff.word.textconv docx2txt

これでGitは、2つのスナップショット間の差分を実行しようとし、いずれかのファイルが.docxで終わる場合、それらのファイルを「word」フィルター(docx2txtプログラムとして定義されている)に通す必要があることを認識します。これにより、差分を取る前に、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プログラムをダウンロードしてインストールすると、それを使用して画像をメタデータに関するテキストに変換できるため、少なくとも差分には発生した変更のテキスト表現が表示されます。以下の行を.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

ファイルサイズと画像寸法が両方とも変更されたことが簡単にわかります。

キーワードの展開

SVNやCVSのようなキーワード展開は、それらのシステムに慣れている開発者からよく要求されます。Gitにおけるこれの主な問題は、Gitが最初にファイルのチェックサムを計算するため、コミット後にコミットに関する情報でファイルを変更できないことです。ただし、チェックアウト時にファイルにテキストを挿入し、コミットに追加する前に再度削除することができます。Git属性はこれを行う2つの方法を提供します。

まず、BLOBのSHA-1チェックサムをファイル内の$Id$フィールドに自動的に挿入できます。この属性を1つまたは複数のファイルに設定すると、次にそのブランチをチェックアウトするときに、GitはそのフィールドをBLOBのSHA-1に置き換えます。これがコミットのSHA-1ではなく、BLOB自体のSHA-1であることに注意することが重要です。以下の行を.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はあまり役に立ちません。なぜなら、それはかなりランダムであり、SHA-1を見ただけではどれが古いか新しいか判断できないからです。

コミット/チェックアウト時にファイルの置換を行う独自のフィルターを記述できることがわかります。これらは「クリーン」フィルターと「スマッジ」フィルターと呼ばれます。.gitattributesファイルでは、特定のパスにフィルターを設定し、チェックアウト直前(「スマッジ」、「スマッジ」フィルターはチェックアウト時に実行されますを参照)とステージング直前(「クリーン」、「クリーン」フィルターはファイルがステージングされたときに実行されますを参照)にファイルを処理するスクリプトを設定できます。これらのフィルターは、あらゆる種類の楽しいことを行うように設定できます。

The “smudge” filter is run on checkout
図169. 「スマッジ」フィルターはチェックアウト時に実行されます
The “clean” filter is run when files are staged
図170. 「クリーン」フィルターはファイルがステージングされたときに実行されます

この機能の最初のコミットメッセージでは、コミットする前にすべてのCソースコードをindentプログラムで実行する簡単な例を挙げています。.gitattributesファイルで\*.cファイルを「indent」フィルターでフィルターするように、フィルター属性を設定することでこれを設定できます。

*.c filter=indent

次に、Gitに「indent」フィルターがスマッジとクリーンで何をするかを伝えます。

$ 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と呼びます)、ファイルをチェックアウト時に「スマッジ」するために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 "Test 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)はそうではないため、どこでも機能するわけではないことに注意する必要があります。これらのフィルターを設計する際には、 gracefullyに失敗し、プロジェクトが引き続き適切に機能するようにする必要があります。

リポジトリのエクスポート

Git属性データを使用すると、プロジェクトのアーカイブをエクスポートする際にも興味深いことができます。

export-ignore

アーカイブを生成する際に、特定のファイルやディレクトリをエクスポートしないようにGitに指示できます。アーカイブファイルに含めたくないが、プロジェクトにチェックインしたいサブディレクトリやファイルがある場合、export-ignore属性を使用してそれらのファイルを指定できます。

たとえば、test/サブディレクトリにいくつかのテストファイルがあり、プロジェクトのtarballエクスポートにそれらを含める意味がないとします。次の行をGit属性ファイルに追加できます。

test/ export-ignore

これで、プロジェクトのtarballを作成するためにgit archiveを実行しても、そのディレクトリはアーカイブに含まれません。

export-subst

デプロイメント用にファイルをエクスポートする際、export-subst属性でマークされたファイルの選択された部分にgit logの書式設定とキーワード展開処理を適用できます。

例えば、プロジェクトにLAST_COMMITというファイルを含めたい場合、git archiveが実行されたときに最後のコミットに関するメタデータが自動的に挿入されるようにするには、.gitattributesLAST_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を実行すると、アーカイブされたファイルの内容は次のようになります。

$ 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 notesを含めることができ、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属性を使用して、プロジェクト内の特定のファイルに対して異なるマージ戦略を使用するようにGitに指示することもできます。非常に便利なオプションの1つは、競合が発生した場合に特定のファイルをマージしようとしないようにGitに指示し、代わりに他のユーザーの側ではなく自分のマージ側を使用するようにすることです。

これは、プロジェクトのブランチが分岐しているか、特殊化されているが、そこから変更をマージし直したい場合や、特定のファイルを無視したい場合に役立ちます。たとえば、2つのブランチで異なる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は元々持っていたバージョンにとどまります。

scroll-to-top