SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

すぐ使いこなせるようになりたい人のためのGit入門

Gitの内部構造をよく理解して、うまく使おう【基本の仕組みを解説】

すぐ使いこなせるようになりたい人のためのGit入門 第3回

  • X ポスト
  • このエントリーをはてなブックマークに追加

Gitオブジェクト

 Gitはバージョン管理に必要なデータを主に「オブジェクト」と呼ばれる概念で表現し、.git/objectsディレクトリで管理しています。

 オブジェクトには以下の4種類があります。

  • blobオブジェクト
    • ファイルの情報が入っているオブジェクト(バックアップ)
  • treeオブジェクト
    • ディレクトリ情報が入っているオブジェクト
  • commitオブジェクト
    • コミットの情報が入っているオブジェクト
  • tagオブジェクト
    • annotated tagの情報が入っているオブジェクト

 そして、Gitは一種のKey-Value Storeとしてこれらのオブジェクトを管理しています。

 Gitでは、Keyはオブジェクトの中身に対するSHA-1ハッシュ値から作られ、Valueはオブジェクトの中身です。一連の流れは以下の通りです。

  1. それぞれのオブジェクト(Value)を作成
  2. オブジェクトの中身をSHA-1ハッシュ化した値をKeyとする
  3. オブジェクトをzlib圧縮した上で、.git/objects/以下にKey名でファイルとして保存
    • この時、検索効率性のために、Keyの先頭2文字でサブディレクトリを切っています

 以下は.git/objects/の一例です。Keyを使えばいつでもオブジェクトの中身(Value)にアクセスできるようになっています。

.git/
└── objects
    ├── 2c
    │   ├── 14dff3277ae4751173f3c26802620de960aaa1
    │   └── 4e28fab2bb84c8af445b0c81f7e7fd27817faf
    ├── bc
    │   └── 96df368963ca37edb955c1c0403471b7c90720
    ├── f9
    │   ├── 1c1a8a1e11f3147eee31b2bd9f9fb52936f87b
    │   ├── 595a6fb7692405a5c4a10e1caf93d7a5bd9c37
    │   └── ccb461cbfb1837f611b5e31a070945b8cdb867
    ├── info
    └── pack

 それでは次に、それぞれのオブジェクトについて説明していきます。こちらのリポジトリ.gitディレクトリの内容に沿って解説していくので、必要に応じて手元にcloneしてください。ここで、Gitのオブジェクトファイルは圧縮された状態で転送されるようになっています(packfile)。そのため、cloneだけでなく以下の手順で解凍する必要があるので注意してください。

# git cloneする
$ git clone git@github.com:tonouchi510/git-chapter-3.git

# 空のリポジトリを用意する
$ mkdir temp && cd temp
$ git init
# 先ほどcloneしたリポジトリのpackfileをunpackし、objectファイルを展開する
$ git unpack-objects < ../git-chapter-3/.git/objects/pack/pack-3a78e8c8bdf27096f21963d7891a4e5236841060.pack

# 元のリポジトリにオブジェクトファイルを転送
$ find .git/objects -not -name 'pack' -a -not -name 'info' -type d -mindepth 1 | xargs -I {} mv {} ../git-chapter-3/.git/objects/
$ cd ../git-chapter-3

 これで今回参照したいobjectsはすべて取り出せた状態になります。

基本構成

 オブジェクトは、最初にオブジェクトの種類やサイズなどのメタデータが記録され、そのあとにコンテンツが続きます。

  • オブジェクトの種類:blob/tree/commit/tag
  • オブジェクトのサイズ:数値(単位はbyte)
  • オブジェクトのコンテンツ:オブジェクトの種類によってさまざま

blob オブジェクト

 blobオブジェクトは、ファイルの実際のバックアップに当たるオブジェクトです。実際に、

 Key=f733735eb5e737a9c15756e0556f2155660fa4b0のオブジェクトについて見ていきます(README.mdに対応するblobオブジェクト)。

 このままだとzlib圧縮されていて分からないため、デコードして表示します(※環境によってはzlibは使えない可能性があるため、その場合は後述するcat-fileで代替してください)。

$ cat .git/objects/f7/33735eb5e737a9c15756e0556f2155660fa4b0 | zlib d
blob 17# git-tutorial-3

 基本構成で説明したように、ファイルの先頭にオブジェクトの種類とファイルサイズをつけたものが記録されていることがわかります。次に、Key=c6c63202bf8abde33a80dfc0a230ad1e9791a33bのオブジェクトについても見てみます。

$ cat .git/objects/c6/c63202bf8abde33a80dfc0a230ad1e9791a33b | zlib d
blob 16# git-chapter-3

 この2つはどちらもREADME.mdについてのblobオブジェクトになります。ここで、blobオブジェクトはファイルの差分ではなく、ある時点でのファイルの中身そのものを記録していることが分かるかと思います。ここはGitの機能を語る上で重要な要素なので、よく覚えておいてください。

 ちなみに、オブジェクトの内容を見たい場合は、cat-fileという便利なサブコマンドもあるので、そちらも使ってみてください。

# tオブションはオブジェクトの種類
$ git cat-file -t c6c63202bf8abde33a80dfc0a230ad1e9791a33b
blob
# sオブションはオブジェクトのサイズ
$ git cat-file -s c6c63202bf8abde33a80dfc0a230ad1e9791a33b
16
# pオブションはオブジェクトの中身
$ git cat-file -p c6c63202bf8abde33a80dfc0a230ad1e9791a33b
# git-chapter-3

tree オブジェクト

 次に、treeオブジェクト(Key=3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b)を見てみます。

$ git cat-file -t 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b
tree
$ git cat-file -s 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b
82
$ git cat-file -p 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b
100644 blob bc96df368963ca37edb955c1c0403471b7c90720	chapter_1.txt
100644 blob aaa8bd84e6e4837be33eee9d1ddb9d06d8d2e46c	chapter_2.txt

 treeオブジェクトはディレクトリとしての情報を保持しています。こちらはreportsディレクトリのある時点のtreeオブジェクトです。このtreeオブジェクトが作られた時点で、そのディレクトリに存在したファイルと、そのバージョンのblobハッシュ値(Key)が記録されています。このKeyから該当のblobオブジェクトにアクセスできます。

 ちなみにリポジトリのルートディレクトリに対応するtreeオブジェクトはこのようになってます。

$ git cat-file -p f9ccb461cbfb1837f611b5e31a070945b8cdb867
100644 blob c6c63202bf8abde33a80dfc0a230ad1e9791a33b	README.md
040000 tree 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b	reports
100644 blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4	test.txt

 これを見ると、先ほどのreportsディレクトリに関するtreeオブジェクトへの参照(Key=3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b)も保持していることが分かると思います。ルートディレクトリのtreeオブジェクトがあれば、そこから辿ることでリポジトリの全てのファイルにアクセスすることができます。つまり、Gitはファイルシステムになっているのです。

commit オブジェクト

 次に、commitオブジェクト(Key=bdcc6040408bacfb69727c0f2637bbcec1dbc487)を見ていきます。

$ git cat-file -p bdcc6040408bacfb69727c0f2637bbcec1dbc487
tree f9ccb461cbfb1837f611b5e31a070945b8cdb867
parent 63c833da2ed9fd43d3f000f35d2ee141ba42e411
author tonouchi510 <xxxxx@gmail.com> 1670215632 +0900
committer tonouchi510 <xxxxx@gmail.com> 1670215632 +0900

Add chapter_2.txt

 記載されている情報を整理すると、commitオブジェクトに含まれている情報は以下の通りです。

  • リポジトリのルートディレクトリのtreeオブジェクトのハッシュ値(Key)
  • 親commitのハッシュ値
  • committerとauthorのタイムスタンプ・名前・メールアドレス
  • コミットメッセージ

 これらのいずれか1つでも変わると、(SHA-1が衝突しない限り)別のcommitハッシュになります。そのためコミットは一意に定まると言えます。また、親コミットのハッシュも含んでいるため、改ざんに強いという特徴もあります。

コミットの仕組み

 ここまで説明した3つのオブジェクトを使えば、コミットの仕組みを実現できます。コミットには大きく3つの段階があります。

  1. コードを編集する
  2. 編集したコードをaddする
  3. commitする

 ステップ2で編集したコードをaddしています。この時Git内部では何が行われているのでしょうか。教科書的な回答として、「コミットに含めたいファイルをindexに登録している」という説明を見たことがあるかもしれません。

indexとは

 具体的にどういうことか見ていくために、もう一つだけ新しい概念としてindexについて説明します。indexの情報は.git/indexに記録されているので、オブジェクトと同じように見ていきます。

$ cat .git/index
DIRCc�[��c�c�[�d:�KCH��S_X�/�S��2����:����0����;	README.mdc�b�WF;c�b�Te�KK���S_X�/�S����6�c�7�U��@4q�� reports/chapter_1.txtc�b��uc�b��9KK���S_X�/�S0������{�>�۝���lreports/chapter_2.txtc�w�2�c�w�2�Ka��S_X�/�S�����L�0U��������D�test.txtTREE94 1
�̴a��7���	E�͸greports2 0
?��~�1�����)6�{�\��(l
                     ����y}ܗ8.cA

 バイナリファイルなので、そのままcatするだけだと文字化けしてしまいます。indexの中身を見るためのサブコマンドも用意されているので、そちらで確認してみます。

$ git ls-files --stage
100644 c6c63202bf8abde33a80dfc0a230ad1e9791a33b 0	README.md
100644 bc96df368963ca37edb955c1c0403471b7c90720 0	reports/chapter_1.txt
100644 aaa8bd84e6e4837be33eee9d1ddb9d06d8d2e46c 0	reports/chapter_2.txt
100644 9daeafb9864cf43055ae93beb0afd6c7d144bfa4 0	test.txt

 左から、ファイルの種類+パーミッション、blobハッシュ、コンフリクトフラグ、ファイル名が並んでいます。indexには、現在参照しているバージョンのすべてのファイルへの参照をもっています。

addの内部動作

 addした時に起こる内部挙動は、基本的に以下の2点です。

  • indexの更新
  • blobオブジェクトの生成

 例えば、適当にファイルを作成してaddすると、indexは以下のように更新されています。

$ echo hoge > temp/hoge.txt
$ git add temp/hoge.txt
$ git ls-files --stage
100644 c6c63202bf8abde33a80dfc0a230ad1e9791a33b 0	README.md
100644 bc96df368963ca37edb955c1c0403471b7c90720 0	reports/chapter_1.txt
100644 aaa8bd84e6e4837be33eee9d1ddb9d06d8d2e46c 0	reports/chapter_2.txt
100644 9c595a6fb7692405a5c4a10e1caf93d7a5bd9c37 0	temp/hoge.txt

 temp/hoge.txtがindexに追加されていることがわかります。また、blobハッシュも記録されており、該当Keyを見にいくとblobオブジェクトも生成されていることが分かると思います。すでにあるファイルを編集した場合も、新しいblobオブジェクトが生成され、そのハッシュ値に書き変わります。

 ここで注意点として、新たにディレクトリが追加されてはいますが、addの段階ではまだtreeオブジェクトの生成は行いません。

commit

 次に、commitした時に起こる内部挙動は、以下の通りです。

  • indexからtreeオブジェクトを生成
  • commitオブジェクトを生成
  • HEADを新しいcommitハッシュに書き換え

 それぞれ詳しく解説していきます。まず、treeオブジェクトの生成です。commitすると差分だけでなく、リポジトリのルートディレクトリを含むすべてのディレクトリのtreeオブジェクトを構築します。

 ただしこの時、indexに変更があった部分だけが新しいblobおよびtreeオブジェクトに書き換えられ、それ以外の参照が変わらない部分はそのまま流用します。

 新しいルートディレクトリまでのtreeオブジェクトが生成できたら、次はcommitオブジェクトを作成します。

 先ほども述べたように、commitオブジェクトはリポジトリのルートにあたるtreeオブジェクトを参照しているため、そこからリポジトリ全体をたどることができ、commit時点のリポジトリの状態を再現できるようになっています。

 また、参照しているblobオブジェクト(ファイル)も差分バックアップではなく、変更があったファイルのフルバックアップでした。つまり、コミットとは**ある時点のスナップショット**になっていると言えます。これがコミットの全貌になります。

 差分を記録しているわけではないため、あるコミット(バージョン)に遷移する際に、逐一コミットをたどって差分を適用しているわけではありません。Gitがcheckoutなどで別のコミットに移った時に瞬時にその状態を復元できるのはこれが理由です。Gitではバージョン間を遷移する際にかかる時間は、履歴の遠さには依存せず、変更されたファイル数に依存するのです。

 最後に、現在参照しているコミットを指す.git/HEADを必要に応じて書き換えます。

$ cat .git/HEAD
ref: refs/heads/main

 HEADが特定のコミットハッシュでなく、このようにブランチを参照しているので、.git/refs/heads/mainの参照しているコミットハッシュを書き換えます。以上がcommitまでの一連の処理になります。

tag オブジェクト

 最後に、他の3つのオブジェクトに比べたらあまり重要ではありませんが、tagオブジェクトについても解説しておきます。それに関連して、まずGitのrefsについて説明しておきます(こちらは重要です)。

refsについて

 refsは特定のコミットを指すポインタのようなものです。コミットハッシュのエイリアスとも言えます。具体的には以下のものがrefsに該当します。

  • tag
    • light-weight tag
    • annotated tag
  • branch
    • HEAD(すでにcommitの仕組みで登場)

 それぞれ順に見ていきます。light-weight tagを作成し、確認してみます。タグを作ると、.git/refs/tags/以下に保存されます。

$ git tag test-1
$ git log -n 1 --oneline
bdcc604 (HEAD -> main, tag: test-1) Add chapter_2.txt
$ cat .git/refs/tags/test-1
bdcc6040408bacfb69727c0f2637bbcec1dbc487

 次にbranchです。こちらは基本的にはlight-weight tagと同様です。違いとしては、保存場所が.git/refs/heads以下になっている点と、branchの場合はそのbranchでコミットを実行すると、指しているコミットハッシュが自動で書き変わっていくことです。

$ cat .git/refs/heads/main
bdcc6040408bacfb69727c0f2637bbcec1dbc487

 次にHEADです。HEADは先ほども言った通り、現在のcommitを指すrefsです。

 .git/HEADに保存されており、commitcheckoutなどで書き変わります。また、branch名でcheckoutした時はコミットハッシュではなくbranchの場所が書き込まれるようになっています。

 最後に、annotated tagについて説明します。他のrefsと違い、こちらはtagオブジェクトとして保存されます。といっても、実態としては単にコメントが付けられるタグというだけです。一応中身を見ていきます。

$ git tag -a test-annotated -m "message"
$ git log -n 1 --oneline
bdcc604 (HEAD -> main, tag: test-annotated, tag: test-1) Add chapter_2.txt
$ cat .git/refs/tags/test-annotated
50b0321cc154b323e6df2eaf31a8559f3a1e6dc8

 他のrefsと違い、コミットハッシュではなくtagオブジェクトのハッシュが記録されています。オブジェクトなので、cat-fileサブコマンドを使って中身を見てみます。

$ git cat-file -t 50b0321cc154b323e6df2eaf31a8559f3a1e6dc8
tag
$ git cat-file -p 50b0321cc154b323e6df2eaf31a8559f3a1e6dc8
object bdcc6040408bacfb69727c0f2637bbcec1dbc487
type commit
tag test-annotated
tagger tonouchi510 <xxxxx@gmail.com> 1670236054 +0900

message

 commitオブジェクトに似た構造になっています。あまり必要性は高くないかもしれませんが、メッセージなどの情報を記録したい場合にはannotated tagの使用も検討してみてください。

commitの仕組みまとめ

 ここまでで、全てのオブジェクトと、commitの仕組みを解説できました。commitの流れを再度まとめておきます。

  1. コードを編集する
  2. 編集したコードをaddする
    • addしたファイルのblobオブジェクトの生成
    • indexの更新
  3. commitする
    • リポジトリ全体のtreeオブジェクトの生成
    • commitオブジェクトを生成
    • HEADの書き換え

 ここまでは、commitまでの観点のみで、オブジェクトやrefsの使われ方を紹介しましたが、他のサブコマンドも基本的にはこれらの参照や書き換えで実現されています。

まとめ

 Gitの内部構造について、大枠の部分を紹介しました。Gitへの抵抗感を払拭できたでしょうか。

 内部構造を知った今では、(あまりないとは思いますが)Gitが壊れた時に、.git/ディレクトリ以下からオブジェクトの参照をたどったり、書き換えたりなど、自力での復旧もやろうと思えばできてしまうはずです(※実際MIXI社内ではそういった事例も過去に発生したようです)。

 Git入門シリーズとして3回に渡りGitの解説をしてきましたが、本記事で最後となります。Git理解への一助となれたら幸いです。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
すぐ使いこなせるようになりたい人のためのGit入門連載記事一覧

もっと読む

この記事の著者

登内 雅人(株式会社MIXI)(トノウチ マサト)

 Vantageスタジオ みてね事業部 Data Engineering グループ所属。 2020年に株式会社ミクシィ(現MIXI)へ新卒として入社。現在は「家族アルバム みてね」の開発チームにて、AIを活用した自動顔認識系の研究開発やMLOpsの整備などを主に担当している。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/17328 2023/02/24 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング