git最強のオプション filter-branch

  • 182
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

Git Advent Calendar / Jun. 21日目の記事を書かせて頂きます。

今回の記事では、gitのfilter-branchを紹介します。

filter-branchとは

これは、大量のコミットの書き換えを機械的に行うオプションです。
(filter-branch自体はシェルスクリプトで書かれています。)
これを使うとレポジトリの歴史上からコミットされたファイルを完全に抹消することができます!

今回、ファイルを抹消するためにfilter-branchの--index-filterオプションを使います。

使うシチュエーション

こんな怖いオプションどこで使うのかというと、例えば下記のようなシチュエーションが考えられます。

  • パスワードファイルを間違ってcommitしてしまった or やんごとなき事情により削除したい
  • 巨大なファイルを間違ってcommitしてしまった

1コミットだけなら良いのですが、何回もcommitされていたり、ブランチを作ってマージとかを繰り返しているとなると、もう手動では削除はできないですよね。

そこで、今回はfilter-branchの--index-filterオプションを使って、全てのcommitを精査して対象のファイルを消します。

実際にやってみる

まず、サンプルのgitのレポジトリを作りました。
ブランチ(test_a)も作り、password.txtというファイルに対してcommit & masterにmergeしています。

[ master ]$ ls
password.txt test.txt
[ master ]$ git log --graph --pretty=format:"%h : %s"
* bdb9b96 : password 2
* b000e66 : password 1
* e0c5788 : initial commit

で、後になってpassword.txtを消したいとなったとします。
今回はサンプルなので、そのままやってしまいますが、実際のサービスではまずは、テスト用にcloneしたレポジトリで試してからやってください。

[ master ]$ ls
password.txt test.txt
[ master ]$ git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch password.txt" --prune-empty -- --all
Rewrite b000e661b5267f00a0cd32f9b83514a8e2714f4c (2/3)rm 'password.txt'
Rewrite bdb9b9650c5fc1fe3c398672c1f22f86881325e1 (3/3)rm 'password.txt'

Ref 'refs/heads/master' was rewritten
Ref 'refs/heads/test_a' was rewritten
Ref 'refs/remotes/origin/master' was rewritten
[ master ]$ ls
test.txt
[ master ]$ git checkout test_a
Switched to branch 'test_a'
[ test_a ]$
test.txt

password.txtは消えてますね!
他のブランチでも消えています。
git log -pしても差分はみれません!
よーしこれで完了だ!!。。。。。とは、いきません。
例えば、

[ master ]$ git show b000e661b5267f00a0cd32f9b83514a8e2714f4c
commit b000e661b5267f00a0cd32f9b83514a8e2714f4c
Author: Spring_MT <today.is.sky.blue.sky@gmail.com>
Date:   Thu Jun 21 15:28:50 2012 +0900

    password 1

diff --git a/password.txt b/password.txt
new file mode 100644
index 0000000..f3097ab
--- /dev/null
+++ b/password.txt
@@ -0,0 +1 @@
+password

とかすると、差分が見れてしまいます。。。。
ローカルレポジトリにgitのオブジェクトがまだ残っていることに起因しています。

[ master ]$ ls .git/objects/b0/
00e661b5267f00a0cd32f9b83514a8e2714f4c
(object_idの最初の二文字がdirを示す)

これを消さないと、まだpassword.txtの中身がわかってしまうので、git gcで消しましょう。

[ master ]$ rm -rf .git/refs/original/
[ master ]$ git reflog expire --expire=now --all
[ master ]$ git gc --aggressive --prune=now
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), done.
Total 6 (delta 0), reused 0 (delta 0) 
[ master ]$ git show b000e661b5267f00a0cd32f9b83514a8e2714f4c
fatal: bad object b000e661b5267f00a0cd32f9b83514a8e2714f4c

これで、レポジトリからpassword.txtとその内容が抹消されました!
git gcしているので、オブジェクトもなくなっているので、復旧は不可能です。
filter-branchをする時以外でも、commitを消した際は、git gcしておくと良いかと思います。

filter-branchは用法、用量を守ってお使いください。

補足

--no-ffでmergeしている場合

mergeコミットがあると、 "error: duplicate parent"となって、git gcしてもオブジェクトが残ってしまいす。
この場合ローカルのデータを消すためには、push(forceオプション付き) -> レポジトリ消す -> clone する必要があります。