本質から理解するgit 内部構造編

Posted by KFTamang on Friday, January 1, 2021

TOC

gitシリーズ

本質から理解するgit2 コミット編

ごあいさつ

あけましておめでとうございます。 git使ってますか? gitはややこしくていまいち使いこなせない、すっきりと理解できないという方も多いかと思います。 かく言う自分も少し前まで良くわからず表層的な使い方だけして、すっきりしない思いを抱えていました。(今もそうかもしれません。)

でもgitの内部構造をなんとなく把握してからは、何をしているか理解しながら使っている感覚を持てるようになりました。 知人のできるエンジニアは「gitのメンタルモデルを構築する」というような感じに表現していました。 gitのメンタルモデルを頭の中に構築することはそこまで難しいことではないわりにあまり解説されていない気がするので、 そのような記事を書くことにしました。

想定読者

gitのコマンドを多少知っており、コミットやプッシュ等の基本操作はできるが、いまいち何をしているか自信の持てない方を想定しています。 コマンドの使い方や用語の解説は世の中に溢れているのでそちらに任せますが、基本的な用語しか登場しないと思います。

「メンタルモデル」とは

そもそもメンタルモデルとはなにかということですが、「gitというツールが一体どのようなデータ構造を用いて、 どのようにしてソースコードのバージョンを管理しているか。あるコマンドはデータに対してどのような動作をするのか」についての理解といえます。

「gitはソースコードのバージョン管理をする」という粒度よりさらに細かく、「gitはどのような形でソースコードやそのバージョンを管理するか」といった程度の粒度の理解を指します。 ここでは、さらに詳細な実装レベルの理解を求めません。というのも、本記事では「自分が何をしているか理解しながらgitを使う」ことを目標にしているからです。 この目標に対して実装レベルの理解はたいして貢献しないわりに、いたずらに覚えることを増やすだけです。

gitはどのようにソールコードを管理しているのか

では本題に入りましょう。 gitの動作を理解するうえで本質的なことは以下になります。

  1. ソースコードその他の管理対象は、ファイルごとに圧縮され、ファイル中身に(ほぼ)固有の数値(ハッシュ値)で管理される。
  2. ある時点でのスナップショット(gitで言うコミット)はディレクトリ構造を反映した木構造とハッシュ値で管理される。
  3. コミットを行うと、コミットを表すコミットオブジェクトと呼ばれる小さなファイルが生成される。
  4. ブランチ(の先頭)やタグなどは特定のコミットオブジェクトを指している。

です。 それぞれをもう少し詳しく解説していこうと思います。

1.ソースコードその他の管理対象は、ファイルごとに圧縮されハッシュ値で管理される

これはそのままですが、コミットを行おうとするとソースコードなどのファイルは圧縮され、その中身から生成されたハッシュ値をつけられて保管されます。 (これをBinary Large OBject, BLOBと呼びます。) ここで重要なのは、どのブランチのどのコミットもどのファイルもすべてが同じディレクトリに放り込まれて、並列に管理されることです。 正確には.git/objects以下にサブディレクトリが作られて、そこに配置されるようです。 サブディレクトリができるのは検索のしやすさのためで、本質的にはフラットな配置と変わらないと思います。

また、もう一つ重要な要素として、ファイルの中身が同じならば重複なくオブジェクトが一つだけ保存されます。 コミットは一部のファイルのみ変更が行わる場合も多く、そのような場合に無駄に重複するデータを保存することなく、ディスク容量を節約できます。

ソースコードは圧縮されてBLOBに

2. ある時点でのスナップショットはディレクトリ構造を反映した木構造とハッシュ値で管理される

gitのレポジトリにはディレクトリが作られて、階層構造で管理されることが多いと思います。そのような場合はディレクトリを表すオブジェクト(treeオブジェクト)が作られます。 といっても単純で、そのディレクトリが含むファイルまたはディレクトリに対応するオブジェクトが記載されているだけです。 このtreeオブジェクト自体もBLOBと同様に圧縮され、ハッシュ値が名前に付けられて管理されます。

そして重要なことですが、コミット時にはスナップショット全体を表すtreeオブジェクトも生成されます。

スナップショットはtreeオブジェクトとBLOBで保存される

3. コミットを行うと、コミットを表すコミットオブジェクトと呼ばれる小さなファイルが生成される

コミットはコミットオブジェクトと呼ばれる小さなファイルで管理されます。 コミットオブジェクトはスナップショット全体の親を指すハッシュ値、親のコミットオブジェクトのハッシュ値、コミットメッセージなどが書いてあります。 つまりコミットオブジェクトはその親のコミットオブジェクト…という風にどんどん辿っていけます。 コミットオブジェクト自体もただのテキストファイルなので、圧縮されハッシュ値で管理されます。

コミットごとにコミットオブジェクトが生成される コミットオブジェクトは有向グラフを形成する

4. ブランチ(の先頭)やタグなどは特定のコミットオブジェクトを指している

gitを使っているとブランチを作ったりします。このブランチはどのように表現されているかというと、そのブランチの最新のコミットオブジェクトのハッシュ値です。 例えばmasterブランチであれば、.git/branches/masterというファイルにコミットオブジェクトを示すハッシュ値が書き込まれています。 これだけです。このブランチの歴史を知りたければ、指されているコミットオブジェクトの親のコミットオブジェクトの、さらに親の…と辿っていきます。 このブランチに新たなコミットがなされた場合はコミットオブジェクトを生成した後、コミットオブジェクトのハッシュ値を新しいものに書き換えます。

また、リリースバージョンの指定などにタグを利用することもありますが、これも同様にコミットオブジェクトのハッシュ値が指定されているだけです。 こちらはブランチと違い、新たなコミットが行われても書き換えられることはありません。

まとめ

以上のようにgitは2系統のグラフ構造を持っています。一つはスナップショットを保存するためのツリー構造です。 これはtreeオブジェクトとBLOBからなっています。実際のディレクトリとファイルに対応しています。

もう一つはコミットのつながりを表すコミットオブジェクトの有向グラフです。 コミットオブジェクトは自分の親のコミットオブジェクトを表すハッシュ値と、スナップショットを表すtreeオブジェクトのハッシュ値を持っています。 そのコミットの内容を知りたければスナップショットを表すtreeオブジェクトを辿ればよいし、コミットの歴史を知りたければ親となるコミットオブジェクトを辿ればよいのです。

gitの内部構造として重要なのはこのふたつです。 ブランチやタグといったものは特定のコミット(に対応するコミットオブジェクトのハッシュ値)を指しているにすぎません。 案外データ構造は単純なことが理解していただけたでしょうか。 (もちろんこれは実用上の重要性とは別の話です。バージョン管理においてブランチ概念は最重要事項のひとつです。)

diffmergeresetなどはこの構造で保持されたデータをもとに、gitがよしなに行ってくれている操作です。 その実装の詳細を知ることは興味深く無駄にはならないかもしれませんが、メンタルモデルの構築にはあまり役立たないのでここでは触れません。 (というか自分もよくわかっていません。)

参考

Gitの仕組み (1) - こせきの技術日記

Gitを支える内部構造についての話

10.3 Gitの内側 - Gitの参照


comments powered by Disqus