Gitの深淵を覗く:オブジェクト指向で理解するGitの内部構造
こんにちは!日々コードを書いてパイプラインを回しているみなさん、Git使っていますか?
「`git add` して `git commit` して `git push` するだけ」——最初はそれでも十分開発を進められます。しかし、複雑なマージコンフリクトに直面したときや、ブランチを切り替えたとき、Gitの挙動に恐怖や疑問を感じたことはありませんか?
「なぜ `git checkout`(あるいは `git switch`)は、何万ファイルあっても一瞬で終わるのだろう?」
「削除したはずのコミットが、なぜか復元できるのはどうしてだろう?」
これらの疑問に対する答えは、Gitの「内部データ構造」に隠されています。実はGitの正体は、差分(Diff)を管理するツールではなく、驚くほどシンプルでエレガントな「オブジェクト指向データベース(キー・バリュー・ストア)」なのです。
この記事では、普段私たちが意識しないGitの低層世界(Plumbingコマンド)を実際に動かしながら、Blob、Tree、Commitといった内部オブジェクトの正体を解き明かします。これをマスターすれば、Gitは「ブラックボックスな魔法」から「意のままに操れる信頼できる相棒」へと変わりますよ。
—
1. 準備:ハンズオン用実験室(Playground)の構築
百聞は一見に如かずです。まずはまっさらな環境を作って、Gitの内部構造を観察する準備をしましょう。
1.1 Gitの動作確認と基本設定
まずはターミナルを開き、Gitが正しくインストールされているか確認します。
Gitのバージョン確認
git –version
期待する出力例: git version 2.40.0 (以上を推奨)
開発者の情報を設定(未設定の場合のみでOKです)
git config –global user.name “Your Name”
git config –global user.email “your.email@example.com”
git config –global init.defaultBranch main
1.2 実験用リポジトリの作成
Gitの仕組みを汚さずに観察するため、専用の実験ディレクトリを作成して初期化します。
実験用ディレクトリの作成と移動
mkdir git-internals-lab
cd git-internals-lab
リポジトリの初期化
git init
この時点で `.git` という隠しフォルダが作られます。ここにGitの全ての秘密が詰まっています。
—
2. Git内部を支える「3つの主要オブジェクト」
Gitはファイルをどう管理しているのでしょうか?Git内部には、主に以下の3つのオブジェクトが存在します。プログラミングの「オブジェクト指向」の考え方に非常に似ています。
| オブジェクト種別 | 役割 | オブジェクト指向での例え |
| :— | :— | :— |
| Blob (Binary Large Object) | ファイルの内容(データ)のみを保持。ファイル名や更新日時は持たない。 | クラスの「フィールド変数(値)」 |
| Tree | ディレクトリ構造を保持。ファイル名、権限、対応するBlobや他のTreeへのリンクを持つ。 | クラスの「構造/インスタンス」 |
| Commit | リポジトリの特定時点の「スナップショット(Tree)」と、親コミット、作者、メッセージ等のメタデータを保持。 | 状態を保持した「オブジェクトの保存データ」 |

(※概念図:Commitは1つのTop-level Treeを指し、Treeはファイル名とともにBlobや部分Treeを指し示します)
最大のポイントは、「すべてのオブジェクトは、自身のデータのSHA-1(またはSHA-256)ハッシュ値というユニークな40文字のIDで一意に識別される」という点です。これを「コンテンツ指向ストレージ(Content-Addressable Storage)」と呼びます。
—
3. ハンズオン:低層コマンド(Plumbing)でオブジェクトを生成してみよう
普段使う `git add` や `git commit` は、ユーザーに優しいPorcelain(磁器)コマンドと呼ばれます。
一方、Git内部を直接操作する低レベルなコマンドをPlumbing(配管)コマンドと呼びます。Plumbingコマンドを使って、実際にオブジェクトが生まれる瞬間を観察しましょう!
ステップ1: Blobオブジェクトの生成(ファイルの「中身」を保存)
ファイルを作成せず、直接Gitのデータベース(`.git/objects`)にテキストデータを保存してみます。
“Hello, Git Internals!” という文字列から Blob オブジェクトを作成し、データベースに書き込む(-w)
echo “Hello, Git Internals!” | git hash-object -w –stdin
出力例 (これがデータの内容から生成された40文字のハッシュ値です):
6c841e247942687a412c1b442ef53531b7829707
この時、Git内部で何が起きているか観察しましょう。
作成されたハッシュ値の「タイプ」を確認する (-t)
git cat-file -t 6c841e247942687a412c1b442ef53531b7829707
出力: blob
ハッシュ値の「中身(内容)」を表示する (-p)
git cat-file -p 6c841e247942687a412c1b442ef53531b7829707
出力: Hello, Git Internals!
【重要発見】
お気づきでしょうか?Blobオブジェクトには「ファイル名」が一切含まれていません。ただの純粋なデータのかたまりなのです。同じ内容のファイルがプロジェクト内に100個あっても、Blobオブジェクトは「1つ」しか作られません。これがGitの圧倒的な容量節約の仕組みです。
—
ステップ2: Treeオブジェクトの生成(「ファイル名と構造」の定義)
次に、ファイル構造を作るために実際のファイルを作成し、インデックス(ステージングエリア)に登録してみます。
実際にファイルを作成
echo “Hello, Git Internals!” > hello.txt
echo “This is a secret note.” > secret.txt
インデックス(ステージングエリア)に追加
git add hello.txt secret.txt
ステージングされた状態から Tree オブジェクトをデータベースに直接書き出す
git write-tree
出力例 (Treeオブジェクトのハッシュ値):
e58866579d4948a32d7dc5b4b1df87c5cf7a47ef
この Tree オブジェクトの中身を覗いてみましょう。
Tree オブジェクトの中身を確認
git cat-file -p e58866579d4948a32d7dc5b4b1df87c5cf7a47ef
【出力結果】
100644 blob 6c841e247942687a412c1b442ef53531b7829707 hello.txt
100644 blob 3e83b4823a3d548f0e5b7c7b8e1f57916960c18d secret.txt
見事です!Tree オブジェクトとは、「パーミッション(100644)」「オブジェクトの種類(blob)」「ハッシュ値」「実際のファイル名」を対にしたディレクトリ一覧表(インデックス)だったのです。
—
ステップ3: Commitオブジェクトの生成(「歴史とメタデータ」の付与)
最後に、この Tree オブジェクトを元にして「コミット」を作ります。
先ほどのTreeオブジェクトを指定して、コミットメッセージ付きでCommitオブジェクトを作成
git commit-tree e58866579d4948a32d7dc5b4b1df87c5cf7a47ef -m “最初のコミット:Git内部構造の理解”
出力例 (Commitオブジェクトのハッシュ値):
8f9a2b3c… (あなたの環境で固有のハッシュが出力されます)
作成された Commit オブジェクトの中身を `git cat-file -p` で覗いてみましょう!
コミットの中身を解析 (ハッシュ値は自分の環境のものを指定してください)
git cat-file -p HEAD
【出力結果】
tree e58866579d4948a32d7dc5b4b1df87c5cf7a47ef
author Your Name
committer Your Name
最初のコミット:Git内部構造の理解
Commit オブジェクトの本質はこれだけです。
1. トップレベルの Tree オブジェクト への参照
2. 親コミット (parent) のハッシュ値(2個目以降のコミットで自動付与)
3. 作者(Author)とコミッター(Committer) の情報・タイムスタンプ
4. コミットメッセージ
—
4. なぜ `git checkout`(ブランチ切り替え)は「爆速」なのか?
内部構造が分かると、冒頭の疑問がロジカルに解決します。
他の古いバージョン管理システム(SVNなど)は、ファイルごとの「差分(Diff)」を計算して書き換えるため、コミット数やファイル数が増えると処理が重くなります。
しかし、Gitは違います。
理由1: ブランチは「ただの40文字のテキストファイル」
Gitにおけるブランチ(`main` や `feature`)は、何重ものデータ構造ではなく、「特定のCommitハッシュが書かれただけのポインタ(テキストファイル)」です。
実際確認してみましょう。
HEADが指しているファイルを確認
cat .git/HEAD
出力: ref: refs/heads/main
mainブランチが指しているハッシュ値を確認
cat .git/refs/heads/main
出力: 8f9a2b3c… (Commitオブジェクトのハッシュ)
ブランチを作成したり切替えたりする処理は、この小さなテキストファイルの参照先を書き換える(数バイトの書き込み)だけなので、1秒もかからず一瞬で完了します。
理由2: 変更がないファイルは「ハッシュの比較」だけでスキップ
`git checkout` で別のブランチに移動する際、Gitは新旧コミットの Tree オブジェクト同士を比較します。
もしあるディレクトリやファイルのハッシュ値が全く同じであれば、データが変更されていないことが一瞬で判明するため、ディスクへの再書き込みを行いません。
変更があったファイルだけを Blob からワーキングツリーに復元する。これが、巨大なプロジェクトでもGitが爆速で動作する真の理由です。
—
5. まとめ:深淵を知ったあなたが手に入れた「極限の知見」
今回学んだGitの内部構造を整理しましょう。
1. Blob: ファイルの「中身」だけを保存するハッシュ空間
2. Tree: ファイル名とディレクトリ構造を管理する対応表
3. Commit: Treeへの参照と作成者、親コミットへのリンクを持つ歴史のノード
4. Branch: 最新のCommitオブジェクトのハッシュ値を保持する単なる「ポインタ」
これを知ることで、明日からの開発がどう楽になるか?
- `HEAD detached` エラーが怖くなくなる
「あ、HEADポインタがブランチ名ではなく、直接Commitオブジェクトのハッシュを指している状態だな」と理解できるため、慌てず `git switch -c new-branch` でポインタを元に戻せます。
- `git reflog` による完全復元の仕組みが解る
コミットを消してしまっても、Gitデータベースの中に Commit や Blob のオブジェクトが存在し続けている限り、ハッシュ値さえ特定できれば絶対に復元できるという強い自信が生まれます。
Gitは魔法ではなく、非常に理にかなった美しいデータ構造で設計されています。
この構造を頭に描くことができれば、複雑なコンフリクト解決やリベース作業も、自信を持って安全にコントロールできるようになりますよ!
ぜひ、ご自身の手元のターミナルで `git cat-file` を叩いて、Gitの美しい世界を体感してみてくださいね。