過去のコミットでgit LFSを使うように歴史改変をする

Subversionからgitに移行したはいいが、履歴があまりにでかいのでPDFを別管理にしたい」という状況を考える。でかいデータやバイナリを別管理にするといえばgit LFS, 昔の履歴に遡って変更を加えるにはgit filter-branchであるが、これらを組み合わせる方法を以下に述べる。

全体の手順としては以下の通り:

  1. 使っているサーバーがLFSに対応しているかどうか確認する(GitHubなら無課金では1Gまでしか使えない、など)
  2. Git LFSはgitとは別なので、別途インストールする (Ubuntuならpackagecloudからインストールできる)
  3. filter-branchを炸裂させる
  4. リポジトリを掃除する

filter-branchを炸裂させる

# 全てのタグやブランチに対して、 *.pdfや*.keyをLFSで管理するように歴史改変する
git filter-branch --tree-filter "git lfs track \"*.pdf\" \"*.key\" >/dev/null" --index-filter "rm .gitattributes" --tag-name-filter cat -- --all

これの動作を解説する。まず git lfs track.gitattributes を書き換える。このファイルに書かれている種類のファイルだけがgit LFSの管理下に置かれる。中身が同じでも、gitの管理下のファイルとgit LFSの管理下のファイルは扱いが異なるため、 git add にてそれらのファイルを追加する必要がある。

そもそもgit filter-branchは、元のコミットをそれぞれ取り出して変更を加え、それらを繋ぎあわせて新しい歴史を作るというものである。このとき、コミットの変更処理のうち今回使う部分を抜き出すと以下のようになる。

  • 専用の t というディレクトリに、コミットの中身をぶち撒く。 (git checkout-index)
  • 逆に、コミットに入っていないファイルが残っていたら、それを削除する。 (git clean)
  • --tree-filter で指定されたコマンドを実行する。
  • t に入っているファイルからインデックスを再構成する。 (git update-index)
  • --index-filter で指定されたコマンドを実行する。
  • コミットする。 (git write-tree, git commit-tree)

つまり、 tree-filter で用意された箇所で .gitattributes を変更するだけでうまくいきそうな感じがあるが、これだけだと Pointer file error: Unable to parse pointer という謎のエラーが出てくる。

これは、 t ディレクトリに .gitattributes が残った状態で次の処理が走るからである。このcheckout-index処理で、git LFSは取り出されたポインタファイルを中身のあるファイルに変換する処理を行う。このとき、正しくない .gitattributes が残っているためにこのエラーが発生する。

そこで、 index-filter.gitattributes を削除している。本来の index-filter の使い方ではないが、うまく動作する。なお、元のコミットに .gitattributes がある場合も、 git checkout-index のときにそれが取り出されるので特に問題はない (はず) 。

末尾にある --tag-name-filter cat -- --all は変換対象を指定している。 --tag-name-filter cat はタグ名をcatで変換する、つまり何もしないように見えるが、これを指定することでタグも変換対象になる。 --allgit rev-list のオプションで、全てのrefを指定している。

特定のブランチだけを書き換えるなら、例えば -- mybranch のようにすればよい。

リポジトリを掃除する

LFSがない状況で一番手っ取り早いのは file:///path/to/repository を別の場所にcloneすることであった。ここで file:// をつけることでgitは .git/objects の中身をハードリンクするなどの横着なしに必要なオブジェクトを取得する。これをしないとpackされたでかいファイルなどがそのままついてきてしまう。

ただLFSの場合はLFSのオブジェクトがついてこないので意味がない。どうせLFSを使うときはサーバーがある状況なので、そのサーバーにpushしてしまうのがよいと思う。

まとめ

頑張って調べたが結局今回は使わないことになった。ぜひ誰か役立ててほしい。