June 2017

Volume 32 Number 6

DevOps - Visual Studio 開発者向けの Git 内部情報

Jonathan Waldman | June 2017

前回 Git に取り組んだ DevOps コラム (msdn.com/magazine/mt767697) では、Git のバージョン管理システム (VCS) と、これまで使い慣れた一元管理型の VCS との違いを取り上げました。その後、Visual Studio 付属の Git ツールを使って、ある Git タスクを実現する方法の例を紹介しました。今回は、新しくリリースされた Visual Studio 2017 IDE 内部で Git が機能するしくみに関連する変更点をまとめます。次に、Git リポジトリをファイル システム内に実装する方法を取り上げ、データ ストアのトポロジと、さまざまなストレージ オブジェクトの構造とコンテンツを調べます。最後にまとめとして Git ブランチについて簡単に説明し、次回以降のコラムで紹介予定の Git の高度な操作に備えて今後の展望を示します。

メモ: 今回はサーバーやリモート処理は使用せず、純粋にローカルのシナリオを使用します。つまり、Visual Studio 2017 と Git for Windows (G4W) をインストールした Windows 搭載 PC を使用します (インターネットやネットワーク接続の有無は問いません)。本稿では Git の内部情報を紹介することになるため、Visual Studio 付属の Git ツールと、Git の基本操作と基本概念を理解していることが前提です。

Visual Studio、Git、開発者

Git は、バージョン管理データ ストアを保持するリポジトリを表すだけでなく、このリポジトリを管理するコマンドを処理するエンジンも表します。 低レベルの操作を実行するのが配管 (Plumbing) コマンドです。この配管コマンドをマクロのような形式でつなげるのが磁器 (porcelain) コマンドです。これにより、配管コマンドの細かい呼び出しが少なくなり、使いやすくなります。Git を習得するにつれ、タスクによってはこのようなコマンド (一部は本稿で取り上げます) を使用する必要があり、コマンドの呼び出しにコマンド ライン インターフェイス (CLI) が必要になることがわかります。残念なことに、Visual Studio 2017 が使用する新しい Git エンジン (MinGit) は Git CLI を提供しないため、Git CLI がインストールされなくなります。G4W 2.10 で導入される MinGit (「最小」 Git) は、Git リポジトリの操作を必要とする Windows アプリケーション向けに設計された、機能セットを減らした移植可能な API です。G4W と、その延長線上にある MinGit は、Git オープン ソースの公式プロジェクトから枝分かれしたものです。つまり、どちらも Git の修正プログラムや更新プログラムが正式に利用可能になると直ちにそれを継承します。また、Visual Studio も最新状態が確保されます。

Git CLI にアクセス (さらに、今回の説明を理解) するには、完全版の G4W パッケージをインストールすることをお勧めします。Git CLI/GUI ツールの他のオプションも利用できますが、G4W (MinGit の公式の親) を選択するのが適切です。具体的には、構成ファイルが MinGit と共有されるためです。G4W の最新セットアップを入手するには、サイトの公式ソース (git-scm.com、英語) のダウンロード セクションにアクセスします。セットアップ プログラムを実行し、[Git Bash Here] (Git コマンド プロンプト ウィンドウを作成) チェック ボックスと [Git GUI Here] (Git GUI ウィンドウを作成) チェック ボックスをオンにします。その結果、エクスプローラーでフォルダーを右クリックすると、現在のフォルダーに対して 2 つのオプションのどちらかを簡単に選べるようになります (Git Bash の「Bash」は「Bourne Again Shell」を指し、Unix シェルでの G4W の Git CLI を表します)。次に、[Use Git from the Windows Command Prompt] (Windows コマンド プロンプトから Git を使用する) を選択します。これにより、Visual Studio のパッケージ マネージャー コンソール (Windows PowerShell) かコマンド プロンプト ウィドウのいずれかから簡単に Git コマンドを実行できるよう、環境が構成されます。

今回の推奨オプション (図 1 参照) を使用して G4W をインストールすると、Git リポジトリとの通信時に、2 つの通信経路が有効になります。 1 つは、Visual Studio 2017 から MinGit API を使用する経路です。もう 1 つは、PowerShell や コマンド プロンプトのセッションから G4W を使用する経路です。2 つは、Git リポジトリまでの通信経路が大きく異なります。MinGit と G4W は異なる通信エンドポイントとして機能しますが、どちらも Git の公式ソース コードから継承されるため、構成ファイルを共有します。磁器コマンドを発行すると、CLI によって処理される前に、磁器コマンドから配管コマンドに翻訳されているのがわかります。ここでのポイントは、CLI に対するベアメタルの Git 配管コマンドの発行を多くの GIT エキスパートが利用し、一部のエキスパートが利用を目指しているのを理解することです。というのも、Git リポジトリの管理、クエリ、更新を行うための最も明快で簡単な方法が GIT 配管コマンドの発行になるためです。低レベルな配管コマンドではなく、Visual Studio IDE が公開する高度な磁器コマンドと Git 操作を使って、Git リポジトリを更新することもできます。ただし、磁器コマンドは、動作内容や呼び出しタイミングを変更するオプションを受け入れることが多いため、常に操作方法が明確になるわけではありません。Git の能力を活かすには、Git の配管コマンドに慣れることが不可欠なので、Visual Studio 2017 と共に G4W をインストールすることを強くお勧めします。Git の配管コマンドと磁器コマンドの詳細については、git-scm.com/docs (英語) を参照してください。

MinGit API との通信経路と Git for Windows のコマンドライン インターフェイス
図 1 MinGit API との通信経路と Git for Windows のコマンドライン インターフェイス

低レベルの Git

Visual Studio 開発者が Git に切り替える際、Team Foundation Server (TFS) など、VCS の既存の知識を利用するのは自然の流れです。実際、コードのチェックアウト/チェックイン、マージ、分岐など、両方のシステムでの操作の説明に使用されている用語や考え方には重複する部分があります。ただし、用語は似ていても、基本的な操作が似ていると考えるのはまったくの誤りで、危険です。というのも、分散型の Git VCS は、ファイルの保存や追跡の方法、馴染みのあるバージョン管理機能の実装方法が根本的に異なるためです。つまり、Git に切り替える際は、一元管理型の VCS をすべて忘れ、心機一転して始めることをお勧めします。

Git でソース管理が行われている Visual Studio プロジェクトで作業する場合の代表的な編集、ステージング、コミットのワークフローは次のようになります。 必要に応じて、プロジェクトのファイルを追加、編集、削除 (これ以降は、まとめて「変更」と呼びます) します。準備ができたら、変更の一部またはすべてをステージングした後、それらをリポジトリにコミットします。コミットすると、それらの変更がリポジトリの完全かつ透過的な履歴の一部になります。では、Git がすべてを内部で管理するしくみを見てみましょう。

有向非巡回グラフ: コミットが行われるたびに、Git が管理する有向非巡回グラフ (グラフ理論用語では「DAG」) の頂点 (ノード) になります。DAG が Git リポジトリを表し、各頂点がデータ要素 (コミット オブジェクト) を表します (図 2 参照)。DAG の各頂点は接線で結ばれます。通例として、親子の関係を示すため、DAG の接線は矢印で表現されます (矢の先が指すのが親頂点、矢の根元が指すのが子頂点です)。原点となる頂点は、リポジトリの最初のコミットを示し、終端の頂点には子頂点がありません。DAG の接線は、頂点間の親子関係を正確に表します。Git のコミット オブジェクト (以下「コミット」) が頂点に相当するため、Git は DAG の構造を利用して、すべてのコミット間の親子関係をモデル化し、リポジトリ最初のコミット以降すべてのコミットから変更履歴を生成できます。さらに、線形グラフとは異なり、DAG は分岐 (複数の子をもつ親) だけでなく、マージ (複数の親をもつ子) もサポートします。Git ブランチ (分岐) は、コミット オブジェクトが新しい子を生成するたびに発生し、マージはコミット オブジェクトが結合して 1 つの子を形成すると発生します。

頂点、接線、矢印の先、矢印の根元、原典となる頂点、および終端の頂点を示す有向非巡回グラフ
図 2 頂点、接線、矢印の先、矢印の根元、原典となる頂点、および終端の頂点を示す有向非巡回グラフ、3 つの分岐 (A、B、C)、2 つの分岐イベント (A4)、1 つのマージ イベント (B3 と A5 を A6 にマージ)

ここで DAG と関連用語を細かく取り上げたのは、高度な Git 操作は Git DAG の頂点を操作することで機能することが多いためです。また、DAG は Git リポジトリを目に見える形で表現する場合にも役立ち、プレゼンテーション中や、Git GUI ツールによって、教材として幅広く利用されています。

Git オブジェクトの概要: ここまでに取り上げたオブジェクトは、Git のコミット オブジェクトだけです。ただし、実際には、コミット、ツリー、BLOB、タグという、4 つの異なるオブジェクト型が Git リポジトリに含まれています。これらのオブジェクトを調べるため、Visual Studio を起動し、[ファイル]、[新規作成]、[プロジェクト] の順に選択して、新しいコンソール アプリケーションを作成します (ここでは Visual Studio 2017 を使用していますが、Git サポートを含んでいれば以前のバージョンでもかまいません)。プロジェクトに名前を付け、[新しい Git リポジトリの作成] チェック ボックスをオンにして、[OK] をクリックします (Visual Studio で以前に Git を構成したことがなければ、Git ユーザー情報ダイアログ ボックスが表示されます。ダイアログ ボックスが表示されたら、コミットするたびに Git リポジトリに書き込むユーザー名とメール アドレスを指定します。コンピューター上のすべての Git リポジトリに同じ情報を使用する場合は、[グローバル .gitconfig に設定する] チェック ボックスをオンにします)。

これを終えると、ソリューション エクスプローラー ウィンドウが開きます (図 3、マーカー 1)。まだコミットを行っていなくても、チェックインするファイルの隣に、薄い青色の錠前アイコンが表示されます (これは、予想外のリポジトリに対して Visual Studio が操作を行う例の 1 つです)。 Visual Studio が行ったことを正確に調べるには、現在の分岐の変更履歴を確認します。

Git リポジトリの履歴レポートを含む新しい Visual Studio プロジェクト
図 3 Git リポジトリの履歴レポートを含む新しい Visual Studio プロジェクト

Git は、既定の分岐に「master」と名付け、これを現在の分岐にします。Visual Studio のステータス バーの右端に、現在の分岐の名前が表示されます (マーカー 2)。現在の分岐は、DAG 上のコミット オブジェクトのうち次のコミットの親になるオブジェクトを特定します (分岐については後ほど詳しく見ていきます)。現在の分岐のコミット履歴を表示するには、master ラベル (マーカー 2) をクリックして、メニューから [履歴の表示](マーカー 3) を選択します。

[履歴 - master] ウィンドウには、複数行の情報が示されます。左側 (マーカー 4) には、DAG の 2 つの頂点があります。それぞれの頂点は、Git DAG でのコミットを表します。ID、作成者、日付、メッセージの各列 (マーカー 5) が、コミットについての詳細情報を示します。master 分岐の HEAD は濃い赤のポインター (マーカー 6) で示されます。この意味については、後ほど詳しく説明します。この HEAD は、コミットによって DAG に新しい頂点が追加された後、次の接線が作る矢印が指す場所をマークします。

レポートには、それぞれ固有のコミット ID をもつ 2 つのコミットを Visual Studio が行ったことが示されています (マーカー 7)。先頭の (最も古い) のコミットは、ID a759f283 で、2 番目のコミットは bfeb0957 で一意に識別されます。これらは、全 40 文字の 16 進数セキュア ハッシュ アルゴリズム 1 (SHA-1) を切り捨てた値です。SHA-1 は暗号化ハッシュ関数で、コミット データなどのメッセージを受け取り、コミット ID などのメッセージ ダイジェスト (完全な SHA-1 ハッシュ値) を作成することで、データの破損を検出するように設計されています。つまり、SHA-1 ハッシュには約 1.46 x 1048 通りの一意になる組み合わせがあるため、チェックサムだけでなく GUID のようにも働きます。他の多くの Git ツールと同様、Visual Studio は完全な値の先頭 8 文字のみを使用します。これは、8 文字でも 43 億通りの一意値が得られ、通常の作業で競合を回避するには十分だと考えられるためです。完全な SHA-1 値を確認するには、履歴レポートの行にマウス ポインターを合わせます (マーカー 8)。

履歴の表示レポートのメッセージ列には、コミットの目的 (コミットの実行者が記入) がそれぞれ示されますが、これはコメントにすぎません。コミットによる実際の変更を調べるには、リストの行を右クリックして、[コミットの詳細の表示] を選択します (図 4 参照)。

リポジトリの最初の 2 つのコミットの詳細
図 4 リポジトリの最初の 2 つのコミットの詳細

最初のコミット (マーカー 1) には、.gitignore と .gitattributes への 2 つの変更が示されています (これらのファイルについては、前回説明しました)。  それぞれの隣にある [追加] は、リポジトリに追加されたファイルを示しています。2 番目のコミット (マーカー 2) には、追加された 5 つのファイルが示され、さらに、親のコミット オブジェクトの ID がクリック可能なリンクとして表示されます。SHA-1 値全体をクリップボードにコピーするには、[操作] メニューをクリックして、[コミット ID のコピー] を選択するだけです。

Git リポジトリのファイル システム実装: Git がリポジトリ内でファイルをどのように格納しているかを確かめるには、ソリューション エクスプローラーでソリューション (プロジェクトではありません) を右クリックして、[エクスプローラーでフォルダーを開く] を選択します。ソリューションのルートには、.git という名前の隠しフォルダーがあります (.git が表示されない場合、エクスプローラーの [表示] メニューの [隠しファイル] をオンにします)。この .git フォルダーが、このプロジェクトの Git リポジトリです。その objects フォルダーが DAG を定義します。 DAG の全頂点と各頂点の親子関係は、リポジトリ内で原点となる頂点から始まるすべてのコミットを表すファイルによってエンコードされます (前述 )。.git フォルダーの HEAD ファイルと refs フォルダーが分岐を定義します。それでは、.git の項目を細かく見てみましょう。

Git オブジェクトの調査

.git\objects フォルダーには、すべての Git オブジェクト型が保存されています。この Git オブジェクト型には、commit (コミット)、tree (フォルダー)、blob (バイナリ ファイル)、tag (commit オブジェクトの使いやすいエイリアス) があります。

commit オブジェクト: ここで、Git CLI を起動しましょう。好みのツール (Git Bash、PowerShell、コマンド ウィンドウ) を使えますが、ここでは PowerShell を使用します。まず、ソリューションのルート git\objects フォルダーに移動して、コンテンツを一覧します (図 5、マーカー 1)。2 文字の 16 進数値で名前が付けられた、多くのフォルダーが含まれているのがわかります。フォルダー内のファイル数が OS の許容値を超えないように、Git はフォルダー名の作成時に、40 バイトの SHA-1 値からそれぞれ先頭 2 文字を取り除いた残りの 38 文字をファイル名に使用して、オブジェクトを保存します。具体的に示すと、プロジェクト最初のコミット ID は「a759f283」であるため、このオブジェクトはフォルダー [a7] (ID の先頭 2 文字) に格納されます。ご想像通り、そのフォルダーを開くと、このオブジェクトが「59f283」という名前のファイルとして格納されています。このような 16 進数で名前が付けられたフォルダーに格納されているファイルは、すべて Git オブジェクトです。容量を節約するため、Git はオブジェクト ストア内のファイルを Zlib 圧縮します。この種の圧縮ではバイナリ ファイルが生成されるため、テキスト エディターでファイルを表示することはできません。代わりに、Git オブジェクト データを正しく抽出し、ダイジェスト可能な形式を使用して表示できる Git コマンドを呼び出す必要があります。

Git コマンドライン インターフェイスを使用した Git オブジェクトの調査
図 5 Git コマンドライン インターフェイスを使用した Git オブジェクトの調査

「59f283」はコミット ID なので、ファイル 59f283 に commit オブジェクトが含まれていることは明白です。ですが、objects フォルダーには、内容がわからないファイルもあります。Git には「cat-file」という配管コマンドがあり、オブジェクトの型とそのコンテンツをレポートします (マーカー 3)。型を取得するには、このコマンドを呼び出すときに、Git オブジェクトのファイル名を一意に表す文字列に加えて、「-t」 (型) オプションを指定します。

git cat-file -t a759f2

今回のシステムでは値「commit」が報告され、a759f2 で始まるファイルが commit オブジェクトを含んでいることを示しています。通常は SHA-1 ハッシュ値の先頭 5 文字を指定するだけで十分ですが、任意の数の文字を指定できます (フォルダー名となる先頭 2 文字を忘れずに追加します)。「-p」 (整形出力) オプションを指定して同じコマンドを実行すると、Git は commit オブジェクトから情報を抽出し、その情報を判読可能な形式で表示します (マーカー 4)。

commit オブジェクトは、親のコミット ID、ツリー ID、作成者名、作成者のメール アドレス、作成者のコミット タイムスタンプ、コミット実行者の名前、コミット実行者のメール アドレス、コミット実行者のコミット タイムスタンプ、およびコミット メッセージというプロパティで構成されます (リポジトリの先頭コミットには、親コミット ID は表示されません)。 各 commit オブジェクトの SHA-1 は、上記の commit オブジェクトのプロパティに含まれているすべての値から計算され、事実上、各 commit オブジェクトが一意コミット IDをもつことが保証されます。

tree オブジェクトと BLOB オブジェクト: commit オブジェクトはコミットに関する情報を含みますが、ファイルやフォルダーは含みません。代わりに、Git の tree オブジェクトを示す、ツリー ID (これも SHA-1 値) を含みます。tree オブジェクトは、他のすべての Git オブジェクトと共に、.git\objects フォルダーに保存されています。

図 6 は、各 commit オブジェクトに含まれるルート tree オブジェクトを示しています。さらに、ルート tree オブジェクトは、必要に応じて、BLOB オブジェクト (次に説明) や他の tree オブジェクトにマップされます。

コミットを表す Git オブジェクトの視覚化
図 6 コミットを表す Git オブジェクトの視覚化

今回のプロジェクトで 2 番目のコミット (コミット ID bfeb09) はファイルとフォルダー (上記の ** 4** 参照) を含むため、これを使用して、tree オブジェクトの機能を示します。図 7 のマーカー 1 は、「cat‑file ‑p bfeb09」の出力を示しています。今回は、最初の commit オブジェクトの SHA-1 値を正確に参照する親プロパティを含んでいるのがわかります (commit オブジェクトの親参照によって、Git がコミットの DAG を構築して管理できるようになります)。

tree オブジェクトの詳細を Git CLI を使用して調査
図 7 tree オブジェクトの詳細を Git CLI を使用して調査

ルート tree オブジェクトは、続いて、必要に応じて、BLOB オブジェクト (Zlib 圧縮ファイル) および他の tree オブジェクトにマップされます。

commit オブジェクト bfeb09 は、ID ca853d をもつ tree プロパティを含みます。図 7 のマーカー 2 は、「cat‑file ‑p ca853d」の出力を示しています。各 tree オブジェクトは、POSIX のアクセス許可マスクに対応するオブジェクトのアクセス許可プロパティ (040000 = ディレクトリ、100644 = 標準非実行可能ファイル、100664 = 標準非実行可能グループの書き込み可能ファイル、100755 = 標準実行可能ファイル、120000 = シンボリック リンク、160000 = Gitlink)、型 (tree または BLOB)、SHA-1 (tree または BLOB の)、および名前を保持します。名前は、フォルダー名 (tree オブジェクト) またはファイル名 (BLOB オブジェクト) です。この tree オブジェクトは、3 つの BLOB オブジェクトと別の tree オブジェクトが圧縮されたものです。この 3 つの BLOB は .gitattributes、.gitignore、DemoConsole.sln の各ファイルを表し、tree は DemoConsoleApp フォルダーを表しています (図 7、マーカー 3)。tree オブジェクト ca853d はプロジェクトの 2 番目のコミットに関連付けられていますが、最初の 2 つの BLOB は、最初のコミットで追加された .gitattributes ファイルと .gitignore ファイルを表しています (図 4、マーカー 1 参照)。 これらのファイルが 2 番目のコミットのツリーに表示される理由は、コミットはそれぞれ前の commit オブジェクトと、現在の commit オブジェクトによってキャプチャされた変更の両方を表すためです。ツリーを 1 レベルずつ調べていくと、図 7 のマーカー 3 の「cat-file -p a763da」の出力には、さらに 3 つの BLOB (App.config、DemoConsoleApp.csproj、Program.cs) と別の tree (Properties フォルダー) が示されています。

BLOB オブジェクトは、単なる Zlib 圧縮されたファイルです。圧縮されていないファイルがテキストを含む場合、BLOB ID を指定して同様の「cat-file」コマンドを使用すると、BLOB のコンテンツ全体を抽出できます (図 7、マーカー 5)。BLOB オブジェクトは複数のファイルを表すため、Git は SHA-1 の BLOB ID を使用して、ファイルが前のコミットから変化したかどうかを判断します。さらに、リポジトリ内で任意の 2 つのコミットの差分を調べるときは、SHA-1 値も使用します。

tag オブジェクト: SHA-1 値は、その性質上、暗号化を目的とした英数字の羅列なので、多少理解しづらいところがあります。tag オブジェクトによって、任意の commit オブジェクト、tree オブジェクト、または BLOB オブジェクトにわかりやすい名前を割り当てることができます。ただし、commit オブジェクトだけにタグを付けるのが最も一般的です。tag オブジェクトには、「簡易」と「注釈付き」の 2 種類があります。どちらの種類も、.git\refs\tags フォルダーでファイルとして表示されます (ファイル名が tag の名前になります)。簡易 tag ファイルのコンテンツは、既存の commit オブジェクト、tree オブジェクト、または BLOB オブジェクトの SHA-1 です。注釈付き tag ファイルのコンテンツは、tag オブジェクトに対する SHA-1 です。このファイルは、他すべての Git オブジェクトと共に .git\objects フォルダーに格納されます。tag オブジェクトのコンテンツを表示するには、同じ「cat-file -p」コマンドを使用します。これにより、タグが付けられたオブジェクトの SHA-1 値が、オブジェクト型、タグの作成者、作成日時、タグ メッセージと共に表示されます。Visual Studio には、commit オブジェクトにタグ付けする方法がたくさんあります。1 つは、コミットの詳細ウィンドウで、[タグの作成] リンクをクリックする方法です。タグの名前は、[コミットの詳細] ウィンドウ (マーカー 3) と履歴の表示レポート (前述のマーカー 9 を参照) に表示されます。

リポジトリ内のオブジェクトにストレージ最適化を適用すると、Git は .git\objects フォルダーに info フォルダーと pack フォルダーを作成します。これらのフォルダーと Git のファイル ストレージ最適化については、今後のコラムで取り上げる予定です。

Git の 4 つのオブジェクト型を理解すると、Git がコンテンツのアドレス指定が可能なファイル システムと呼ばれる理由がわかります。それは、多くのファイルやフォルダーに含まれる任意の種類のコンテンツを 1 つの SHA-1 値に削減できるためです。この 1 つに削減された SHA-1 値を後で使用して、同じコンテンツを正確かつ信頼できるかたちで作り直すことができます。キーやインデックスを使う、ごく一般的なルックアップ テーブルの実装に例えると、SHA-1 がキーで、コンテンツが値になります。さらに、変更されていないファイルは同じ SHA-1 値を生成するため、コミット間でファイルのコンテンツが変更されていない場合、Git の処理効率は高くなります。つまり、新しいオブジェクトを作成しなくても、commit オブジェクトは前回のコミットで使用された BLOB ID または ツリー ID の SHA-1 値をそのまま参照できます。ファイルが新しくコピーされることはありません。

分岐

Git ブランチを完全に理解するには、Git が内部で分岐を定義する方法を理解する必要があります。結論から言うと、head と HEAD という 2 つの重要な用語を理解することに尽きます。

まず、head (すべて小文字) とは、新しい commit オブジェクトごとに Git が管理する参照です。このしくみを示すため、図 8 に複数回のコミットと分岐の操作を示します。Commit 01 では、Git がリポジトリの最初の head 参照を作成し、既定で「master」という名前が付きます (master は、既定の名前という以外に意味をもたない属性名です。この名前は Git チームによって変更される可能性があります)。Git は新しい head 参照を作成するときに、ref\heads フォルダー内にテキスト ファイルを作成し、新しい commit オブジェクトの完全な SHA-1 をそのファイルに格納します。Commit 01 では、Git が「master」という名前のファイルを作成し、commit オブジェクト A1 の SHA-1 をその master ファイルに格納しています。Commit 02 では、Git が heads フォルダー内の master head ファイルを更新し、古い SHA-1 を削除して、A2 の完全な SHA-1 コミット ID に置き換えます。Git は Commit 03 でも同じことを行います。 heads フォルダー内の master head ファイルを更新して、A3 の完全なコミット ID を保持します。

1 つの head より便利な 2 つの head: heads フォルダーで 1 つ HEAD ファイルとさまざまなファイルを管理する Git
図 8 1 つの head より便利な 2 つの head: heads フォルダーで 1 つ HEAD ファイルとさまざまなファイルを管理する Git

heads フォルダー内の master というファイルは、そのファイルが指す commit オブジェクトの分岐の名前です。奇妙なことに、分岐の名前は、コミットのシーケンスではなく 1 つの commit オブジェクトを指します (この概念については、後ほど具体的に説明します)。

図 8 の「分岐の作成とファイルのチェックアウト」セクションを見てみましょう。ここでは、ユーザーが Visual Studio の印刷のプレビュー機能に対して新しい分岐を作成しています。ユーザーは、この分岐に「feat_print_preview」という名前を付け、master を基にして、チーム エクスプローラーの [新しいローカル ブランチ] ペインの [ブランチのチェックアウト] チェック ボックスをオンにしています。このチェック ボックスをオンにすることで、新しい分岐が現在の分岐になるよう Git に指示します (これについては、この後すぐに説明します)。背後では、Git が feat_print_preview という heads フォルダーに新しい head ファイルを作成し、commit オブジェクト A3 の SHA-1 値を格納します。その結果、heads フォルダーに存在する master と feat_print_preview という 2 つのファイルは、どちらも A3 を指すことになります。

Commit 04 では、Git が判断を迫られることになります。 通常、Git は heads フォルダー内のファイル参照の SHA-1 値を更新することになりますが、フォルダーには 2 つのファイル参照があります。更新すべきファイル参照はどちらでしょう。 ここで、HEAD の出番です。HEAD (すべて大文字) は .git フォルダーのルートにある 1 つのファイルです。このファイルは、heads フォルダー内の “head” (すべて小文字) ファイルを指します (“head” ファイルには決まった名前はありませんが、“HEAD” ファイルの名前は実際常に HEAD です)。 head ファイル HEAD は、次のcommit オブジェクトの親 ID として割り当てられるコミット ID を含みます。実践的な観点では、HEAD は DAG 上にある Git の現在位置をマークします。そのため、head は複数存在する可能性があっても、HEAD は常に 1 つしかありません。

8 に戻ると、Commit 01 では、HEAD は master という head ファイルを指し、master が A1 を指すことが示されています (つまり、master head ファイルには commit オブジェクト A1 の SHA-1 が含まれています)。Commit 02 では、HEAD が既に master ファイルを指しているため、Git が HEAD ファイルに手を加える必要はありません。これは、Commit 03 でも同様です。ただし、「新しい分岐の作成およびチェックアウト」のステップでは、ユーザーが分岐を作成し、[ブランチのチェックアウト] チェック ボックスをオンにすることで、分岐のためにファイルをチェックアウトしています。それに応じて、Git は HEAD を更新し、master ではなく feat_print_preview という head ファイルを指すようにします ([ブランチのチェックアウト] チェック ボックスをオンにしなければ、HEAD は master を指したままになります)。

HEAD を理解すれば、Commit 04 では Git が判断する必要がないことがわかります。 Git は HEAD の値を調べ、feat_print_preview head ファイルを指していることを確認するだけです。その後、B1 のコミット ID を含むように、feat_print_preview head ファイルの SHA-1 を更新する必要があることを認識します。

[ブランチのチェックアウト] のステップでは、ユーザーがチーム エクスプローラーの [ブランチ] ペインで [master] 分岐を右クリックして、[チェックアウト] を選択しています。それに応じて、Git は Commit A3 のファイルをチェックアウトして HEAD ファイルを更新し、master head ファイルを指すようにします。

これで、Git での分岐操作が非常に効率的かつ高速な理由が明らかになります。 新しい分岐の作成は、1 つのテキスト ファイル (head) を作成して別のファイル (HEAD) を更新することにすぎません。分岐を切り替えるのも、テキスト ファイル (HEAD) を 1 つ更新するだけです。その後、作業ディレクトリ内のファイルがリポジトリから更新されるときは、通常、パフォーマンスに少し影響します。

commit オブジェクトには分岐の情報が含まれません。 実際、分岐は HEAD ファイルのみで管理され、heads フォルダー内のさまざまなファイルの参照として機能します。ただし、Git を使う開発者が分岐を話題にするときは、master 分岐または新しい分岐を原点にする commit オブジェクトのシーケンスを指しています。前述の図 2 の場合、多くの開発者は A、B、C の 3 つの分岐があると考えます。 分岐 A は、A1 から A6 まで続くシーケンスです。A4 での分岐操作により、新しい分岐 B1 と C1 が作成されます。 したがって、分岐 B は B1 から B3 まで続くコミットのシーケンス、分岐 C は C1 から C2 まで続くコミットのシーケンスになります。

ここで忘れてはいけないのは、Git ブランチの正式な定義です。 Git ブランチは、単なる commit オブジェクトへのポインターです。さらに Git は、すべての分岐の分岐ポインター (head) と、現在分岐の 1 つの分岐ポインター (HEAD) を管理します。

次回は、リポジトリからのファイルのチェックアウトとリポジトリへのチェックイン、インデックスの役割、各コミット中に tree オブジェクトが構築されるしくみを取り上げる予定です。Git がストレージを最適化するしくみやマージのしくみ、差分の動作も調べる予定です。


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

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