導入
学校の授業でオープンソースのプログラムをいじってみろというのがあったので、gitを題材に選びいじってみました。
gitを選んだ理由としては
- 普段使っている
- (出来たら)実用的である。
というのがあります。
(以降Gitに対する過多な賞賛が含まれると思われます注意してください。)
Gitについて
Gitのローカルレポジトリは作業ディレクトリ
と隠しディレクトリの.git
に分けることが出来ます。
作業ディレクトリ
は普段見えているファイルたちです。Gitの本体は.git
です。
Gitは素晴らしいのでcommit
やadd
などのフロントエンドのコマンドを用意することで.git
内の仕組みを気にしなくてもいいようになっていますが、gitプログラムを読み解く上では各コマンドが.git
内でどのような動作をしているのか知る必要があるので説明します。
作ったばかりの.gitの中身は以下のようになっています。
.git
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ ├── prepare-commit-msg.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
ここで、重要なのは以下の2つです。
-
.git/objects/
にはgitオブジェクトが -
.git/refs/
にはリファレンスが保存されます。
gitオブジェクト
gitオブジェクトはgitで扱われる全ての実質的なデータが相当します。
試しにファイルを追加して一回コミットをした状態だと以下のようになります。
├── objects
│ ├── 92
│ │ └── 681d2ebc1770f6e814278d73cb08e57804383c
│ ├── a0
│ │ └── 2c80e91d01b4495cc77406a91020a182395ade
│ ├── ec
│ │ └── 839f7db32f64d4bea7514703ca6439f7682b01
│ ├── info
│ └── pack
└── refs
├── heads
│ └── master
└── tags
おそらく参照を早くするためと思いますが、GitオブジェクトはIDの最初の2文字がディレクトリ名となり、その中にそれ以降の文字列のファイル名として保存されていいます。 例えばIDがec839f7db32f64d4bea7514703ca6439f7682b01
のコミットはec/9f7db32f64d4bea7514703ca6439f7682b01
として保存さます。このようなコンテンツに対してキーを用意するシステムを内容アドレスファイルシステムといいます。
gitオブジェクトにはtree
,blob
,'commit','tag'の4種類ありそれぞれ40文字のID(ハッシュ)がついています。ハッシュからはオブジェクトの種類は判別できません。
blob
オブジェクトはファイルに、tree
オブジェクトはディレクトリに、commit
オブジェクトはコミットに、tag
オブジェクトはタグに相当します。
各オブジェクトがどの種類でどのような内容か知るにはcat-file
コマンドを使います。IDを-t
オプションで渡すと種類が、-p
オブションで渡すと内容が分かります。
$ git cat-file -t ec839f7db32f64d4bea7514703ca6439f7682b01
commit
$ git cat-file -p ec839f7db32f64d4bea7514703ca6439f7682b01
tree 92681d2ebc1770f6e814278d73cb08e57804383c
author username <email@gmail.com> 00000000000 +0900
committer username <email@gmail.com> 00000000000 +0900
initial ci
commitコマンドについて
commit
オブジェクトはコミッターの情報とコミットメッセージ以外にその時のトップレベルのツリーと親コミット(直前のコミット)の情報を持ちます。ツリーがわかると芋づる式にその時のファイルがわかるのでこれでその時のデータが完全に再現することが出来るようになっている。
つまり、commit
コマンドはadd
コマンドでステージングエリアにあるファイルをblob
オブジェクトとして生成し、それを内包するディレクトリごとに適宜tree
オブジェクトを作り、ルートディレクトリに相当するtree
オブジェクトのIDを持ったcommit
オブジェクトを生成するコマンドだということが分かります。
packについて
本筋から外れますが、gitオブジェクトを扱う上でpackという素晴らしい機能があります。
上記で説明した方法だと、ファイルが更新するたびに新しいblob
が作られるのではと思いませんでしたか?はいその通りで、作られます。
しかしこれでは1MBのファイルを10回編集しコミットをしたら.git
は10MBにもなってしまいます。
そこでGitは素晴らしいので変更の履歴をまとめて保存したpackと呼ばれるものを作ります。
試しに、明示的にpackを作るにはgc
コマンドを使います。他にリモートサーバーにpushした時などに自動的に行われるようです。
$ tree .git/objects
.git/objects
├── 30
│ └── 1d87049de91d6b7cb369d411a5ce1b11577c75
├── 5c
│ └── 38ffc553c162fc2e6440633796e9a8696f9c0e
├── 8f
│ └── 48c91a5128c2d68e3764a47337221c420290f6
├── 92
│ └── 681d2ebc1770f6e814278d73cb08e57804383c
├── a0
│ └── 2c80e91d01b4495cc77406a91020a182395ade
├── af
│ └── faa516590fbd9301669d54219058f94910e0fc
├── e6
│ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
├── ec
│ └── 839f7db32f64d4bea7514703ca6439f7682b01
├── info
└── pack
$ git gc
$ tree .git/objects
.git/objects
├── 8f
│ └── 48c91a5128c2d68e3764a47337221c420290f6
├── info
│ └── packs
└── pack
├── pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.idx
└── pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.pack
すっきりしましたね。verify-pack
で確認することが出来ます。
$ git verify-pack -v .git/objects/pack/pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.idx
affaa516590fbd9301669d54219058f94910e0fc commit 211 146 12
ec839f7db32f64d4bea7514703ca6439f7682b01 commit 169 121 158
5c38ffc553c162fc2e6440633796e9a8696f9c0e blob 3931 1565 279
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 blob 0 9 1844
301d87049de91d6b7cb369d411a5ce1b11577c75 tree 69 77 1853
92681d2ebc1770f6e814278d73cb08e57804383c tree 37 48 1930
a02c80e91d01b4495cc77406a91020a182395ade blob 58 67 1978 1 5c38ffc553c162fc2e6440633796e9a8696f9c0e
non delta: 6 objects
chain length = 1: 1 object
.git/objects/pack/pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.pack: ok
ec839f7db32f64d4bea7514703ca6439f7682b01
などさっきまであったオブジェクトがまとめられているのが分かると思います。
リファレンス
もう一つ重要な要素はリファレンスと呼ばれるものです。
commit
オブジェクトは親の情報を持つので、IDが分かればそこから過去のコミットは芋づる式に分かります。しかしそのために最後のコミットのIDを覚えておくの現実的ではありません。そこでgitは素晴らしいのでリファレンスと呼ばれるものがあります。.git/refs
以下にはブランチごととタグごとのリファレンスがあります。それらのファイルにはcommit
オブジェクトのIDが保存されています。
$ tree .git/refs/
.git/refs
├── heads
│ └── master
└── tags
$ cat .git/refs/heads/master
734e7a7e79ef7cbbef2beb5e3a9933186e6bb4b2
$ git log --oneline
734e7a7 3rd
affaa51 2nd
ec839f7 initial ci
もうひとつHEADと呼ばれるリファレンスへのリファレンスが存在します。現在のどのブランチにチェックアウトしているかがここでわかります。
$ cat .git/HEAD
ref: refs/heads/master
また、packをするとpacked-refs
というのもできますが、これは普通のリファレンスと同様です。
まとめ
gitが上手に隠していた本質が分かってきたと思います。このようにgitは洗練された(複雑な)機能をもちつつ、それを感じさせないようにフロントエンドを整えてくれています(それでも複雑ですが)。そのためプログラムを読んでいると戸惑うことが多々有りましたが、これらの知識があると少し楽になると思います。
Gitを手探ってみた その2に続く。