Gitは、内部でオブジェクト指向のデータモデルを持つバージョン管理システムです。この構造は、データの一貫性と効率的な管理を可能にする基盤となっています。この記事では、Gitの4つの主要オブジェクトを中心に、その設計思想と具体的な動作を深掘りしていきます。
GitHubを利用していると、マージやコミットといった用語に出会いますよね。何となく利用しているこの用語、裏ではどんな動作をしているのか?って案外知らないと思います。
このページでは、Gitの裏側を理解し、Gitを使いこなすために必要なシステム知識に焦点を当てて解説します。
1. Gitのデータ構造の全体像
Gitは、変更履歴を単に「ファイルの差分」で記録するのではなく、プロジェクト全体の「スナップショット」を保存する仕組みを採用しています。スナップショットとは、プロジェクトの特定時点におけるすべてのファイルの状態を記録したものを指します。
用語:スナップショット
- 完全な記録
- スナップショットは、プロジェクト全体のディレクトリ構造とすべてのファイルの内容を記録します。
- Gitは変更がなかったファイルについて、過去のスナップショットのデータを再利用するため、効率的に管理することができます。(ですが、差分ではありません。)
- ファイル差分ではない
- 多くのバージョン管理システム(例: Subversion)はファイルの「差分」を記録しますが、Gitは「完全なスナップショット」を保存します。
- 実際には、内容が変化していないファイルのデータは過去のスナップショットを参照する形で保存するため、ストレージを節約します。そのため、
- 履歴の基盤
- スナップショットを連続して保存することで、プロジェクトの履歴が形成されます。
- これにより、過去の状態に戻ったり、変更の原因を追跡したりすることが容易になります。
Gitのスナップショットは、プロジェクトの完全な状態を記録する仕組みで、差分を記録するのではなく効率的に全体のスナップショットを管理します。この仕組みにより、高速な操作、簡単な履歴管理、信頼性の高いデータ保存が可能になっています。
そしてこのスナップショットの仕組みの核となるのが以下の4つのオブジェクトです。
- Blob: ファイルそのものを保持。
- Tree: ファイルやディレクトリの構造を表現。
- Commit: プロジェクトのスナップショットを記録。
- Tag: 特定のコミットに名前を付ける。
これらのオブジェクトは、SHA-1ハッシュという一意な識別子によって管理され、.git/objectsフォルダに保存されます。
2. Blobオブジェクト:Gitの基礎となるファイル単位のデータ
Blobは、ファイルの内容そのものを保持するオブジェクトです。このオブジェクトにはファイル名やパーミッション情報は含まれず、純粋にファイルの中身だけが保存されます。簡単に考えたいのであれば、Blobはファイルそのもののデータと考えてOKです。
Blobオブジェクトの特徴
- ファイル内容を保存
- Blobオブジェクトには、テキストファイルやバイナリファイルの内容そのものが保存されます。
- ファイル名やそのファイルがどのディレクトリに所属しているかなどの情報は後述するTreeオブジェクトが管理します。
- SHA-1ハッシュによる一意性
- Blobオブジェクトは、その内容に基づくSHA-1ハッシュ値で一意に識別されます。
- 同じ内容のファイルであれば、異なるブランチやリポジトリ内でも同じBlobとして扱われます。
- 変更があった場合は新しいBlobが作成される
- ファイルの内容が変更されると、新しいBlobオブジェクトが作成され、古いBlobはそのまま保持されます。
- QBlobが、もしファイルそのものだとしたら、変更が加えられると、Blobは内部的に2つに分かれる=リポジトリ全体の容量が増える理解でよいか?
- A
Gitにおいて、Blobはファイル内容そのものを保持するオブジェクトだが、ファイルが変更されるたびに新しいBlobオブジェクトが作成されるため、その理解はおおむね正しい。しかし、Gitは内部的に効率的な方法でデータを管理するため、リポジトリの容量が単純に増え続けるわけではない。
参考 リポジトリ全体の容量に与える影響
- 短期的には容量が増える
- 変更ごとに新しいBlobが作成されるため、短期的にはリポジトリ全体の容量が増加します。
- 長期的には圧縮(パックファイル)が適用される
- Gitはリポジトリを効率的に管理するために、パックファイルという仕組みで不要なデータを圧縮します。
- 複数のBlob間のデルタ圧縮を行い、差分のみを保存する形に変換します。
- これにより、容量の増加は最小限に抑えられます。
3. Treeオブジェクト:ディレクトリ構造を表現する要
Treeオブジェクトは、Gitでディレクトリ構造を表現するオブジェクトです。ディレクトリ内のファイルやサブディレクトリを管理し、それらがどのBlobオブジェクトや他のTreeオブジェクトに対応しているかを記録します。
Treeオブジェクトの役割
- ディレクトリの表現
- Treeオブジェクトは、ディレクトリ内に含まれるファイルやサブディレクトリの一覧を保持します。
- ファイル名やパーミッションの管理
- 各エントリには、ファイル名、ファイルの種類(BlobまたはTree)、ファイルのパーミッション情報が含まれます。
- Blobや他のTreeへの参照
- Treeオブジェクトは、ファイルそのもの(Blob)やサブディレクトリ(別のTree)へのSHA-1ハッシュ値で参照を持っています。
/ ├── file1.txt ├── dir1/ │ ├── file2.txt │ └── file3.txt └── dir2/ └── file4.txt
仮に上記のようなディレクトリ構造があるとすると、root(/)ディレクトリのTreeオブジェクトは以下のようなデータを保持します。
100644 blob <ハッシュ値1> file1.txt 040000 tree <ハッシュ値2> dir1 040000 tree <ハッシュ値3> dir2
参考 変更があった場合のBlobとTreeの関係
- Blobの生成
- ファイルの内容が変更されると、その新しい内容に基づいて新しいBlobオブジェクトが作成されます。
- 元のBlobオブジェクト(古い内容)はそのままリポジトリ内に保持され、新しいBlobが既存のTreeやCommitから参照されるようになります。
- Treeの更新
- ファイルが属するディレクトリを表すTreeオブジェクトが更新されます。
- 具体的には、変更されたファイルに対応するBlobのSHA-1ハッシュがTreeオブジェクト内で置き換えられます。
- 他のファイルやディレクトリに変更がない場合、それらに対応する参照は再利用されます。
4. Commitオブジェクト:履歴管理の中核
Commitオブジェクトは、履歴を管理する中核的な存在で、プロジェクトの特定時点における状態(スナップショット)を記録します。各コミットには、その時点のディレクトリ構造や変更内容に関する重要な情報が含まれています。
Commitオブジェクトの役割
- スナップショットの記録
- Commitオブジェクトは、特定時点のプロジェクト全体を表すTreeオブジェクトを指します。
- 履歴の管理
- 各Commitオブジェクトは親コミット(または複数の親コミット)への参照を持ち、これによって履歴が連続的に追跡されます。
- メタデータの保存
- コミット作成者、日時、コミットメッセージといったメタ情報を記録します。
以下は、Commitオブジェクトの内部構造を示したものです。
tree <TreeオブジェクトのSHA-1ハッシュ> parent <親コミットのSHA-1ハッシュ> # 初回コミットには存在しない author <作者名> <メールアドレス> <タイムスタンプ> <タイムゾーン> committer <コミッター名> <メールアドレス> <タイムスタンプ> <タイムゾーン> <空行> <コミットメッセージ>
参考 上記Commitオブジェクトの各要素の説明
- tree
- Treeオブジェクトへの参照を持ち、その時点のプロジェクトのディレクトリ構造を表します。
- 変更があった場合、この参照が新しいTreeオブジェクトを指します。
- parent
- 親コミットへの参照です。これにより、コミット履歴が連続してつながります。
- マージコミットでは、複数の親を持つことがあります。
- 初回コミットには
parent
はありません。
- author
- コミットを最初に作成した人(通常は実際にコードを書いた人)。
- 名前、メールアドレス、日時が記録されます。
- committer
- 実際にコミットをリポジトリに登録した人(通常はauthorと同じですが、リベースなどの操作では異なる場合があります)。
- コミットメッセージ
- このコミットで行われた変更内容や意図を説明するメッセージ。
- チーム開発では、わかりやすく簡潔なメッセージを書くことが推奨されます。
Blob、Tree、Commitの関係
- Blob
- ファイルそのものの内容を表すオブジェクト。
- ファイル名やパーミッションは含まず、純粋に内容だけが記録されます。
- Tree
- ディレクトリを表すオブジェクト。
- ディレクトリ内のファイルやサブディレクトリへの参照(BlobまたはTree)を持ち、ファイル名やパーミッションも記録します。
- Commit
- Treeオブジェクトへの参照を持つことで、その時点のディレクトリ構造を記録します。
- 親コミットへの参照、作者情報、コミットメッセージを含み、変更履歴を管理します。
Commitオブジェクト └── Treeオブジェクト(ルートディレクトリ) ├── Blobオブジェクト(ファイル1の内容) ├── Treeオブジェクト(サブディレクトリ1) │ ├── Blobオブジェクト(ファイル2の内容) │ └── Blobオブジェクト(ファイル3の内容) └── Treeオブジェクト(サブディレクトリ2) └── Blobオブジェクト(ファイル4の内容)
これらのオブジェクトを必要最小限の変更を加えることで、最小限の容量で資源のバージョン管理を可能にしているというわけです。
次に説明するTagオブジェクトも重要ですが、Tagオブジェクトはあくまでも補助的な役割です。基本的には、Blob、Tree、Commitの3つのオブジェクトこそ、Gitの基本的なデータ構造を構成する中核であり、これらの仕組みを理解することがGit全体の動作を理解するための基礎となります。
5. Tagオブジェクト:重要なポイントへのラベル付け
Tagオブジェクトは、Gitで特定のコミット(または他のオブジェクト)に対してラベルを付けるためのオブジェクトです。主にバージョン管理や重要なマイルストーンに名前を付けるために使用されます。
Tagオブジェクトの役割
- 特定のコミットへのラベル付け
- バージョン番号(例:
v1.0.0
)やリリース名(例:release-2023-Q4
)として使用されます。 - チーム開発において、特定の状態を分かりやすく示すために役立ちます。
- バージョン番号(例:
- Git履歴の可読性向上
- 長いSHA-1ハッシュ値の代わりに、人間が理解しやすい名前でコミットを参照できます。
- 安定版の指標
- たとえば、製品のリリース時点のコミットにタグを付けておけば、後からその状態を簡単に復元できます。
- QTagオブジェクトは単に、コミットにしるしをつけるだけ、その関連性を管理しているだけでよいか?
- A
結論として、Tagオブジェクトは特定のオブジェクト(通常はCommitオブジェクト)に対する「目印」を提供するためのものであり、その理解で正しい。Gitの履歴やデータ構造そのものに変更を加えるわけではなく、関連性(どのコミットがどのタグに対応するか)を管理する補助的な役割を担っています。