要件
以下のようなことができればよいものとします。
パスを与えられたとき、
- そのパスが示すファイルの内容 (open, read)
- そのパスが示すディレクトリの内容 (opendir, readdir)
- そのパスが示すファイルまたはディレクトリの情報 (stat)
- パーミッション
- 最終更新時刻
- ファイルサイズ
を得られる。
Git のファイルツリー構造を読む
Git に登場し、われわれが対象とすることができる概念は「オブジェクト」という名前のもとに抽象化されています。通常、直接操作することがあるのはコミットとタグだけですが、残りのツリー(tree)とブロブ(blob)をここでは取り扱うことになります。
ここでは詳しく解説できませんが、簡単に言えばツリーがディレクトリに対応し、ブロブがファイル(の内容)に対応します。コミットオブジェクトは時刻やコミットメッセージの他に、ひとつのツリーオブジェクトへの参照を持っています。適当なリポジトリで git cat-file
で様子をうかがってみます。
% git cat-file -p HEAD tree cfe3cb534d9e626f3cff23583e6cb666b0ebb997 # コミット "HEAD" が指すツリーID parent 0f1a7c509da2ed307f3a9fcb9ea0907fb8fe3601 author motemen <motemen@gmail.com> 1418313657 +0900 committer motemen <motemen@gmail.com> 1418313657 +0900 init
HEAD
が指すツリーが見つかったので、これをさらにたどってみます。
% git cat-file -p cfe3cb534d9e626f3cff23583e6cb666b0ebb997 100644 blob daf913b1b347aae6de6f48d599bc89ef8c8693d6 .gitignore 100644 blob ba5d35222be3983257723ddcd0bf8fcf3ee90cdf LICENSE 100644 blob 0613648f84ecb1ab9ca6182b46f74539f227ec72 README.md 040000 tree e90529ca501f5001c0706065aebe735bede754be git
ルートディレクトリの中身らしきものが見えてきました。LICENSE と git に対応するオブジェクトをそれぞれ覗いてみると:
# LICENSE % git cat-file -p ba5d35222be3983257723ddcd0bf8fcf3ee90cdf The MIT License (MIT) … # git/ % git cat-file -p e90529ca501f5001c0706065aebe735bede754be 100644 blob a9ecb54af0fcb66c64cb9ca814d22887c6dfe115 git.go 100644 blob 1516b582b5010469d02f0d7995d426489ce0cc1d git_test.go
blob がファイルの内容であり、tree がディレクトリに対応することがわかると思います。古いコミットに対しておなじ操作を行ってみると別のツリーオブジェクトをたどることになり、古いファイルの内容にアクセスできます。ワークスペースの内容が変更されコミットされるたびに、新しいツリーオブジェクトが生成され、新しいコミットはそのツリーオブジェクトを指します。
これで、以下のようにすれば特定リビジョン(コミット)におけるリポジトリの内容を取得できそうだと想像できます:
- 当該のコミットが指すツリーを得る。
- ツリーをたどり、深い階層のブロブやツリーを得る。
Git を用いた具体的な実装
git
コマンドを通じて、ツリーとブロブによる擬似ファイルシステムを実装することにします。その前にこのオブジェクトの指定方法を知っておくと便利です: <rev>:<path>
。git cat-file -p HEAD:README.md
のようにして使います。特定のリビジョン rev におけるファイルまたはディレクトリ path に対応するブロブ、ツリーを指す記法といったところです。リポジトリルートを指したいときは HEAD:
のように指定します。./
などとすると現在のディレクトリからの相対パスになることに注意。
1. ファイルの内容を取得する
ファイルに対応するブロブの SHA1 がわかっていれば、以下のコマンドで内容を取得できます。
% git cat-file blob ba5d35222be3983257723ddcd0bf8fcf3ee90cdf
先に登場した git cat-file -p
でもいいですが、上のコマンドであればブロブでないオブジェクトを表示しようとすると失敗してくれます。
2. ディレクトリの内容を取得する および 3. ファイルやディレクトリの情報を取得する
git ls-tree
を使います。上で使ったように git cat-file
でも同じような出力は得られますが、ツリー的オブジェクトでないものを引数に与えたときエラーになってくれるので、ls-tree のほうがよいでしょう。
ls-tree はそのままだと現在のディレクトリを使用するので、--full-tree
で無視させます。また、-l
オプションでブロブのサイズも取得しておきます。プログラムから使用するときは -z
オプションで NUL 区切りにしておくのがよいです。
% git ls-tree --full-tree -l HEAD:git 100644 blob a9ecb54af0fcb66c64cb9ca814d22887c6dfe115 6385 git.go 100644 blob 1516b582b5010469d02f0d7995d426489ce0cc1d 867 git_test.go
モードの読み方
ls-tree の出力の最初の列がオブジェクトのモードを表しています。これに関して直接的なドキュメントはないみたいだけど、git-fast-importなんかを見るとなんとなく分かると思います。
実際のところ、以下の5パターンしかありません。
100644
: 標準ファイル100755
: 標準ファイル(実行可能)040000
: ディレクトリ120000
: シンボリックリンク160000
: Gitlink。git-submodule で使います
Gitはファイルのパーミッションに関しては、実行ビットしか意識していません。
最終更新時刻
これには git log
を使えば良さそうです。rev を指定することで、当該リビジョン以前の最後の変更を探し出してきます。
% git log -1 --pretty=format:%aD <rev> git/git.go Fri, 12 Dec 2014 01:00:57 +0900
Go による実装
以上を踏まえた実装が motemen/go-vcs-fs です。
godoc/vfs.FileSystem という便利なインターフェースがあり、これを実装するようなコードになっています。じつは冒頭にあげた要件もこれがベースになっています。
sourcegraph/go-vcs
どうぞご利用ください……と言おうと思ってたんですが、sourcegraph/go-vcs というほとんど同じ先行実装がありました……機能が豊富であり、インターフェースが同じなのはもちろん、シンボリックリンクの扱いが TODO になってる点も一緒だったので、こちらを使うのがよさそうです。
こんな風に使います:
import ( "sourcegraph.com/sourcegraph/go-vcs/vcs" _ "sourcegraph.com/sourcegraph/go-vcs/vcs/gitcmd" ) ... repo, err := vcs.Open("git", ".") commit, err := repo.ResolveRevision(rev) fs, err := repo.FileSystem(commit)
sourcegraph.com/sourcegraph/go-vcs/vcs/gitcmd
をインポートすることで、vcs.Open("git", ...)
が使えるようになり、sourcegraph.com/sourcegraph/go-vcs/vcs/git
のほうだと libgit2 を使った実装になるみたいです。同様に Mercurial にも対応してるのが便利そうですね。