要約
git内部のデータ格納に関するサブコマンド、git cat-file
と git hash-object
を自分でPerlで実装しgit内のデータの保存方法について知る
目次
- イントロダクション
- git内部のデータの確認
- perl実装の紹介
実装
イントロダクション
自己紹介
駅メモにて主にバックエンドを担当しているid:toricorです。 仕事ではPerl実装のサーバ周りを触ることが多いです。
仕事以外では、以前Perlで簡単なJVMを書きました 。
毎日使うソフトウェアといえば
gitは欠かせないですよね。 でも毎日の仕事で生成されるあの膨大なファイルやコミットを、内部でどのように記録し管理しているか気になりませんか。私は気になりました。
本家の説明によると
Git は内容アドレスファイルシステムです。 素晴らしい。 …で、それはどういう意味なのでしょう? それは、Gitのコアの部分はシンプルなキー・バリュー型データストアである、という意味です。
Git - Gitオブジェクト
少しわかりにくいですね
Gitを自分で書いたら理解できるかも!?
"Write yourself a Git!" https://wyag.thb.lt/
gitの自力実装の方法を紹介してくれています。 数百行程度のPython実装でgitのいろいろなサブコマンドが実装できます。
以前Python実装のままチーム内で紹介したのですが、弊社はPerlをよく使う会社なので今回Perlで書き直しました(ただし一部のみ)。
以降はデータの格納という点に注目してgitを見ていきたいと思います。
内部のデータの確認
.git
のどこにデータが入るのだろうか
gitを使う場合に必要なデータ(ブランチ、コミット、など)は.git
に全て格納されています。
適当なGitプロジェクトをつくり、コミットをした場合に .git
の中身がどのように変わっていくかをまずは確認します。
git init
直後
% tree .git .git ├── HEAD ├── config ├── description ├── hooks │ ├── applypatch-msg.sample │ (省略) │ └── update.sample ├── info │ └── exclude ├── objects │ ├── info │ └── pack └── refs ├── heads └── tags 8 directories, 16 files
ファイル作成 & git add
% echo 'print "Hello Git!\n";' > hello.pl % git add . % tree .git .git ├── HEAD ├── config ├── description ├── hooks │ ├── applypatch-msg.sample │ (省略) │ └── update.sample ├── index ├── info │ └── exclude ├── objects │ ├── 36 │ │ └── 9b01f4c6a00c39f0362e3f6c9648c2dc178b47 <-- ファイルが増えたぞ?? │ ├── info │ └── pack └── refs ├── heads └── tags 9 directories, 18 files
git add .
を実行後、.git/objects/
以下に1つファイルが増えました。
git commit
% git commit -m 'add an example file' [master (root-commit) dc8cc4a] add an example file 1 file changed, 2 insertions(+) create mode 100644 hello.pl % tree .git .git ├── COMMIT_EDITMSG ├── HEAD ├── config ├── description ├── hooks │ ├── applypatch-msg.sample │ (省略) │ └── update.sample ├── index ├── info │ └── exclude ├── logs │ ├── HEAD │ └── refs │ └── heads │ └── master ├── objects │ ├── 36 │ │ └── 9b01f4c6a00c39f0362e3f6c9648c2dc178b47 │ ├── dc │ │ └── 8cc4a05ebbb7771a46e61bfe6dcfbefb905eaa <-- これと │ ├── dd │ │ └── b362c0207a67dc4d63684794a50fcdaf69a155 <-- このファイルがcommitしたら増えた! │ ├── info │ └── pack └── refs ├── heads │ └── master └── tags 14 directories, 24 files
コミット操作により、更に2つのファイルが生成されました。
これらはGitオブジェクト
と呼ばれます。
生成されたファイル(Gitオブジェクト)の中身を見てみる
% less .git/objects/36/9b01f4c6a00c39f0362e3f6c9648c2dc178b47 ".git/objects/36/9b01f4c6a00c39f0362e3f6c9648c2dc178b47" may be a binary file. See it anyway?
バイナリファイルでした
% xxd .git/objects/36/9b01f4c6a00c39f0362e3f6c9648c2dc178b47 00000000: 7801 4bca c94f 5230 3264 2828 cacc 2b51 x.K..OR02d((..+Q 00000010: 50f2 48cd c9c9 5770 cf2c 51e4 52b2 e602 P.H...Wp.,Q.R... 00000020: 0087 f508 5c
ちょっとこれでは内容がよくわからないですね。
Gitオブジェクトのフォーマットについて
実はコミットや特定の時点でのファイルを表すGitオブジェクトは、ハッシュ値に基づいたファイルとなります。 それぞれオブジェクト作成時に40文字からなるSHA-1ハッシュが計算され最初の2文字をディレクトリ、残り38文字がファイル名のファイルとなります。
例:
.git/objects/36/9b01f4c6a00c39f0362e3f6c9648c2dc178b47
ハッシュ値が369b01f4c6a00c39f0362e3f6c9648c2dc178b47
のとき36
というディレクトリに9b01f4c6a00c39f0362e3f6c9648c2dc178b47
というファイルが作られます。
Q. Gitオブジェクトの中身はどういう変換を受けている? (普通のファイル: blob(後述)の場合)
- 普通のファイルの内容にヘッダーをつけて、zlibを使い圧縮されています
ヘッダー: オブジェクトタイプ + 空白 + データの長さ + ヌルバイト
Gitオブジェクトを読み取るコマンド: git cat-file
Gitオブジェクトを扱うためのgitのサブコマンドを紹介しておきます。 読み取り専用のサブコマンドがあります。
git-cat-file - Provide content or type and size information for repository objects
では実際に今回生成された3つのGitオブジェクトの内容を見てみます。
-t
オプションでGitオブジェクトのタイプを表示します。
% git cat-file -t 369b01f4c6a00c39f0362e3f6c9648c2dc178b47 blob % git cat-file -t dc8cc4a05ebbb7771a46e61bfe6dcfbefb905eaa commit % git cat-file -t ddb362c0207a67dc4d63684794a50fcdaf69a155 tree
-p
オプションでもう少し詳しく見てみましょう
# blob % git cat-file -p 369b01f4c6a00c39f0362e3f6c9648c2dc178b47 print "Hello Git! "; # commit % git cat-file -p dc8cc4a05ebbb7771a46e61bfe6dcfbefb905eaa tree ddb362c0207a67dc4d63684794a50fcdaf69a155 author toricor <toriyabe@mfac.jp> 1597738778 +0900 committer toricor <toriyabe@mfac.jp> 1597738778 +0900 add an example file # tree % git cat-file -p ddb362c0207a67dc4d63684794a50fcdaf69a155 100644 blob 369b01f4c6a00c39f0362e3f6c9648c2dc178b47 hello.pl
このようにGitオブジェクトは上に示したblob, commit, treeタイプと他にtagタイプの4つから成り立っています。
Gitオブジェクトを書き込むコマンド: git hash-object
git-hash-object - Compute object ID and optionally creates a blob from a file
git cat-file
と対になるコマンドになります。Gitオブジェクトにしたいファイルのハッシュ値を計算して(-w
付きなら).git/objects/
以下にGitオブジェクトファイルを作成します。
Gitを実装しよう
git cat-file
とgit hash-object
を理解できればgit内部のデータの格納について基本は理解したといえそうです。
参考にしたサイトはPython実装ですが、 Perlでこれらのコマンドを実装してみました。
実装のポイント
いくつかポイントを絞って実装を紹介します。
例1: SHA-1のハッシュ値を求める
Digest::SHA1モジュールのsha1_hex
が使えます。
use Digest::SHA1 qw/sha1_hex/; my $data = ...; # 適当なデータ my $len = length($data); # データの長さ # build a header my $fmt = 'blob'; # commit/blob/tree/tag my $result = "$fmt $len\x00$data"; # e.g. 369b01f4c6a00c39f0362e3f6c9648c2dc178b47 my $digest = sha1_hex($result);
例2: Gitオブジェクトを解凍してデータを確認する
- バイナリファイルを読むのでopenの第2引数に"<:raw"を指定します
binmode($fh)
でもいいです
- 圧縮・解凍にはCompress::Zlibモジュールが使えます
# simple_uncompress.pl # 適当なPerlプロジェクトを作成し簡単なスクリプトを用意しました use Compress::Zlib qw//; sub dump_commit { my ($path) = @_; open(my $fh, "<:raw", $path) or die $!; my $bufs; while (read $fh, my $buf, 1024) { $bufs .= $buf; } close $fh; my $uncompressed = Compress::Zlib::uncompress($bufs); print $uncompressed; } # git logした結果から適当なものを選んでcommitタイプのGitオブジェクトを見てみる my $path = '.git/objects/8f/083b8b230e833e1ea7e679fac265ca6a422c02'; dump_commit($path);
% carton exec -- perl simple_uncompress.pl commit 220tree 6e7c0d7b065c5bd3708c8aeb7e85c81ccffbb01d parent 97a587d82878afd2d81d56b30d3dd9a910316b8a author toricor <toriyabe@mfac.jp> 1596040311 +0900 committer toricor <toriyabe@mfac.jp> 1596040311 +0900 refactor HashObject
ヘッダ+コミットからなる表示が見えました!
Gitを動かそう
では先の実装ポイントを踏まえて実装したperlのスクリプトを実行してみましょう
ハッシュ値 => 内容表示 ( cat-file
)
コミットを調べます
% carton exec -- perl main.pl cat-file -t 8f083b8b230e833e1ea7e679fac265ca6a422c02 commit % carton exec -- perl main.pl cat-file -p 8f083b8b230e833e1ea7e679fac265ca6a422c02 tree 6e7c0d7b065c5bd3708c8aeb7e85c81ccffbb01d parent 97a587d82878afd2d81d56b30d3dd9a910316b8a author toricor <toriyabe@mfac.jp> 1596040311 +0900 committer toricor <toriyabe@mfac.jp> 1596040311 +0900 refactor HashObject
コミットの中身が表示できました
ファイル => Gitオブジェクト
適当なファイルのハッシュ値を求めます
% echo 'print 123;' > 123.pl % carton exec -- perl main.pl hash-object -t blob 123.pl 4a8a8bc47b318a7ca83c5f739440e969fef59325
ハッシュ値を求めるだけではなくGitオブジェクトも作ってみます(ファイルはblob
タイプです)
% carton exec -- perl main.pl hash-object -w 123.pl 4a8a8bc47b318a7ca83c5f739440e969fef59325 % ls .git/objects/4a/8a8bc47b318a7ca83c5f739440e969fef59325 8a8bc47b318a7ca83c5f739440e969fef59325
Gitオブジェクトが.git/objects以下に作られました
% git cat-file -t 4a8a8bc47b318a7ca83c5f739440e969fef59325 blob % git cat-file -p 4a8a8bc47b318a7ca83c5f739440e969fef59325 print 123;
本物のgit cat-file
コマンドからもきちんとGitオブジェクトの内容が認識されているようですね!
まとめ
意外とgitは自分で作れます!
参考文献
- Write yourself a Git!
- Git 第10章 Gitの内側