CH 16 / 75第3部 Gitの内部構造 ── なぜそう動くのか

Gitは差分ではなくスナップショットを保存する

このページで分かること

  • 「差分方式」と「スナップショット方式」の違いが分かる
  • Git の各コミットが、その時点の全ファイルの状態を持っていることを確かめられる
  • 全部を持っても容量が増えない理由が分かる
  • git diff が表示する差分が、どこから来ているかが分かる
このページの目次8 items

ここから第3部です。第2部では Git の操作を覚えました。第3部では、その裏で Git が何を保存し、どうたどっているかを見ます。

初めて読むときは流し読みで構いません。ただ、ここを通しておくと、第4部のブランチやマージが「手順の暗記」ではなく「仕組みからの理解」になります。とくに第3部の18・19は、第4部の直接の前提です。

2つの記録の仕方#

上段は Git のスナップショット方式で、1回目・2回目・3回目のそれぞれが3つのファイルすべての状態を持ち、変わっていないファイルは前と同じ中身を指すだけ。下段は差分方式で、1回目だけが全体を持ち、2回目と3回目は前からの変化だけを持つ

バージョン管理のソフトには、大きく2つの記録の仕方があります。

  • 差分方式:最初の状態と、そこからの変化(差分)を積み重ねて記録します。3回目の全体を見るには、1回目から順に差分を当てはめていきます
  • スナップショット方式:記録のたびに、その時点の全ファイルの状態を丸ごと持ちます。どの回も、それだけで全体がそろいます

Git は後者です。スナップショット(snapshot)は「その瞬間を写した1枚の写真」という意味で、コミットはまさにこの1枚です。第1部の05ではコミットを「変更の記録」と呼びましたが、正確には「その時点の全体の写真」が数珠つなぎになっていて、写真どうしを比べると変更が分かる、という構造です。

本当に全部を持っているのか確かめる#

コミットが何を持っているかは、git ls-tree で一覧できます。練習用フォルダの最新のコミットを見てみます。

text7 lines
cd ~/git-practice
git ls-tree HEAD

# 出力:
# 100644 blob 64442fd642ae90b9c4be6583fa758d8b2e884f66  .gitignore
# 100644 blob a2a4dd6683d9012c00d693af6c5b1f02f225a091  shopping.txt
# 100644 blob d3038db570e4c542737f1ab3a559741e5af3628f  todo.txt

最新のコミットは第2部の15の「Revert」で、変えたのは todo.txt だけでした。それでも、3つのファイルすべてが並んでいます。各行の長い英数字は、そのファイルの中身を指すIDです(第3部の17で詳しく扱います)。

1つ前、2つ前のコミットも見てみます。

text13 lines
git ls-tree HEAD~1

# 出力:
# 100644 blob 64442fd642ae90b9c4be6583fa758d8b2e884f66  .gitignore
# 100644 blob a2a4dd6683d9012c00d693af6c5b1f02f225a091  shopping.txt
# 100644 blob 04db51d409e081cec2c991dd2a558fee63053023  todo.txt

git ls-tree HEAD~2

# 出力:
# 100644 blob 64442fd642ae90b9c4be6583fa758d8b2e884f66  .gitignore
# 100644 blob a2a4dd6683d9012c00d693af6c5b1f02f225a091  shopping.txt
# 100644 blob d3038db570e4c542737f1ab3a559741e5af3628f  todo.txt

ここから2つのことが読み取れます。

  1. 変わっていないファイルは、同じIDのままです。.gitignore と shopping.txt は3つのコミットで同じIDを指しています。中身を3回保存しているのではなく、同じ1つの中身を3つのコミットが指しているのです
  2. 最新と2つ前の todo.txt は同じID(d3038db…)です。第2部の15の revert で「郵便局に行く」を打ち消した結果、todo.txt は2つ前とまったく同じ中身に戻りました。Git は中身が同じなら同じIDを付けるので、新しく保存せず、以前の中身をそのまま指しています

全部を持っても重くならない理由#

毎回すべてのファイルを持つと聞くと、記録のたびに容量が増えそうに思えます。実際には、次の2つの仕組みで抑えられています。

  • 同じ中身は1つしか保存しない:上で確かめたとおり、変わっていないファイルは前と同じ中身を指すだけです。新しく保存されるのは、変わったファイルの分だけです
  • 保存するときに圧縮する:中身はすべて圧縮して保存されます。さらに、記録がたまると Git は保存用のファイルをまとめ直し、似た中身どうしを差分の形で詰めて容量を減らします(第3部の20で見ます)

2つ目は一見「やはり差分方式では」と思える点ですが、これは保存するときの詰め方の話です。Git がコミットを扱うときの考え方は、あくまで「それぞれが全体の写真」です。どのコミットを開くときも、別のコミットから順に組み立て直す必要はありません。

では、git diff の差分はどこから来るのか#

第2部の10や11で見た差分は、どこにも保存されていません。git diff や git log -p を実行するたびに、2枚の写真を比べて計算し直しているのです。

第2部の10の git diff の出力に、次の行がありました。

text1 lines
index 520f940..d3038db 100644

この 520f940 と d3038db は、比べた2つの todo.txt の中身のIDです。上の git ls-tree に出てきた d3038db… と同じものです。差分は「この中身」と「あの中身」を比べた結果を、その場で表示したものだったわけです。

スナップショットだから得られること#

  • どの時点にもすぐ戻れる:目的のコミットの写真を開くだけで、途中を順にたどる必要がない
  • ブランチを気軽に作れる:第4部の21で見るとおり、ブランチは「どの写真を指すか」の名前でしかない
  • 壊れたら分かる:中身からIDを計算しているので、保存した内容が少しでも壊れるとIDと合わなくなる

よくある疑問#

Q. 大きなファイルを少しだけ変えると、毎回まるごと保存されるのですか?
A. 変えたファイルは、その時点の中身が新しく保存されます。ただし圧縮され、あとで保存用のファイルをまとめ直すときに似た中身との差分で詰められるので、何倍にも膨らむことはありません。動画や画像のように圧縮済みで差分が効きにくいファイルは、容量が増えやすいので注意が必要です。

まとめ#

  • Git のコミットは、前からの差分ではなく、その時点の全ファイルの状態(スナップショット)を持つ
  • 変わっていないファイルは、前のコミットと同じ中身を指すだけで、重複して保存されない
  • 中身が同じなら、いつ記録しても同じIDになる
  • git diff の差分は保存されておらず、2つのスナップショットを比べてその場で計算している
  • 保存時の圧縮で差分を使うことはあるが、コミットの扱い方はあくまでスナップショット

次のページ#

17. 4つのオブジェクト(blob / tree / commit / tag) ── スナップショットの中身を開けて、Git が実際に保存している4種類の部品を見ます。