🌳

【Git】最初は理解できなかったGitの3つの仕組み

に公開

Gitでバージョン管理、していますか?

株式会社アドバンテッジリスクマネジメント 開発チームの古堅です。

Gitといえばソースコードのバージョン管理システムですが、ずっと「Gitって便利だけど、よく分からないなー」と感じていました。
Gitのことをもっと理解したい!という思いから、参考書籍を開き、特に内部でどのようなことをしているのかに関心を持って学びました。

今回は、個人的に面白い!すごい!と感じた、Gitの基盤となっている3つの仕組みをご紹介します。なぜGitがこのような設計になっているのかを理解できれば、バージョン管理が今よりきっと楽しくなると思います。

3つの仕組み

Gitとは?

Gitとは、「コンテンツの変化を管理するための分散型バージョン管理システム」 です。

集中型のバージョン管理が主流だったなか、「もっと高速にしたい」「履歴を完全に分散させたい」という思いを元に、分散型のバージョン管理システムとしてGitが誕生しました。

gitとは出典:【Pro Git】1.1 使い始める - バージョン管理に関して

最初はハッカーにしか使えないくらいのものだったそうですが、多くの開発者の協力を経て、今では世界中のエンジニアにとって欠かせないツールとなっています。

そんなGit...

そんなGitですが、個人的には以下のような理由から苦手意識がありました。

  • 覚えるコマンド多すぎない?(addcommitfetch...)
  • 状態の概念が難しくない?(ワーキングツリー?ステージ?...)
  • ブランチってなに?今、ここはどこ?
  • ちょっとしたコマンドに潜む恐怖(reset --hardpush --force...)

しかし、一見ややこしい設計にも、履歴を守る・管理するための思想があリます。
その思想を感じられる3つの仕組みを見ていきましょう。

最初は理解できなかったGitの3つの仕組み

① 📥 3つの状態で履歴を管理する

Gitは、以下の3つの状態を使って履歴を段階的に管理します。

■ 作業ディレクトリ

実際にファイルを編集する場所です。

■ ステージングエリア

次の履歴に含める変更を、一時的に置いておく場所です。この概念が最初は馴染まず厄介でした。例えとしては「舞台袖」「下書き保存」など。名称はインデックスとも呼ばれます。

■ .gitディレクトリ(リポジトリ)

確定した履歴(コミット)を保存する場所です。ここが本丸の履歴管理場所です。ここに来るまでにはステージングエリアを通る必要があります。

3つの状態で履歴を管理する出典:【Pro Git】1.3 使い始める - Gitの基本

これによって何が嬉しいのかというと、履歴の内容を整理することで間違った変更を予防できることです。Gitは自動で保存などしません。自分で「これを保存する!」と明示的に指示するという方針を採用しているので、意図しない変更が入り込まないようになり、履歴が整理できます。この仕組みがあるからこそ、余計な心配をせずに気軽にファイルの編集ができると感じます。

利用シーン:
✅ 複数の箇所を同時に修正中、特定ファイルだけ履歴に登録したいとき
✅ 履歴登録前に、変更内容を確認・整理したいとき
✅ 試験的な変更として、履歴には含めずに一時的に保存しておきたいとき

まとめると、3つの状態で履歴を管理することのポイントは以下です。

ポイント1

また、このことを意識してから多様なコマンドも理解しやすくなりました。

例えば、git addgit commitは、どちらも履歴を次の状態へ移すという観点で同じだと気づきました。git reset(過去の状態に戻すコマンド)も、オプションによってどの状態に影響を加えているのかがはっきりしました。(以下表参照)

作業ディレクトリ ステージングエリア .gitディレクトリ
--soft 保持 保持 ↩️ 戻す コミットだけ取り消し
--mixed 保持 ↩️ 戻す ↩️ 戻す コミットとステージング
取り消し
--hard ↩️ 戻す ↩️ 戻す ↩️ 戻す 全部取り消し

※デフォルトは --mixed

② 📸 差分ではなくスナップショット

Gitでは履歴を、ある時間におけるすべてのデータのスナップショットとして保存します。
これは、Gitと他のバージョン管理システムとの大きな違いのひとつです。

他のバージョン管理システム(Subversion、CSVなど)では多くの場合、下の図のように、ファイルごとの変更の情報を保持して、バージョンを管理します。

差分によるバージョン管理出典:【Pro Git】1.3 使い始める - Gitの基本

一方Gitは、下の図のように、その時その時の全てのファイルを、スナップショットのように保持してバージョンを管理します。

スナップショットによるバージョン管理出典:【Pro Git】1.3 使い始める - Gitの基本

パッと考えると、「差分だけを保持していけば、必要な情報が最小限で済むし効率が良いよね!」ってなりそうですよね。
しかし、Gitでは、差分ベースの方式ではなく、その時点での完全な状態を保持するスナップショット方式を採用することで、早くて確実な履歴の巻き戻しを実現しています。

従来のバージョン管理システムだと、履歴を巻き戻すには、今までの変更を連続して適用することで復元していました。これだと計算や整合性チェックが大変です。それに対してスナップショット方式だと、計算などせず素早くその時点の状態を復元することができるのです!

冷蔵庫の在庫管理に例えると、料理したメニューを元に差分で辿るより、その時々の写真をとっていた方が早くて確実に辿れますよね(そんなことしたことないですが)。

コミットがこのスナップショットに対応しています。差分はコミット同士を比較して計算することで求めることでき、任意のコミット間の差分を抽出するなど、履歴の変化を柔軟に管理することができます。

利用シーン:
✅ 複数の履歴を行き来しながら、編集作業を進めることができる
✅ 異なる履歴間の差分を比較できる(AとBの差分・XとBの差分)
✅ 履歴が壊れた場合でも、過去のスナップショットから復旧が可能

まとめると、差分ではなくスナップショットで管理することのポイントは以下です。

ポイント2

③ 🧬 オブジェクトのハッシュでデータを管理する

  • オブジェクトとは、情報やデータ、属性や振る舞いなどをひとまとまりにした実体のことです(本で例えると、本そのもの)。
  • ハッシュとは、元のデータから一定の計算をして求められた、規則性のない値です。データの内容が同じであれば何回計算しても同じ値となり、データの内容が1文字でも違えば、全く異なる値になります(本で例えると、本のバーコードのようなものでしょうか)。

Gitでは、データの実体として以下3つのオブジェクトを基本として扱います。
そして各オブジェクトには、中身をハッシュ化したキーを付与します。

■ blobオブジェクト

ファイルの内容を持つオブジェクトです。あくまでもファイルの中身のみを持っており、ファイル名は持ちません。git addコマンドで生成されます。

■ treeオブジェクト

ファイル名と、対応するblobオブジェクトへの参照情報を持つオブジェクトです。フォルダのようなイメージで、複数のblobオブジェクトへの参照情報を持つことができます。git commitコマンドで作成されます。

■ commitオブジェクト

最上階のtreeオブジェクトや親となるcommitオブジェクトへの参照情報を持つオブジェクトです。履歴をスナップショットたらしめるものです。git commitコマンドで生成されます。

オブジェクトのハッシュでデータを管理する出典:【Pro Git】10.2 Gitの内側 - Gitオブジェクト

つまり、Gitの内部では、自分たちが普段見ている「ファイル・ディレクトリ構成」をそのまま保持しているわけではありません。代わりに、各ファイルの内容をハッシュで一意に識別し、それらを繋いで管理しています。
このハッシュによる管理が、Gitが高速であることの鍵です。同じ内容なら同じハッシュになるので、重複したデータの保存を防いだり、ハッシュの比較によって大量ファイルでも差分を瞬時に見つけることができます。

以下のようなディレクトリ構成で、ハッシュ管理の例を見てみます。

my-project/
├── notes.txt
├── todo.txt
└── src/
    └── main.js

Gitは以下のように、ファイルの中身(blob)とフォルダ構成(tree)をオブジェクトとそのハッシュで管理しています。

tree: my-project             → ハッシュ: ccc01
  ├── blob: notes.txt        → ハッシュ: bbb11
  ├── blob: todo.txt         → ハッシュ: bbb22
  └── tree: src/             → ハッシュ: ttt01
         └── blob: main.js   → ハッシュ: bbb33

ここで、src/main.jsを編集しました。この時Gitでは、変更があったファイル(blob)と、それを含むフォルダ(tree)のオブジェクトだけを新しく作成します。

tree: my-project             → ハッシュ: ccc02(src/が変わったので新しくなる)
  ├── blob: notes.txt        → ハッシュ: bbb11(変更なし、再利用)
  ├── blob: todo.txt         → ハッシュ: bbb22(変更なし、再利用)
  └── tree: src/             → ハッシュ: ttt02(main.jsが変わったので新しくなる)
         └── blob: main.js   → ハッシュ: bbb44(変更後)

このように、変更があった部分だけを新しく記録することで、ハッシュを見れば状態の変化が瞬時に辿れるようになっています。また、同じ内容のファイルはオブジェクトを再利用することで、スナップショット方式を採用しながらストレージも節約しています。

まとめると、オブジェクトのハッシュでデータを管理することのポイントは以下です。

ポイント3

利用シーン:
✅ 差分を爆速で確認できる
✅ 大規模なシステムでも高速に履歴検索が可能

実際の流れ: 新規ファイルを履歴に残すまで

ここまでGitの内部で行われている3つの仕組みをご紹介しました。

それでは、hello.txtというファイルを作ってからGitでバージョン管理するまで、実際に何が起きているのかを見てみます。

1. 作業ディレクトリにファイルを作成

echo 'Hello, world!' > hello.txt

3つの状態のうち今はまだ作業ディレクトリに存在するだけで、履歴として残されていません。
① 📥3つの状態で履歴を管理する

2. ファイルをステージングエリアに追加

git add hello.txt

addコマンドで、ファイルをステージングエリアに追加します。この時、hello.txtの中身"Hello, world!"からハッシュ値を生成し、blobオブジェクトとして保存します。
① 📥3つの状態で履歴を管理する② 📸 差分ではなくスナップショット③ 🧬 オブジェクトのハッシュでデータを管理する

blobオブジェクトは作られましたが、ここでもまだ履歴(commitオブジェクト)は作成されていません。

3. コミットして履歴に記録

git commit -m 'add hello.txt'

ステージングエリアの情報をもとに、treeオブジェクト(ファイル名・blobへの参照)が作成されます。そして、最上位のtreeオブジェクトの参照・時刻・コミットメッセージなどを含んだcommitオブジェクトが作成され、.gitディレクトリに保存されます。
① 📥3つの状態で履歴を管理する② 📸 差分ではなくスナップショット③ 🧬 オブジェクトのハッシュでデータを管理する

以上のように、git addgit commitコマンドを使うことで、新規ファイルをGitの履歴に登録することができました。作られたオブジェクト間の関係は以下のようになっています。

オブジェクト構造

全てを手動でやってみたい方へ

git addgit commitはGitの動きをまとめて、開発者が扱いやすくした "磁気コマンド"(高レベルの意味)と呼ばれます。一方、 "配管コマンド"(低レベルの意味)を使うと、オブジェクト作成など一つ一つの処理を手動で進めることができます。

echo 'Hello, world!' > hello.txt

# ファイルの内容からblobオブジェクトを作成
git hash-object -w hello.txt
# 出力例(blobオブジェクトのハッシュ値)
# af5626b4a114abcb82d63db7c8082c3c4756e51b

# ステージングエリアに登録
git update-index --add --cacheinfo 100644 af5626b4a114abcb82d63db7c8082c3c4756e51b hello.txt

# ステージングエリアの内容からtreeオブジェクトを作成
git write-tree
# 出力例(treeオブジェクトのハッシュ値)
# 9bae58e071c4601a7f7dabbdf7562645b32f64d8

# commitオブジェクトの作成
git commit-tree 9bae58e071c4601a7f7dabbdf7562645b32f64d8 -m 'Plumbing commit'
# 出力例(commitオブジェクトのハッシュ値)
# 1b8de21f876200801ce97a4d9c12e233613fe971

# ブランチの参照を更新
git update-ref refs/heads/main 1b8de21f876200801ce97a4d9c12e233613fe971

参考: 【Pro Git】10.1 Gitの内側 - 配管(Plumbing)と磁器(Porcelain)

その他の特徴

今回は3つをピックアップしてご紹介しましたが、Gitにはその他にもたくさんの設計上の工夫や開発者にとって嬉しいポイントがあります。

  • 内容ベースの追跡でファイル名の変更を自動検出
  • パックファイル作成することで、ディスク使用量を節約
  • オープンソースで誰でも無料で使える
  • 補助コマンドや補助ツールが豊富で、カスタマイズ性が高い
  • Linux・Mac・Windowsなど環境に依らず利用できる and more

まとめ

Gitは便利なツールでありながら、考え抜かれた設計思想のかたまりでした!
Gitの内部でどのようにデータが管理されているかを知ることで、これまで感じていた苦手意識が和らぎました。まだ理解できていない仕組みも多いので、これからも少しずつ紐解いていきたいです。

ここまで読んでいただき、ありがとうございました。

参考書籍

Pro Git 実用 Git
Pro Git 実用 Git
・無料で読める
・GitHub創業メンバーが執筆
・Gitの内部構造を解説
・実例コマンドが丁寧
・図が多くて理解しやすい
・実務的な内容も網羅
https://git-scm.com/book/ja/v2 https://www.oreilly.co.jp/books/9784814400614/
ARMテックブログ

Discussion