お腹.ヘッタ。

関数型とかセキュリティとか勉強したい。進捗つらぽよ

Git Objectの話

これは Harekaze Advent Calendar 2017 23日目の記事です。

git objectを皆さんはご存知ですか。そもそもとしてgitをご存知であるか問題があるわけですが、まぁ親の顔よりgitのdiffをみた人もいるんと思うので知ってることにして話を進めます。

で、git objectって何よって話をすると皆さんがgit initをしてホゲホゲすると生まれる.git/objectsを指しており、gitのコアの部分のデータストアのことを指します。

さて、このオブジェクトは大きく分けて三つの構成がされています。

  • blob
    • ファイルデータを表現する
  • tree
    • フォルダ構造を表現する
  • commit
    • treeへの参照、コミットしたユーザーやタイムスタンプなどのスナップショットの表現

それをラップしているのが僕らが普段扱うコマンドたち。

そのコマンドも大きく分けると二つで、

  • 磁器コマンド(Porcelain command)
    • 僕らが普段使うようなbranchとかユーザーフレンドリーなコマンドたち
  • 配管コマンド(plumbing command)
    • 普段のgitフロー開発では普通全く使う必要がないgit objectを直接触るようなhash-objectなどのコマンド

後者の方はあまり聞きませんが、今回はこれも用いて行なっていきます。

何が何だかわからんって人もいるかもしれませんが、git使ったことあるならばなんとなく分かるんじゃないかなと思うのでつらつらと書いて行きます。

ではサックっと見ていきましょう

.git/objects

gitのデータは前述の通り、gitオブジェクトという形で.git/objectsの中に格納される。 実際にコピってやってみよう。(以下よ#〜は結果)

git init
echo "hello" > piyo.txt
git add piyo.txt
git commit -m "init commit"

find .git/objects -type f
#.git/objects/73/0c1bf1546ce745073e50572b64d951427b868c
#.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
#.git/objects/2e/1206f177f168e9654bd0f803c3b79100308516

中身を見てみると.git/objectsフォルダの中にファイルが3つできているのがわかると思う。 この不規則に見えるgit objectの名前は40文字のhashがつけられていて、先頭の2文字がサブフォルダ名、残りの38文字をファイル名として1つのオブジェクトにつき一つのファイルに保存される。ちなみにこのobjectの名前はsha1で生成される。

さて、これではそれぞれどんなオブジェクトなのかわからない。そこでgit cat-fileというコマンドを用いる

git cat-file -t 2e12
#tree
git cat-file -t 730c
#commit
git cat-file -t ce01
#blob

よって以上のobjectはtree,commit,blobと言うことがわかった

blob object

-pオプションでオブジェクトの中身を表示ができる。

git cat-file -p ce01
#hello
cat piyo.txt
#hello

blob objectはファイルデータのスナップショットで、ファイル名などのメタ情報は含まれてない。

tree object

git cat-file -p 2e12
#100644 blob ce013625030ba8dba906f756967f9e9ca394464a    piyo.txt
ls 
# piyo.txt

tree objectはfile構造を指していて、file属性やblob objectへの参照、filenameを記録している。 なお、サブフォルダの場合はtree objectへの参照になる。

commit object

git cat-file -p 730c
#tree 2e1206f177f168e9654bd0f803c3b79100308516
#author take <****@gmail.com> 1513838255 +0900
#committer take <****@gmail.com> 1513838255 +0900

#init commit

//流石にメールはぼかしましたw

commit objectには、トップレベルのtreeへの参照、commitしたユーザー情報、タイムスタンプ、commitメッセージが含まれる。

ちなみに最初のcommitなので親commitがないが、2回目以降のコミットでは親commitへの参照も含まれる。 ほんまか?って思うかもしれないので試しに見てみる。

ではまずは新しいファイルで新たにオブジェクトが生成されるか確認する。

echo "world" > fuga.txt
git add fuga.txt
git commit -m "second commit"
ls
#fuga.txt piyo.txt

find .git/objects -type f
#.git/objects/da/22367adec08f45e1f4c368361f7582159c9012
#.git/objects/b4/7b491cb488426c0ea4c0b04bc0ac9609fb6cc0
#.git/objects/73/0c1bf1546ce745073e50572b64d951427b868c
#.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
#.git/objects/83/c7a922210527a3543ce0cfa9f1f63d9dce921b
#.git/objects/2e/1206f177f168e9654bd0f803c3b79100308516

確かに、以下のobjectが増えていることがわかる。

.git/objects/da/22367adec08f45e1f4c368361f7582159c9012
.git/objects/b4/7b491cb488426c0ea4c0b04bc0ac9609fb6cc0
.git/objects/83/c7a922210527a3543ce0cfa9f1f63d9dce921b

ではcommit objectをgit cat-fileをする。

git cat-file -p 83c7
#tree da22367adec08f45e1f4c368361f7582159c9012
#parent 730c1bf1546ce745073e50572b64d951427b868c
#author take <hayatake396@gmail.com> 1513954425 +0900
#committer take <hayatake396@gmail.com> 1513954425 +0900

#second commit

この場合、確かにparentと言う親commitが出来ていることがわかる。

折角なので、他に増えたobjectたちも確認してみよう。

git cat-file -p da22
#100644 blob b47b491cb488426c0ea4c0b04bc0ac9609fb6cc0    fuga.txt
#100644 blob ce013625030ba8dba906f756967f9e9ca394464a    piyo.txt

treeなのでファイル自体がやはり増えている。

git cat-file -p b47b
#world

blobなのでファイルの中身が確かに出てきている。素晴らしい。とりあえずgitobjectが頑張ってることはわかった。

ここで思うだが、git objectを配管コマンドから直接いじって見たい気持ちになったりするわけである。

と言うわけでこれってもしかして全くファイル作らずにgit commitができるのではと言うことが容易に思えるので検証していく

配管コマンドを用いたファイルを一度も作らずに行うcommit

初めはまずはgit initをして確かに初めにはないもないことを確認する。

mkdir fugafuga
cd fugafuga
git init
#Initialized empty Git repository in /Users/Home/Desktop/fugafuga/.git/
find .git/objects
#.git/objects/
#.git/objects/pack
#.git/objects/info

さて、使うコマンドはgit hash-objectと言う物を使う。これはobjectIDを計算してファイルからblobを作成するコマンドである。 オプションがいくつか設定する必要があって、今回使うのは-w,--stdinと言う実際にobjectデータベースに書き込むオプションと標準入力からobjectを読み込むと言うものを指定する。 ちなみに-wをつけないとただ単にIDを吐くだけになる。blobは出来ない。

echo 'create data' | git hash-object -w --stdin
#070f3bd01632c945394b3aac7187a9d91ca4816a
ls
#(何もない)
git cat-file -p 070f
#create data

これで確かにblob objectだけができた。

次にこれを格納するtree objectを作る。

さて、下準備をする必要がある。 まず、使うコマンドはgit update-indexと言うコマンドを使う。これはworking treeのファイル内容をindexに登録するコマンドである。

これにもオプションをつける必要があり、Stageingしてるファイルに対してこれは実行されるので(と言うかファイルがないのでStageingする場所もない)--addオプションをつけて追加すること必要ある。

また、このindexに追加するものはファイルではなくてobjectデータベースにあるので--cacheinfoオプションをつけて指定された情報をindexに直接挿入してやることになります。このオプションは引数を3つ取ることになって--cacheinfo <mode> <object> <path>と記述する必要がある。

ちなみにだがこのmodeとはchmodなどと同じパーミッションの数値の値を指しています。今回の100644chmod 644と同じですので通常の一般的な権限のファイルであることを意味します。逆に実行可能ファイルとかならば100755になります。

git update-index --add --cacheinfo 100644 8b137891791fe96927ad78e64b0aad7bded08bdc test.txt

さて、これ無事indexにぶち込んだのでStageingエリアをtree objectに書き出すことができるようなった。 これをどう書き出すかと言うとgit write-treeコマンドで行う。これはまだtreeがないときに自動でindexからtree objectを作る。

git write-tree
#c47b19c8b7a3b7724138f73e4cd53efa0f1e9595
git cat-file -p c47b
#100644 blob 070f3bd01632c945394b3aac7187a9d91ca4816a    test.txt

これによって作ることができた。

さて、次はcommit objectです。ここまでやれば簡単で、

echo 'first commit' | git commit-tree c47b
#5cb61261e3c4d2d15d09583c107577564cc6335e
git cat-file -p 5cb6
#tree c47b19c8b7a3b7724138f73e4cd53efa0f1e9595
#author take <*****@gmail.com> 1514031034 +0900
#committer take <****@gmail.com> 1514031034 +0900

#first commit

確かにcommitが出来ました。やったぜ!

終わりに

私は今回NICT主催のSecHack365というのに参加しているんですが、計画性のなさが露呈して、開催期間中に記事を書く羽目になりました。ツライ....

次は@Knom10さんです。お楽しみに!

参考