Open5

Reposoup: ファイル列挙リポジトリの設計

okuokuokuoku

外出のたびに2種類のデータがGitリポジトリに生成、蓄積されていく。

  1. Git LFSに格納される写真データ
  2. GPX 1.x 形式の航跡データ(1秒ごとに地球上のどこに居たのかを記録したデータ)

これらのデータを時系列・空間的に整理したいので、Gitリポジトリ内部のデータを完全に列挙するためのリポジトリを生成する。つまり、写真の1枚1枚、航跡データXMLの1つ1つに256bitsのIDを振り、 IDとファイル実体の関連付けを行う ためのリポジトリをファイル列挙リポジトリとして用意する。

... 最初からファイル名をファイルIDにして格納すれば良いじゃんというのもあるかもしれないが、あんまりファイルのコピー時に複雑なことをやりたくないもんで。。

okuokuokuoku

ファイルIDのアサイン

今回は、ファイルIDは類推可能でも良いものとする。例えばGyazoのようにURLを知っている = リソースにアクセス権がある という単純化を行っている場合はそれでは不都合になる。

LFSファイル

Git LFSに格納されているファイルは oid としてファイル自身のSHA256が使用されている。というわけで、これをそのままファイルIDとして利用する。

...が、oidだけではファイルを取り出すのは面倒なので

# ファイルID<SPC>LFSフラグ<SPC>サイズ<SPC>初登場のコミット<SPC>パス
732a758621cc15b0fc990437170b06386af1879640e5a4a38457419a7e67f067 L 3955210 99fc6cb3e623f984465f1c4efae5d6af41936593 sync/DCIM/Camera/PXL_20220331_052024030.jpg

のような感じでファイルID 732a7586... と コミット 99fc6cb3... および パスを関連付けておく。また、ファイルサイズと"Git LFSに格納されているかどうか?"がわかるフラグも用意する。

通常ファイル

通常ファイルは、 SHA256("初登場コミット || ':' || パス") をファイルIDとする。逆算はできないのでファイルIDとコミット、パスの関係をLFSファイル同様に記録する。

LFSと異なり、 ファイルの内容をIDにしない 。全く移動が無かったケースなどで内容のないファイルが生成される可能性があるが、内容が無かったという情報は残す必要があるため。

ファイルフラグ

フラグ 内容
L LFSファイルである
b バイナリファイルである
t テキストファイルである
s symlinkである
x submoduleである めんどいのでやめ
d 削除

今回の用途としては Lbt にだけ意味があるが、例えばsymlinkとかが混入するとよくないので一応Git自体と同レベルの表現力を持たせることにする。

okuokuokuoku

制約の設定

簡単のため、いくつか重大な制約を入れる。

  • マージコミットを扱わない
  • non-fast-forwardがあった場合は更新に失敗して良い

要するに履歴はフラットで、あるコミットには高々0〜1個の親が居ると仮定できるものとする。このような制約を設けないと ファイルの初登場コミットを安易に決定できない という問題が出る。今回のユースケースでは、一度コミットされたファイルが変更されることは無い。

okuokuokuoku

コミットの列挙

Gitリポジトリのコミットは rev-list で列挙できる。また --first-parent でマージコミットの最初の親だけを追跡させられる。

$ git rev-list --first-parent 58f57cda5a1f5be5de34355764263d3c4bea84fe..master
22a55b9c403fc3f929cc39727a06e44a7db19715
7f7a9f5bb9466d9c2c3162355ef90061a784dfae
7de0b9fbca09789570ce4f44c40835dd554da616
f5983194a54d757c60460e6718b2787c4954dd4d
5cb0f9d1aa348c1fc67c0c24fbebc707a7fb1645

番兵(Sentinel)

あるリポジトリの最初のコミット(root)には親が存在しない。このような場合特別なtree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 を最初のコミットとして使える。

$ git rev-list --first-parent master | wc -l
1149

$ git rev-list --first-parent 4b825dc642cb6eb9a060e54bf8d69288fbee4904..master | wc -l
1149

このtreeは空のtreeで、通常のgit実装ではリポジトリに常に存在するようにハードコードされる。コミットではないので、コミットの列挙は常に末端に到達する。(FIXME: コード的な裏付けを取った方が良い気はする。)

ブランチの圧縮

rev-list のようなコマンドはブランチの履歴の合流点以前にも遡ってしまう。一応 rev-list--not オプションで履歴の集合から別のブランチを消せるが、"ブランチのブランチ"のようなケースを救う方法が無い。

... これは手動でトラックするしかないかなぁ。。手元のユースケースでは、OneDriveと携帯電話側で別々のブランチを運用している。

okuokuokuoku

コミットで追加/削除されたファイルの列挙

あるコミットで変更されたファイルを列挙するには diff コマンドを使用する。複数回の列挙が必要になる。

対象ファイルの列挙

$  git -c "diff.renamelimit=9999999" --no-abbrev --raw fe1ec044f3ad522abeb969417c10fe1d841ecb64..f9c3da95c46ce41b46da6c4cdb509d8a735b55ab
:100644 000000 131c45d8b8400d28b79c6f1c3945e9a7cc36b46a 0000000000000000000000000000000000000000 D      lib-runtime/lib-runtime.cmake

まず、 --raw --no-abbrev でdiffを実施し、ファイル名とblobのSHAを得る。リネームの場合はファイル名が2つ得られることに注意。

バイナリファイルの検出

$ git diff --numstat --no-renames 4b825dc642cb6eb9a060e54bf8d69288fbee4904..
1       0       counter
-       -       random.blob

diff --numstat を使用すると、バイナリファイルは - - のように出力されるので判別ができる。

ファイルサイズおよびLFSファイルかどうかの検出

まず blob のサイズを検出する。

$ git cat-file -s f1e5a492ba88c46e255cede380a123d0b44dcd6c
133

パスがバイナリファイルであることがわかっていれば、それをそのまま採用する。

適当なスレッショルド以下(500バイトとか)であれば -p で内容を出力して、LFSのポインタファイルであるか確認する。

$ git cat-file -p f1e5a492ba88c46e255cede380a123d0b44dcd6c
version https://git-lfs.github.com/spec/v1
oid sha256:28e961214c55bafbbad55dd031d9ed4e255d7f511caee53ab9d09bfe28f0dbf4
size 35448633

サイズはポインタファイルに書かれている。