次の方法で共有



August 2017

Volume 32 Number 8

DevOps - Git の内部構造: アーキテクチャとインデックス ファイル

Jonathan Waldman | August 2017

前回 (msdn.com/magazine/mt809117) は、Git が有向非巡回グラフ (DAG) を使用してリポジトリのコミット オブジェクトを整理する方法を説明しました。また、コミット オブジェクトが参照できる BLOB、ツリー、タグというオブジェクトについても説明しました。コラムの最後では、分岐を紹介し、HEAD と head の違いを説明しました。今回のコラムの前に前回のコラムをお読みください。今回は、Git の "3 つのツリー" のアーキテクチャと、そのインデックス ファイルの重要性について説明します。これらの追加の Git 内部構造を理解することで、基礎知識が増え、Git ユーザーとしてさらに実力が付き、また、Visual Studio IDE のグラフィカルな Git ツールを使ってさまざまな Git 操作を実行するときに、新たな洞察が得られます。

Visual Studio は Git API を使用して Git と通信すること、また、Visual Studio IDE Git ツールは、基盤の Git エンジンの複雑さと機能を抽象化することを前回説明しました。それは、Git コマンド ライン インターフェイス (CLI) を利用しないバージョン管理ワークフローの実装を望む開発者にとって朗報です。そうでなければこの IDE の便利な Git 抽象化は、混乱を招きかねません。たとえば、プロジェクトを Git ソース管理に追加する基本ワークフローを考えてみましょう。プロジェクト ファイルを変更したら、それらのファイルをステージングし、ステージングしたファイルをコミットします。そのためには、チーム エクスプローラーの [変更] ウィンドウで、変更されたファイルの一覧を参照し、ステージングするファイルを選択します。一番左側の画像 (図 1 参照) をみると、作業ディレクトリの 2 つのファイルが変更されています (マーカー 1)。

チーム エクスプローラーの [変更] ウィンドウでは、[変更] セクションと [ステージング済みの変更] セクションに同じファイルを表示可能

チーム エクスプローラーの [変更] ウィンドウでは、[変更] セクションと [ステージング済みの変更] セクションに同じファイルを表示可能

右方向の次の画像では、変更したファイルの 1 つ、Program.cs をステージングしました (マーカー 2)。ステージングすると、Program.cs は [変更] の一覧から [ステージング済みの変更] に "移動した" ように見えます。作業ディレクトリにある Program.cs をさらに変更して保存すると、Program.cs は [ステージング済みの変更] セクションに表示されたままですが (マーカー 3)、[変更] セクションにも表示されます (マーカー 4)。 舞台裏での Git の動作を理解していないと、Program.cs には作業フォルダー内と Git 内部のオブジェクト データベース内の 2 つの "コピー" があることがわかるまで当惑するかもしれません。そのことに気が付いたとしても、ステージング済みのコピーをステージング解除した場合や、2 つ目の変更した Program.cs のコピーをステージングした場合、作業中のコピーの変更を元に戻した場合、分岐を切り替えた場合に、どうなるかは見当がつかないかもしれません。

ファイルをステージング、ステージング解除、元に戻す、コミット、チェック アウトしたときの Git の動作を完全に理解するには、まず、Git のアーキテクチャを理解する必要があります。

Git の 3 つのツリーのアーキテクチャ

Git は 3 つのツリーのアーキテクチャを実装します (この場合の "ツリー" はディレクトリ構造とファイルのことです)。図 2「Git の 3 つのツリーのアーキテクチャは重要なインデックス ファイルをすべて利用して、スマートで効率的なパフォーマンスを実現」の左のツリーから順に説明します。1 つ目のツリーは、作業ディレクトリ (.git 隠しフォルダーがある OS ディレクトリ) にあるファイルとフォルダーのコレクションです。2 つ目のツリーは、通常、.git フォルダーのルートにあるインデックスと呼ばれる単一のバイナリ ファイルに格納されます。3 つ目のツリーは、DAG を表す Git オブジェクトで構成されます (SHA-1 値を基に名前が付けられた Git オブジェクトは、.git\objects 以下にある 2 文字の 16 進数値で名前が付けられたフォルダーと、.git\objects\pack にある "パック" ファイル、および .git\objects\info\alternates ファイルで定義したファイル パスにも格納できました。覚えていますか)。Git リポジトリは、.git フォルダーにあるすべてのファイルによって定義されることを忘れないでください。DAG のことを Git リポジトリと表現することがよくありますが、これは正確とはいえません。Git リポジトリには、インデックスと DAG の両方が含まれます。

Git の 3 つのツリーのアーキテクチャは重要なインデックス ファイルをすべて利用して、スマートで効率的なパフォーマンスを実現

図 2 Git の 3 つのツリーのアーキテクチャは重要なインデックス ファイルをすべて利用して、スマートで効率的なパフォーマンスを実現

どのツリーにもディレクトリ構造とファイルが格納されていますが、ツリー固有のメタデータを保持し、保存と取得を最適化するために、それぞれ別のデータ構造を利用していることに注意してください。1 つ目のツリー (作業ディレクトリ ツリー、"作業ツリー" とも呼ばれます) は、ただの OS ファイルとフォルダーで (OS レベルであるという以外に、特別なデータ構造はありません)、ソフトウェア開発者と Visual Studio のニーズを満たします。2 つめのツリー (Git インデックス) は、作業ディレクトリと DAG を構成するコミット オブジェクトの両方に関与します。それにより、Git において、作業ディレクトリのファイルの内容をすばやく比較し、コミットをすぐに実行できるようにしています。3 つ目のツリー (DAG) は、Git により過去の堅牢なバージョン管理システムを追跡可能にします。Git はインデックスとコミット オブジェクトに格納されている項目に、便利なメタデータを追加します。たとえば、インデックスに格納されているメタデータは、作業ディレクトリ内のファイルに対する変更を検出する場合に便利で、コミット オブジェクトに格納されているメタデータは、コミットを実行したユーザーと理由を追跡するときに便利です。

3 つのツリーのアーキテクチャの 3 つのツリーを確認し、また、今回この後どのようなことを解説するのか見通しが付くように、ここで少しまとめてみます。作業ディレクトリのツリーは、既に使い方をよくご存じの OS ファイル システムが実体なので、このツリーの動作については既に知識をお持ちです。また、前回のコラムを読み終えている場合は、DAG についての実践的な知識もお持ちだと思います。したがって、この時点で不足しているのは、作業ディレクトリと DAG の両方に関与するインデックス ツリー (これ以降は「インデックス」) です。実際、インデックスは、非常に重要な役割を果たすため、ここからはインデックスのみを取り上げます。

インデックスのしくみ

インデックスは "ステージング領域" と同じ意味だという親切なアドバイスを聞かれたことがあるかもしれません。 それは正しい面もありますが、そのように表現すると、本来の役割が曲げて伝えられることになります。インデックスの役割は、ステージング領域をサポートするだけでなく、Git が作業ディレクトリ内のファイルに対する変更を検出しやすくすること、分岐およびマージプロセスを調整し、ファイルごとに競合を解決したり、安全にいつでもマージを中止できるようにすること、次のコミット オブジェクトに参照が書き込まれている、ステージングされたファイルとフォルダーをツリー オブジェクトに変換できるようにすることです。Git は、作業ツリー内のファイルについての情報と DAG から取得したオブジェクトについての情報の保持にもインデックスを利用します。つまり、一種のキャッシュとしてもインデックスを活用します。では、インデックスについてさらに詳しく見ていきましょう。

インデックスは、独自の自己完結型のファイル システムを実装し、フォルダーやファイルへの参照とそれらのメタデータを併せて格納できるようにしています。Git がこのインデックスを更新する方法とタイミングは、発行された Git コマンドの種類と、指定されたコマンド オプションによって決まります (必要であれば、Git update-index 配管コマンドを使って各自でインデックスを管理することもできます)。これはあまりに広範なトピックになるため、ここでは取り上げません。ただし、Visual Studio の Git ツールを操作するときは、Git がインデックスを更新する主な方法と、Git がインデックスに格納されている情報を使用する主な方法を知っておくと役に立ちます。図 3 は、ファイルをステージングしたときに Git が作業ディレクトリのデータでインデックスを更新すること、および、マージ (マージの競合があった場合)、複製やプル、または分岐の切り替えを開始したときに、DAG のデータでインデックスを更新することを表しています。一方、Git は、commit の発行後に DAG を更新するときと、分岐の複製やプル、または切り替え後に作業ディレクトリを更新するときに、インデックスに格納されている情報を利用します。Git がインデックスを利用し、インデックスが実にさまざまな Git 操作に関わっていることがわかると、インデックスを変更する高度な Git コマンドの価値がわかるようになり、効果的に Git の動作を操れるようになります。

主な Git アクション

図 3 インデックスを更新する主な Git アクション (緑) とインデックスに格納されている情報を利用する Git アクション (赤)

作業ディレクトリに新しいファイルを作成して、インデックスに書き込まれるときの処理を確認してみましょう。そのファイルをステージングすると、直ちに Git は次の文字列連結式を使用してヘッダーを作成します。

blob{space}{file-length in bytes}{null-termination character}

次に Git は、ヘッダーをファイル コンテンツの先頭に連結します。したがって、"Hello" という文字列を含むテキスト ファイルの場合、ヘッダーとファイル コンテンツを連結すると次のような文字列が生成されます ("H" の文字の前に null 文字があることに注意してください)。

blob 5Hello

よりわかりやすくするために、以下にこの文字列の 16 進数バージョンを示します。

62 6C 6F 62 20 35 00 48 65 6C 6C 6F

Git は次に、この文字列の SHA-1 を計算します。

5ab2f8a4323abafb10abb68657d9d39f1a775057

Git は次に、既存のインデックスを調べて、同じ SHA-1 でそのフォルダー\ファイル名のエントリが存在するかどうかを判断します。存在する場合は、.git\objects フォルダーから該当する BLOB オブジェクトを見つけて、その変更時刻を更新します (Git は、リポジトリに既に存在するオブジェクトを上書きすることはありません。最終更新日を更新して、この新しく追加されたオブジェクトがガベージ コレクションの対象になるタイミングを遅らせます)。存在しない場合は、SHA-1 文字列の最初の 2 文字を .git\objects 以下のディレクトリ名に使用し、残りの 38 文字を BLOB ファイルの名前に使用してから、Zlib 圧縮を行い、コンテンツを書き込みます。この例では、Git によって、5a という名前のフォルダーが .git\objects に作成された後、そのフォルダーに b2f8a4323abafb10abb68657d9d39f1a775057 という名前のファイルとして BLOB オブジェクトが書き込まれます。

この方法で BLOB が作成されると、当然あるはずのファイル プロパティの 1 つであるファイル名が、この BLOB オブジェクトには存在しないことに驚かれるかもしれません。 しかし、それは仕様です。Git はコンテンツによりアドレス可能なファイル システムでした。そのため、ファイルではなく、SHA-1 を基に名前が付けられた BLOB オブジェクトを管理します。各 BLOB オブジェクトは、通常、少なくとも 1 つのツリーから参照されます。さらにツリー オブジェクトは、通常、コミット オブジェクトから参照されます。最終的に、Git ツリー オブジェクトは、ステージングするファイルのフォルダー構造を表すことになります。しかし、Git は、commit を発行するまでこれらのツリー オブジェクトを作成しません。したがって、Git がインデックスのみを使用してコミット オブジェクトを準備するならば、インデックスには各 BLOB のファイル パス参照もキャプチャする必要があると結論付けることができ、まさにそれを Git は行っています。実際、2 つの BLOB の SHA-1 値が同じでも、それぞれが別のファイル名またはパス/ファイル値にマップされている限り、インデックス内では別のエントリになります。

Git は、インデックスに書き込む各 BLOB オブジェクトと併せて、ファイルの作成日や更新日など、ファイルのメタデータも保存します。Git は、作業ディレクトリ内の各ファイルの SHA-1 値を総当たりで再計算するのではなく、この情報を基にファイル日付の比較とヒューリスティックを利用することで、作業ディレクトリ内のファイルに対する変更を効率的に検出します。このような戦略のおかげで、チーム エクスプローラーの [変更] ウィンドウでの情報表示 (または、Git の状態を扱う磁器 (porcelain) コマンドを発行するとき) にかかる時間が短縮されます。

作業ディレクトリのファイルのインデックス エントリとそのファイルに関連するメタデータが保存されると、このファイルのコピーと作業ディレクトリ内に残されたコピーとを比較できるようになるため、そのファイルを "追跡" するように Git に指示が出されます。技術的には、追跡されるファイル、は作業ディレクトリ内にも存在し、次のコミットに含まれるファイルです。それが、追跡されないファイルとの違いです。追跡されないファイルには、作業ディレクトリ内にあってもインデックスに含まれないファイルと、明示的に追跡しないように指定されているファイルの 2 種類があります (「インデックス拡張」を参照してください)。つまり、インデックスによって Git は、追跡するファイル、追跡しないファイル、追跡してはならないファイルがどれかを判断できます。

インデックスの特定のコンテンツについて理解を深めるため、具体的な例を使いましょう。まずは、新しい Visual Studio プロジェクトを作成します。このプロジェクトが複雑かどうかは、あまり重要ではありません。この後の説明には、2、3 のファイルがあれば十分です。MSDNConsoleApp という名前の新しいコンソール アプリケーションを作成します。[ソリューションのディレクトリを作成] チェック ボックスと、[新しい Git リポジトリの作成] チェック ボックスをオンにします。[OK] をクリックしてソリューションを作成します。

この後すぐに Git コマンドを発行します。ご自身のシステムで実行する場合は、作業ディレクトリでコマンド プロンプト ウィンドウを開き、説明を読みながらそのウィンドウをすぐに使えるようにしてください。特定の Git リポジトリに対して Git コマンド ウィンドウをすばやく開くには、Visual Studio の [チーム] メニューの [接続の管理] を選択します。ローカルの Git リポジトリとそのリポジトリの作業ディレクトリへのパスの一覧が表示されます。リポジトリ名を右クリックして、[コマンド プロンプトを開く] を選択すると、ウィンドウが起動されます。ここに、Git CLI コマンドを入力します。

ソリューションを作成したら、チーム エクスプローラーの [分岐] ウィンドウ (図 4、マーカー 1) を開き、Git によって master という名前の既定の分岐 (マーカー 2) が作成されていることを確認します。master 分岐 (マーカー 2) を右クリックし、[履歴の表示] (マーカー 3) を選択します。Visual Studio によって自動的に 2 つのコミットが作成されていることを確認します (マーカー 4)。1 つ目には「Add .gitignore and .gitattributes」というコミット メッセージがあり、2 つ目には「Add project files」というコミット メッセージがあります。

履歴の表示

図 4 履歴を表示し、新規プロジェクト作成時の Visual Studio の処理を確認

チーム エクスプローラーの [変更] ウィンドウを開きます。Visual Studio は Git API を利用して、このウィンドウに項目を設定します。これは、Visual Studio 版の Git status コマンドです。現在、このウィンドウは、ステージングされていない変更が作業ディレクトリにないことを示しています。Git は、各作業ディレクトリ ファイルの各インデックス エントリを比較することで、これを判断します。インデックスのファイル エントリと関連するファイルのメタデータがあれば、作業ディレクトリ内のファイルに変更、追加、削除、または名前の変更が行われているかどうかを Git は判断できます (ただし、.gitignore ファイルで指定されているファイルは除きます)。

このように、Git が作業ディレクトリ ツリーと、HEAD によって参照されているコミット オブジェクト間の違いを検出するうえで、インデックスは重要な役割を果たします。インデックスから Git エンジンにどのような情報が提供されるかについてもう少し詳しく知るために、前に開いておいたコマンド ライン ウィンドウから、次の配管コマンドを実行します。

git ls-files --stage

インデックスに現在含まれているファイルの完全な一覧を生成する場合は、このコマンドをいつでも実行できます。今回のシステムでは、次の出力が生成されます。

100644 1ff0c423042b46cb1d617b81efb715defbe8054d 0       .gitattributes
100644 3c4efe206bd0e7230ad0ae8396a3c883c8207906 0       .gitignore
100644 f18cc2fac0bc0e4aa9c5e8655ed63fa33563ab1d 0       MSDNConsoleApp.sln
100644 88fa4027bda397de6bf19f0940e5dd6026c877f9 0       MSDNConsoleApp/App.config
100644 d837dc8996b727d6f6d2c4e788dc9857b840148a 0       MSDNConsoleApp/MSDNConsoleApp.csproj
100644 27e0d58c613432852eab6b9e693d67e5c6d7aba7 0       MSDNConsoleApp/Program.cs
100644 785cfad3244d5e16842f4cf8313c8a75e64adc38 0       MSDNConsoleApp/Properties/AssemblyInfo.cs

出力の最初の列は、8 進数で表された Unix OS ファイル モードです。ただし、Git はファイル モード値を完全にはサポートしていません。表示されるのは 100644 (EXE 以外のファイル) と 100755 (Unix ベースの EXE ファイル -- Git for Windows も実行可能ファイル対して 100644 を使用します) のみになると思われます。2 列目はファイルの SHA-1 値です。3 列目はファイルのマージ段階の値を表します。競合がない場合は 0、競合がある場合は 1、2、3 のいずれかになります。最後に、インデックスに格納されている 7 個の BLOB オブジェクトのそれぞれのパスとファイル名があります。Git は次回コミットの前にツリー オブジェクトを構築するとき、このパス値を使用します (詳しくは後ほど説明します)。

では、インデックス ファイル自体を詳しく見ていきましょう。これはバイナリ ファイルなので、HexEdit 4 (hexedit.com からダウンロードできる無料の 16 進エディター) を使用して、コンテンツを表示します (図 5 参照、画面の一部のみ抜粋)。

プロジェクトの Git インデックス ファイルの 16 進ダンプ

図 5 プロジェクトの Git インデックス ファイルの 16 進ダンプ

図 6 Git インデックスのヘッダーのデータ形式

インデックス ファイル - ヘッダー エントリ
00 ~ 03
(4 バイト)
DIRC ディレクトリ キャッシュ エントリの固定ヘッダー。
インデックス ファイルの先頭に必ずこのエントリが付きます。
04 ~ 07
(4 バイト)
バージョン インデックスのバージョン番号 (Git for Windows は
現在バージョン 2 を使用)。
08 ~ 11
(4 バイト)
エントリの数 4 バイトの値。インデックスは最大で
4,294,967,296 エントリをサポート。

インデックスの最初の 12 バイトにはヘッダーが含まれます (図 6 参照)。最初の 4 バイトには、必ず DIRC ("directory cache" (ディレクトリ キャッシュ) の略) という文字が含まれます。Git インデックスがキャッシュと呼ばれることが多いのはこのためです。次の 4 バイトにはインデックスのバージョン番号が保持されます。これは、Git の特定の機能を使用しているのでなければ、既定では 2 になります (スパース チェックアウトなど、特定の機能を使っているときは、バージョンは 3 または 4 になります)。最後の 4 バイトには、インデックスに含まれているファイル エントリの総数が保持されます。

12 バイトのヘッダーの後には、n 個のインデックス エントリのリストが続きます。この "n" は、インデックス ヘッダーが示すエントリ数と同じになります。各インデックス エントリの形式を図 7 に示します。Git はパス/ファイル名フィールドを基に、インデックス エントリを昇順で表示します。

図 7 Git インデックス ファイルのインデックス エントリのデータ形式

インデックス ファイル - インデックス エントリ
4 バイト 32 ビットの作成時刻 (秒単位) 1970 年 1 月 1 日、午前 00 時00 分 00 からの経過秒数です。
4 バイト 32 ビットの作成時刻 - ナノ秒コンポーネント 作成時刻のナノ秒コンポーネント (秒単位) です。
4 バイト 32 ビットの変更時刻 (秒単位) 1970 年 1 月 1 日、午前 00 時00 分 00 からの経過秒数です。
4 バイト 32 ビットの変更時刻 - ナノ秒コンポーネント 作成時刻のナノ秒コンポーネント (秒単位) です。
4 バイト デバイス ファイルに関連付けられているメタデータ。Unix OS で使用されているファイル属性に由来します。
4 バイト inode
4 バイト モード
4 バイト ユーザー ID
4 バイト グループ ID
4 バイト ファイル コンテンツの長さ ファイルのコンテンツのバイト数です。
20 バイト SHA-1 BLOB オブジェクトの SHA-1 値に対応します。
2 バイト フラグ (上位ビットから下位ビットへ) 1 ビット: assume-valid/assume-unchanged フラグ、1 ビット: 拡張フラグ (バージョン 3 以下は必ず 0。1 の場合は、パス\ファイル名の前にさらに 2 バイト追加)、2 ビット: マージ段階、12 ビット: パス\ファイル名の長さ (0xFFF 未満の場合)
2 バイト
(バージョン 3
以上)
フラグ (上位ビットから下位ビットへ)
1 ビット: 将来使用
1 ビット: skip-worktree フラグ (スパース チェックアウト)
1 ビット: intent-to-add フラグ (git add -N)
13 ビット: 未使用、必ず 0
可変長 パス/ファイル名 NU 終端

最初の 8 バイトは、1970 年 1 月 1 日午前 0 時からのオフセット値として表されたファイルの作成時刻です。2 番目の 8 バイトは、1970 年 1 月 1 日午前 0 時からのオフセット値として表されたファイルの変更時刻です。次に、ホスト OS 関連のファイル属性のメタデータである 4 バイトの値が 5 つ (デバイス、inode、モード、ユーザー ID、グループ ID) 続きます。Windows で使用される値はモードのみで、これは、ls-files コマンドの出力を説明したときに既に述べたとおり、通常、8 進数の 100644 になります (これは 4 バイトの 814AH 値に変換されます。図 5 の 26H 参照)。

メタデータの後には、ファイルのコンテンツの長さを表す 4 バイトの値が続きます。図 5 では、この値は 030 から始まり、00 00 0A 15 (10 進では 2,581) と表示されています。これは、次に示すように今回のシステムの .gitattributes ファイルの長さです。

05/08/2017  09:24 PM    <DIR>          .
05/08/2017  09:24 PM    <DIR>          ..
05/08/2017  09:24 PM             2,581 .gitattributes
05/08/2017  09:24 PM             4,565 .gitignore
05/08/2017  09:24 PM    <DIR>          MSDNConsoleApp
05/08/2017  09:24 PM             1,009 MSDNConsoleApp.sln
               3 File(s)          8,155 bytes

               3 Dir(s)  92,069,982,208 bytes free

オフセットの 034H は、BLOB オブジェクトの 20 バイトの SHA-1 値です。

1ff0c423042b46cb1d617b81efb715defbe8054d.

この SHA-1 は、問題のファイル (.gitattributes) のファイル コンテンツを含む BLOB オブジェクトを指しています。

048H は、2 つの 1 ビットのフラグと、2 ビットのマージ段階値、12 ビットの現在インデックス エントリのパス/ファイル名で構成される 2 バイトの値です。2 つの 1 ビットのフラグのうち 1 つは、上位ビットでインデックス エントリに as-sume-unchanged フラグが設定されるかどうかを指定します (これには、通常、Git update-index 配管コマンドが使用されます)。下位ビットは、2 バイトの追加のデータがパス\ファイル名のエントリの前に含まれるかどうかを示します。このビットはバージョン 3 以上の場合は必ず 1 にします)。次の 2 ビットには、マージ段階の値が保持されます。これは前述のとおり 0 ~ 3 になります。12 ビット値には、パス\ファイル名の文字列の長さが保持されます。

拡張フラグが設定されている場合、2 バイトの値に skip-worktree フラグと intent-to-add bit フラグが、フィラーによるプレースホルダーと併せて保持されます。

最後に、可変長のバイト シーケンスに、パス\ファイル名が保持されます。この値は 1 つ以上の NULL 文字で終端します。この終端文字の後には、インデックス内の次の BLOB オブジェクトか、1 つ以上のインデックス拡張エントリが続きます (これについては後ほど説明します)。

前に、Git はステージングされているデータがコミットされるまで、ツリー オブジェクトを構築しないと説明しました。つまり、インデックスは、最初はパス/ファイル名と BLOB オブジェクトへの参照のみになります。ただし、commit が発行されると、Git は直ちにインデックスを更新して、最後のコミット中に作成したツリー オブジェクトへの参照を含めます。次のコミット中にそれらのディレクトリ参照がまだ作業ディレクトリに存在している場合は、キャッシュ済みツリー オブジェクトへの参照を使用して、Git が次のコミット中に実行しなければならない処理を軽減できます。このようにインデックスにはさまざまな役割があり、そのため、インデックスとも、ステージング領域とも、キャッシュとも呼ばれます。

図 7 のインデックス エントリは、BLOB オブジェクト参照しかサポートしていません。ツリー オブジェクトを格納する場合、Git は拡張を使用することになります。

インデックス拡張

インデックスには、特別なデータ ストリームを格納する拡張エントリを含めることができます。それらの拡張エントリでは、Git エンジンが作業ディレクトリ内のファイルを監視し、次のコミットを準備するときの判断材料となる追加情報を提供します。最後のコミット中に作成されたツリー オブジェクトをキャッシュするため、Git は、作業ディレクトリのルートと、各サブディレクトリのインデックスに、ツリー拡張オブジェクトを追加します。

図 5マーカー 2 は、インデックスの最終的なバイト数を示し、インデックスに格納されているツリー オブジェクトをキャプチャしています。図 8 にツリー拡張のデータ形式を示します。

図 8 Git インデックス ファイルのツリー拡張オブジェクトのデータ形式

インデックス ファイル - キャッシュ済みツリー拡張のヘッダー
4 バイト TREE キャッシュ済みツリー拡張のヘッダーの決められた署名です。
4 バイト TREE 拡張データの長さを表す 32 ビットの数値  

 

キャッシュ済みツリー拡張エントリ
可変 パス NULL 終端のパス文字列 (ルート ツリーの場合は null 文字のみ)。
ASCII 数値 エントリの数 このツリー エントリによってカバーされるインデックス内のエントリ数を表す ASCII 数値です。
1 バイト 20H (空白文字)  
ASCII 数値 サブツリーの数 このツリーに含まれるサブツリーの数を表す ASCII 数値です。
1 バイト 0AH (ライン フィード文字)  
20 バイト ツリー オブジェクトの SHA-1 このエントリによって生成されるツリー オブジェクトの
SHA-1 値です。

オフセット 284H に設定されるツリー拡張データ ヘッダーは、"TREE" という文字列 (キャッシュ済みツリー拡張データの先頭の文字になる) で始まり、その後に、後続の拡張データの長さを示す 32 ビット値を連結して構成されます。次は、ツリー エントリごとのエントリです。最初のエントリは、ツリー パスを表す可変長の NULL 終端文字列値です (ルート ツリーの場合は NULL のみ)。その次の値は ASCII 値です。したがって、16 進数エディターでは "7" になります。これは、現在のツリーがカバーする BLOB エントリの数です (これはルート ツリーなので、前に Git ls-files ステージング コマンドを発行したときに表示されたエントリー数と同じになります)。次の文字は空白で、そのあとにまた ASCII 値が続きます。これは、現在のツリーに含まれるサブツリーの数を表します。

今回のプロジェクトのルート ツリーには、MSDNConsoleApp というサブツリーが 1 つだけあります。この値の後に、ラインフィード文字が続き、さらにツリーの SHA-1 が続きます。SHA-1 はオフセット 291 から始まっています。先頭文字は 0d21e2 です。

0d21e2 が実際にルート ツリーの SHA-1 であることを確かめてみましょう。それには、コマンド ウィンドウに切り替えて、以下を入力します。

git log

このコマンドにより、最近のコミットの詳細が表示されます。

commit 5192391e9f907eeb47aa38d1c6a3a4ea78e33564
Author: Jonathan Waldman <jonathan.waldman@live.com>
Date:   Mon May 8 21:24:15 2017 -0500

  Add project files.

commit dc0d3343fa24e912f08bc18aaa6f664a4a020079
Author: Jonathan Waldman <jonathan.waldman@live.com>
Date:   Mon May 8 21:24:07 2017 -0500

  Add .gitignore and .gitattributes.

最も新しいコミットは、タイムスタンプが 21:24:15 のコミットです。したがって、これが、インデックスを最後に更新したコミットになります。そのコミットの SHA-1 を使用して、ルート ツリーの SHA-1 値を特定できます。

git cat-file -p 51923

次の出力が生成されます。

tree 0d21e2f7f760f77ead2cb85cc128efb13f56401d
parent dc0d3343fa24e912f08bc18aaa6f664a4a020079
author Jonathan Waldman <jonathan.waldman@live.com> 1494296655 -0500
committer Jonathan Waldman <jonathan.waldman@live.com> 1494296655 -0500

先行するツリー エントリが、ルート ツリー オブジェクトです。そこから、インデックス ダンプのオフセット 291H の位置にある 0d21e2 が、実際にルート ツリー オブジェクトの SHA-1 であることが確かめられます。

その他のツリー エントリは、SHA-1 値の直後に、オフセットの 2A5H の位置から始まります。ルート ツリー以下のキャッシュ済みツリー オブジェクトの SHA-1 値を確認するには、次のコマンドを実行します。

git ls-tree -r -d master

このコマンドでは、現在の分岐を再帰処理し、ツリー オブジェクトのみが表示されます。

040000 tree c7c367f2d5688dddc25e59525cc6b8efd0df914d    MSDNConsoleApp
040000 tree 2723ceb04eda3051abf913782fadeebc97e0123c    MSDNConsoleApp/Properties

最初の列のモード値 040000 は、オブジェクトがファイルではなく、ディレクトリであることを示しています。

インデックス最後の 20 バイトには、インデックス自体を表す SHA-1 ハッシュが保持されています。予想に反せず、Git はこの SHA-1 値を使用して、インデックスのデータの整合性を検証します。

今回の例のインデックス ファイルのエントリについてはすべて説明しましたが、通常は、インデックス ファイルはこれよりも大きく、複雑になります。インデックス ファイル形式は、次のような追加の拡張データ ストリームをサポートします。

  • 操作をマージし、マージの競合の解決をサポートするストリーム。これには、"REUC" (resolve undo conflict の略) というシグネチャが付きます。
  • 追跡されないファイル (.gitignore および .git\info\exclude ファイルで指定するか、core.excludesfile によって参照することで、追跡から除外されるファイル) のキャッシュを維持するためのストリーム。"UNTR" というシグネチャが付きます。
  • インデックス分割モードをサポートして、非常に大きなインデックス ファイルでのインデックス更新を高速化するためのストリーム。"link" というシグネチャが付きます。

インデックスの拡張機能によって、インデックスの機能を拡張できます。

まとめ

今回は、Git の 3 つのツリーのアーキテクチャを確認し、インデックス ファイルの基盤の処理について詳しく説明しました。Git では、特定の操作に応じてインデックスが更新されること、また、その他の操作を実行するために、インデックス内の情報を利用することを説明しました。

インデックスについて深く考えなくても Git を使用することはできます。しかし、インデックスの知識があると、Git は作業ディレクトリ内のファイルの変更をどのように検出するか、ステージング領域とは何で、それが便利なのはなぜか、特定の機能が Git ではあれほど高速に処理されるのはなぜかということがわかるので、Git のコア機能について貴重な洞察が得られます。また、チェックアウト コマンドやリベース コマンドのコマンドライン バリアントや、ソフト リセット、混合リセット、ハード リセットの違いも理解しやすくなります。これらの機能を使うと、特定のコマンドを発行するときに、インデックス、作業ディレクトリ、またはインデックスと作業ディレクトリの両方を更新するかどうかを指定できます。これらのオプションは、Git ワークフロー、戦略、高度な操作についてのドキュメントを読むときに目にすることになります。今回は、インデックスがどのように使われるかをより深く理解できるように、インデックスが果たす重要な役割を紹介しました。


Jonathan Waldman は、マイクロソフト テクノロジにその誕生以来から携わっており、ソフトウェア人間工学を専門とする、マイクロソフト認定プロフェッショナルです。彼は Pluralsight 技術チームのメンバーで、現在は団体や民間企業のソフトウェア開発プロジェクトをリードしています。連絡先は、jonathan.waldman@live.com (英語のみ) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Kraig Brockschmidt、Saeed Noursalehi、Ralph Squillace、および Edward Thomson に心より感謝いたします。


この記事について MSDN マガジン フォーラムで議論する