2015年3月29日日曜日

Gitの最初の姿

この4月で、Gitが誕生してからちょうど10年になるようだ。
10年前、つまり一番最初のGitはどのようなプログラムだったんだろう?

当然だけど Git プロジェクトのリポジトリには10年前の最初の姿が e83c516 というコミットとして記録されている。
% git log --max-parents=0
commit e83c5163316f89bfbde7d9ab23ca2e25604af290
Author: Linus Torvalds <torvalds ppc970.osdl.org>
Date:   Thu Apr 7 15:13:13 2005 -0700

    Initial revision of "git", the information manager from hell
# --max-parents=0 は親のないコミット、つまりルートコミットを探す方法だ。
# もちろんこれが Linus が書いた本当に最初のツリーとは限らない。 Gitリポジトリから辿れる最初、という意味だ。
今回はこの当時の姿について少し見てみようと思う。


まずは適当なところから clone して e83c516 をチェックアウトしてみよう。
% git clone https://github.com/git/git.git
(略)
% cd git/
% git checkout e83c516
Note: checking out 'e83c516'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at e83c516... Initial revision of "git", the information manager from hell
% git ls-files
Makefile
README
cache.h
cat-file.c
commit-tree.c
init-db.c
read-cache.c
read-tree.c
show-diff.c
update-cache.c
write-tree.c
% wc -l *.[ch]
   93 cache.h
   23 cat-file.c
  172 commit-tree.c
   51 init-db.c
  259 read-cache.c
   43 read-tree.c
   81 show-diff.c
  248 update-cache.c
   66 write-tree.c
 1036 total
約1KLoC, 10ファイル程度からなるプログラムだったようだ。

早速使ってみよう。 まずビルドする必要があるんだけど、環境によってはリンクエラーになるので、必要に応じて以下のように Makefile に編集を加える。 ウォーニングが気になる場合は同じく下のように cache.h で string.h をインクルードするよう編集しよう。
% git diff
diff --git a/Makefile b/Makefile
index a6bba79..aeb6d4b 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ all: $(PROG)
 install: $(PROG)
        install $(PROG) $(HOME)/bin/

-LIBS= -lssl
+LIBS= -lssl -lcrypto -lz

 init-db: init-db.o

diff --git a/cache.h b/cache.h
index 98a32a9..ff15410 100644
--- a/cache.h
+++ b/cache.h
@@ -9,6 +9,7 @@
 #include <stdarg.h>
 #include <errno.h>
 #include <sys/mman.h>
+#include <string.h>

 #include <openssl/sha.h>
 #include <zlib.h>
% make
gcc -g   -c -o update-cache.o update-cache.c
gcc -g   -c -o read-cache.o read-cache.c
gcc -g -o update-cache update-cache.o read-cache.o -lssl -lcrypto -lz
gcc -g   -c -o show-diff.o show-diff.c
gcc -g -o show-diff show-diff.o read-cache.o -lssl -lcrypto -lz
gcc -g   -c -o init-db.o init-db.c
gcc   init-db.o   -o init-db
gcc -g   -c -o write-tree.o write-tree.c
gcc -g -o write-tree write-tree.o read-cache.o -lssl -lcrypto -lz
gcc -g   -c -o read-tree.o read-tree.c
gcc -g -o read-tree read-tree.o read-cache.o -lssl -lcrypto -lz
gcc -g   -c -o commit-tree.o commit-tree.c
gcc -g -o commit-tree commit-tree.o read-cache.o -lssl -lcrypto -lz
gcc -g   -c -o cat-file.o cat-file.c
gcc -g -o cat-file cat-file.o read-cache.o -lssl -lcrypto -lz
ビルドできた。

次はインストールだ。 このMakefileでは $HOME/bin 配下に決め打ちで実行ファイルがインストールされる。問題があるなら適宜 Makefile を書き換えて make install したり、make install はせずにビルドしたディレクトリにパスを通してもいい。ここではそのまま make isntall したものとして話を続ける。 ついでにインストール先に PATH も通しておこう。
% make install
install update-cache show-diff init-db write-tree read-tree commit-tree cat-file /home/kt/bin/
% PATH=$HOME/bin:$PATH
さて、これでインストールは完了だ。上に表示されているとおり、インストールされるコマンドは7個。それぞれ以下のような働きをする。

init-db:                オブジェクトデータベースとして .dircache/ ディレクトリ (今で言う .git 相当) を初期化する。
update-cache:  ワーキングツリーの内容でインデックスを更新する
write-tree:          インデックスの内容でオブジェクトデータベースにツリーオブジェクトを書き出す
commit-tree:    ツリーオブジェクト (と親コミットオブジェクト) から新しいコミットオブジェクトを作る
cat-file:                オブジェクトデータベースからコンテンツを取り出す
read-tree:           ツリーオブジェクトの内容を人間に読める形に整形して表示する
show-diff:          インデックスとワーキングツリーの違いを調べる

では使ってみよう。まず適当なディレクトリにコンテンツになるファイルを用意する。
% mkdir hello
% cd hello
% cat > hello.c
#include <stdio.h>

int main(void) {
        printf("Hello, world!\n");
        return 0;
}
% make hello
cc     hello.c   -o hello
% ./hello
Hello, world!
% rm -f hello
コンテンツが用意できたらデータベースの初期化から最初のコミット作るところまでやってみよう
% init-db                        ------------------------------------------------- [1]
defaulting to private storage area
% ls -a                          ------------------------------------------------- [2]
./  ../  .dircache/  hello.c
% update-cache hello.c           ------------------------------------------------- [3]
% write-tree                     ------------------------------------------------- [4]
f5dd081f798bb1580d743d19fd3f271b7e49ce29
% echo 'Initial commit' | commit-tree f5dd081f798bb1580d743d19fd3f271b7e49ce29 --- [5]
Committing initial tree f5dd081f798bb1580d743d19fd3f271b7e49ce29
ef6b77786d8eb30548f79294ea1cc0cf276b82b8
init-db コマンドでオブジェクトデータベースの初期化する[1]. オブジェクトデータベースは .dircache という名前のディレクトリだ[2].  update-cache によって指定したファイルがオブジェクトデーターベースに登録され、インデックスが更新される[3].  write-tree を実行することで、現在のインデックスの内容から作成したツリーオブジェクトがオブジェクトデータベースに書きだされ、そのオブジェクト名が表示されるので[4], それ引数に指定して commit-tree を実行することでコミットオブジェクトが作成される。 コミットメッセージは標準入力から読み込む仕様だ[5].  このへんの流れは、 オンラインマニュアル gitcore-tutorial(7) を見ると現在でも git の UI を一皮むけばそんなに変わってないことがわかる。

じゃあ今作ったコミットの中身を調べてみよう。 作成したオブジェクトを取り出すには cat-file (blob, commit用) または read-tree (tree用) を使う。
% cat-file ef6b77786d8eb30548f79294ea1cc0cf276b82b8   --------------------------- [6]
temp_git_file_hpYNfV: commit
% cat temp_git_file_hpYNfV                            --------------------------- [7]
tree f5dd081f798bb1580d743d19fd3f271b7e49ce29
author Katsuyuki Tateishi <kt kiri.localdomain> Sat Mar 28 10:18:50 2015
committer Katsuyuki Tateishi <kt kiri.localdomain> Sat Mar 28 10:18:50 2015

Initial commit
% read-tree f5dd081f798bb1580d743d19fd3f271b7e49ce29  --------------------------- [8]
100664 hello.c (38fa160ff0e8b048120c9dae168233ed427cabf9)
% cat-file 38fa160ff0e8b048120c9dae168233ed427cabf9   --------------------------- [9]
temp_git_file_AyoTHF: blob
% cat temp_git_file_AyoTHF                            --------------------------- [10]
#include <stdio>

int main(void) {
        printf("Hello, world!\n");
        return 0;
}
% diff temp_git_file_AyoTHF hello.c                   --------------------------- [11]
%
コミットしたときに表示されたSHA1値 (ef6b777...) を引数に cat-file を実行すると、 temp_git_file_ というプリフィックスを持った一時ファイルが生成される[6].  この一時ファイルの中身をみると、 コミット ef6b777 の情報がダンプされている[7]. それを見ると、 コミット ef6b777 を構成するツリーオブジェクトの名前が f5dd08... だったことがわかるので、その名前を read-tree に与えて実行すると、 そのツリーの内容が表示される[8].  ツリーに存在する hello.c のオブジェクトは 38fa16... という名前だったことはツリーの内容からわかるので、 それを cat-file してやれば再び一時ファイルが生成され[9], 中身を見れば hello.c ぽい内容であることがわかる[10].  diff をとってやれば hello.c と同一の内容のファイルであることが確認できる[11].

それじゃあここでコンテンツを編集して、もう一度コミットしてみよう。
% vi hello.c
% show-diff                                           --------------------------- [12]
hello.c:  38fa160ff0e8b048120c9dae168233ed427cabf9
--- -   2015-03-28 14:56:20.208784174 +0900
+++ hello.c     2015-03-28 14:55:14.055732217 +0900
@@ -1,6 +1,6 @@
 #include <stdio.h>

 int main(void) {
-       printf("Hello, world!\n");
+       printf("Hello, Git!\n");
        return 0;
 }
% update-cache hello.c                                --------------------------- [13]
% show-diff                                           --------------------------- [14]
hello.c: ok
% write-tree                                          --------------------------- [15]
3d3ff4984c92985b40016e9023317b791fa2f58c
% echo 'Change the message' | commit-tree 3d3ff4984c92985b40016e9023317b791fa2f58c -p ef6b77786d8eb30548f79294ea1cc0cf276b82b8
d5089a7537a972a67a02299d26b6b9d2467a65d4
% cat-file d5089a7537a972a67a02299d26b6b9d2467a65d4
temp_git_file_zwnpaG: commit
% cat temp_git_file_zwnpaG
tree 3d3ff4984c92985b40016e9023317b791fa2f58c
parent ef6b77786d8eb30548f79294ea1cc0cf276b82b8
author Katsuyuki Tateishi <kt kiri.localdomain> Sat Mar 28 14:57:29 2015
committer Katsuyuki Tateishi <kt kiri.localdomain> Sat Mar 28 14:57:29 2015

Change the message
% read-tree 3d3ff4984c92985b40016e9023317b791fa2f58c
100664 hello.c (b916f7b8ae2f3f134144f5436b2d415214ee7739)
% cat-file b916f7b8ae2f3f134144f5436b2d415214ee7739
temp_git_file_EEbNDB: blob
% cat temp_git_file_EEbNDB
#include <stdio.h>

int main(void) {
        printf("Hello, Git!\n");
        return 0;
}
追跡中のファイルを編集したあと、ローカルチェンジは show-diff で確認できる[12].  update-cache によってワーキングツリーの内容でインデックスを更新[13]した直後、つまりインデックスとワーキングツリーに差分がない状態では show-diff すると単に <filename>: ok と表示されるのがわかる[14].  コミットまで、そしてコミットした内容を確認するの道のり[15]はさっきとほとんど同じだ。 1点だけ異なるのは commit-tree に -p で親コミットの SHA1 を与えてやる必要がある、ということだけ。

最初期の Git でできることは以上だ。 当然だけど現在の Git (最新ver.は v2.3.4)から比べると、 できることは本当に少ない。 でもblob, commit, tree などのオブジェクトや、 インデックスの存在など、 Git のコンセプトのコアの部分は現在まで全く変わっていないことがわかる。手作業を厭わなければマージコミットを作ることだって可能だし、 その気になればバージョン管理のための UI をかぶせることも (洗練されていなくてもよければ) そんなに難しくないはずだ。 もちろんこの感覚は現在の Git を知っている後付の知識に基づいたものであって、 何も知らない状態で「コレが新しいバージョン管理のためのプログラムだよ」といって見せられたら、多くの人は困惑してしまうんじゃないかなと思う。

Linus が作った最初の Git はこんな形だったけど、 この直後から現メンテナ(開発責任者)の Hamano 氏も参加し、二人を中心に改良が続けられ、3ヶ月後の v0.99 が出たころにはだいぶ使いやすい UI になっている(今と比べれば全然だけど)。

現在になっても未だに UI については賛否両論ある Git だけど、コアのコンセプトさえ一旦理解してしまえばそんなに困ることはない。現に今回10年前の Git に戻って調べた僕自信、各コマンドの名前とmain関数だけ見れば使い方は大体想像がついた。きっと未来の Git に対しても同じことが言えると思う。 とはいえ Git はそのコミュニティによって活発に改良されていて、UI もゆっくりとではあるけれど確実に進化を続けている。この先10年でどのように変わるのか楽しみだ。

0 件のコメント:

コメントを投稿