この記事で伝えること
git commit や git log は毎日使うのに、「Gitの中で何が起きているか」はあまり意識したことがない、という方は多いと思います。
この記事では、Gitが内部でどのようにデータを管理しているかを解説します。内部構造を知ることで、リベース・マージ・チェリーピックの動作が直感的に理解でき、トラブル時の対処もしやすくなります。
Gitの基本 — 「差分」ではなく「スナップショット」
まず大前提として、GitはSVNのような差分管理ツールではありません。コミットのたびに「その時点のファイルツリー全体のスナップショット」を記録しています(同一内容のファイルは共有して重複しない)。
この設計が、ブランチの切り替えや履歴の巻き戻しを高速にしている理由です。
Gitオブジェクトの4種類
Gitは内部で .git/objects/ ディレクトリにオブジェクトをハッシュ(SHA-1)で保存します。種類は4つです。
| オブジェクト | 役割 |
|---|---|
| blob | ファイルの内容そのもの |
| tree | ディレクトリ構造(blobやtreeへの参照) |
| commit | スナップショット(treeへの参照 + メタ情報) |
| tag | 注釈付きタグ(commitへの参照) |
具体的な構造を見てみる
# 試しに新規リポジトリを作って1ファイルをコミット
mkdir git-internal-demo && cd git-internal-demo
git init
echo "Hello, Git!" > hello.txt
git add hello.txt
git commit -m "first commit"
この時点で .git/objects/ に何があるか確認します。
find .git/objects -type f
# 例:
# .git/objects/8a/b686eafeb1f44702738c8b0f24f2567c36da6d ← blob
# .git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 ← tree
# .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 ← commit
blobオブジェクト
blobはファイルの「中身」だけを持ちます。ファイル名すら持ちません。
git cat-file -t 8ab686ea # → "blob"
git cat-file -p 8ab686ea # → "Hello, Git!"
同じ内容のファイルは、名前が違っても同一blobを参照します。 これが重複排除の仕組みです。
treeオブジェクト
treeはディレクトリを表し、「ファイル名 + パーミッション + blobへのハッシュ参照」を持ちます。
git cat-file -p HEAD^{tree}
# 100644 blob 8ab686ea... hello.txt
サブディレクトリがある場合は、treeが別のtreeを参照します。これがファイルシステムのディレクトリ構造に対応しています。
commitオブジェクト
commitは以下の情報を持ちます:
git cat-file -p HEAD
# tree 4b825dc6... ← そのコミット時点のルートtree
# parent e6c3a7f9... ← 1つ前のcommitのハッシュ
# author ...
# committer ...
#
# first commit
parent が前のコミットを指すことで、コミット履歴はリンクリスト(連鎖)になっています。
ブランチとは何か
多くの人が「ブランチ=コピー」とイメージしますが、実際は違います。
ブランチは特定のcommitハッシュを指す「ポインタ(テキストファイル)」に過ぎません。
cat .git/refs/heads/main
# e3f0a1c2... ← mainブランチが指すcommitのSHA-1
git checkout -b feature をしても、新しいcommitは作られません。同じコミットを指す新しいポインタが追加されるだけです。だからブランチの作成が一瞬で終わります。
HEADとは何か
HEAD は「今いるブランチ(またはコミット)」を指す特別なポインタです。
cat .git/HEAD
# ref: refs/heads/main ← mainブランチを経由してcommitを参照
git checkout でブランチを切り替えると、この HEAD ファイルの内容が変わります。detached HEAD 状態はブランチを経由せず直接commitハッシュを書いている状態です。
コミットをやり直す仕組み(resetとrebase)
内部構造を知ると、git reset も怖くなくなります。
# HEAD~1 まで戻る(1つ前のcommitをHEADにする)
git reset --soft HEAD~1
これは「HEAD が指すcommitを1つ前に変える」だけです。ファイルは変わりません(--softの場合)。
git rebase は「別のcommitのツリーを親としてコピーを作り直す」操作です。commitハッシュが変わるのはそのためです。
筆者の考え・所感
Gitを使い始めた頃、私は「コミット=差分の記録」だと思っていました。
差分管理なら何十回もコミットすると遅くなりそう……と漠然と不安だったのですが、スナップショット方式と知って腑に落ちました。内容が同じファイルはblobを共有するので、軽量なのです。
ブランチが「ポインタ」だと知ったときの衝撃は大きかったです。「git branch feature で何十MBもコピーされる」と思っていたのが、実際にはたった数十バイトのファイルが増えるだけだったとわかったとき、Gitへの信頼感がぐっと上がりました。
内部構造を知ると、git reflog(削除されたcommitのハッシュを探すコマンド)が「失われたcommitを救える理由」も直感的にわかります。オブジェクトはGCが走るまで .git/objects/ に残っているからです。
まとめ
- Gitは差分でなくスナップショットを保存する
- データは blob(ファイル内容)・tree(ディレクトリ)・commit(スナップショット) の3種のオブジェクトで表現される
- ブランチは「commitハッシュを指すポインタファイル」に過ぎず、切り替えコストはほぼゼロ
- 内部構造を知ることで、
resetやrebaseの動作が直感的に理解できるようになる