Permalink
Fetching contributors…
Cannot retrieve contributors at this time
90 lines (54 sloc) 9.22 KB
layout page_type page_datetime page_id page_title page_tag page_description
./src/jade/_post.jade
post
2014-07-07T01:45:00
46
git filter-branchで過去の全てのcommitから画像ファイルの追加/変更をなかったことにしてリポジトリを軽量化する
Git
歴史の書き換えだぁ〜!

表題の通り、分散型バージョン管理システムのGitでいわゆる「歴史の書き換え」をする。

この処理を行う想定としては、複数人で進めているプロジェクトで開発の途中までは画像をリポジトリに含めて管理していたけど、今度から画像は別で管理することにしてリポジトリから消したい、などという場合。その後月日が経った状況で画像をcommitしていた頃のlogがとても容量を食っている場合でももちろん可。

写真素材サイトで画像をうっかりGit管理してたとか、ゲーム系でキャラクターや背景の高解像度の画像をGit管理していた頃があるとかだと、新しいbranchをcheckoutしてpushする度にリポジトリはどんどん肥大化していく。そうやってギガバイト単位に膨れ上がったリポジトリでも、filter-branchで劇的に軽量化することができる。

具体的にはfilter-branchというコマンドを使う。ファイルを持っていた歴史を残しておきたいのであればfilter-branchをする必要なないが、新規メンバーがそのリポジトリをcloneする時間を少しでも短縮したいと考えるなら、試してみる価値はある。

これをやる前に注意して欲しいのは、filter-branchを実行した環境以外では、実行後にリポジトリをcloneし直す必要があるということ。なぜなら、filter-branchはcommit自体の書き換えを行うのものなので、revertなどとは違いその書き換え自体のログは残らない。さらにcommitのハッシュも再発行される。そしてこれをリモートのリポジトリに反映するためにgit push -fする。なので、以前とは全く違うリポジトリになると考えた方がいい。もしfilter-branchに失敗したら、当然ながら、絶対にpushしてはいけない。やり直したい場合は実行環境ではもうログが失われているので、別のディレクトリに移動してfilter-branch実行前の状態をcloneしてやればいい。

手順はさほど難しくない。まず最初に、コラボレーターやコミッター全員にその時点での変更(branch含む)を全てpushしてもらい、変更作業を止める。そしてを行う作業者の手元で全てのbranchでpullしておく。push漏れ・pull漏れがあるとワークツリーがデグレードしてしまうので忘れずにやって欲しい。

次にリポジトリから消したいファイルやディレクトリを決める。これは通常のgitコマンドなので複数指定できる。今回はわかりやすいようにXXXフォルダ配下全てとYYY/img/admin-face.jpgファイルを削除してみる。

歴史の書き換えはたったの数個のコマンド実行で済む。

$ git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch XXX/ YYY/img/admin-face.jpg" --prune-empty -- --all

filter-branch--index-filterオプションで渡しているダブルクォーテーションの中が、各commitに対して行うコマンドになる。git rm -rfなのでフォルダもファイルも対象に消す。見た目わかりづらいがXXX/とYYY/img~の間にはスペースがあり複数指定している。

--chacedオプションがあるとcommit logからだけ削除してワークツリーでは残すようになる(つまりファイルが手元に残る)。--cachedオプションがないとワークツリーからも消えるのでそこは注意してほしい。

--ignore-unmatchで当てはまる対象がない時のエラーを無視できる。

--prune-emptyがあると、対象のファイルをcommit logから消した時に生まれうる、コメントのみの空commitも消してくれるようになる。前述の2つのオプションはgit rmコマンドで、--prune-emptyfilter-branchのオプションだ。

ハイフン2つで前のコマンドのオプションから抜け、--allで全ブランチを対象に同じ処理を行う。

実行完了には少し時間がかかるかもしれない。ファイル数やcommit数で前後する。

あとはローカルのキャッシュを消したりゴミ掃除したりしていく。

$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

掃除し終わったらgit log -pなどで消えていることを確認し、force pushする。

$ git push -f

コマンドが$ git push origin <branch-name> -fでないのは、前述では全ブランチに対してfilter-branchを行っているので、これをリポジトリに丸ごと反映したいから。filter-branchをしたのが特定のbranchだけ(あまりないと思うが)ならpush -fする時にbranchを指定しないとそれはそれで事故になるので注意。

以上となる。リモート反映後に他の作業者にリポジトリをcloneし直してもらえば、以後は重いlogのない軽量なリポジトリとなる。commitがリハッシュされているのは実行環境と別の作業環境でgit logし合って見ればわかる。

消すファイルの数やcommitの数にもよるので、この歴史の書き換えにかかる時間や軽量化できる容量は一概には言えないが、僕が関わっているプロジェクトで実行したところ8万オブジェクトで1.6GBあったリポジトリが、4万オブジェクトの145MBまで軽量化することができた。すごい威力を感じる。


filter-branchは別にリポジトリの軽量化を目的とした機能ではない。リポジトリにうっかり含めてしまったセキュリティ上問題あるデータをlogからも消し去りたい場合にも使う。全commitから目的のファイルをホニャララするのがfilter-branchの機能というわけだ。また、--index-filterではなく--tree-fliterを使って同様の処理を行うこともできるが、こちらはcheckoutをbranchごとに行うとのことで--index-filterより低速らしい。--tree-filterでは--allオプションはいらないのかどうか、調べたけどすぐに出てこなかったのでわからない。

実行するのはgitコマンド以外も可能だ。こちらの記事では--tree-filterオプションで、cp -fコマンドで強制的にファイルを上書きコピーしている。


この記事をある程度書いたところでもっとわかりやすく書いているブログがあることを知った。悔しいのでdskdでは--index-filterのオプションの書き方を少しだけ詳しく書いた。

本文の表現を加筆修正

なぜfilter-branchのあとで他の作業者はリポジトリをcloneし直すのか

filter-branchが実行されたリポジトリをリモートに反映後、他の作業者がcloneではなくそれまでのディレクトリでpullをするとどうなるかというと、普通にMergeされてpullできる。しかし、この作業環境のリポジトリには削除したかったファイルやそのcommitが残っているので、pullしたのちに何かを編集してcommitしてpushすると、消したかったはずの情報がまたリモートに送られてしまい意味がなくなる。さらに、filter-branchしたあとのリポジトリのcommit messageがfilter-branch前のに混ざって二重にcommitしているように見える。ハッシュは異なるので別の変更履歴として扱われるわけだ。git logするとちょっと耐えられない感じになるだろう。

履歴はよごれるしpushするとfilter-branchが無意味になので、他の作業環境では必ずcloneし直そう。

「なぜfilter-branchのあとで他の作業者はリポジトリをcloneし直すのか」の見出しとその本文を追記


下記は自分用のメモ:

$ git filter-branch --commit-filter '
 GIT_AUTHOR_NAME="oti"
 GIT_AUTHOR_EMAIL="otiext@gmail.com"
 GIT_COMMITTER_NAME="oti"
 GIT_COMMITTER_EMAIL="otiext@gmail.com"
 git commit-tree "$@"
' -- --all

authorやcommiterで秘匿情報が混ざったら--commit-filterを使う。改行は削除してカタカタッターンすること。