Git リポジトリに上がっているファイルを履歴ごと消すには？

仕事で必要になったので、ファイルを履歴ごと消す方法を試してみました。

#ファイルを消しても履歴は残っている

例えば、1GB のバイナリファイルを Commit & Push したとします。
そして、それを **git rm** で削除したとしてもリポジトリの容量は減りません。

なぜか？

**git rm** は「ファイルが削除されたことにするコマンド」であって、「Git リポジトリ内に保存されている履歴を消すコマンド」ではないからです。

http://git-scm.com/book/ja/v1/%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%82%8B-Git%E3%81%AE%E5%9F%BA%E6%9C%AC

このサイトに書かれていますが、Git は「差分」ではなく「スナップショット」を保存して、「どのスナップショットを参照するのか？」をコミット単位ごとに切り替える仕組みです。

**git rm** は、この「どのスナップショットを参照するのか？」という情報を削除するコマンドです。
もし、データを丸ごと消したいのであれば、**保存されている全ての「スナップショット」を消さなければなりません。**

# 保存されている全ての履歴を消す

では、どうすれば全てのスナップショットを消せるのか？
それは、以下のコマンドを実行すればできます。

```:cmd.exe
git filter-branch --tree-filter "rm -f [消したいファイルパス]" HEAD
git filter-branch --tree-filter "rm -f -r [消したいディレクトリパス] " HEAD
```

上が指定したファイルを消すためのコマンド、
下が指定したディレクトリ以下を消すコマンドです。(下は パスが / で終わる必要があります。)

コマンド実行後、

```:cmd.exe
Rewrite 30d202caa00757219fc7e1d2a0220c1501bc38b9 (9/9)
Ref 'refs/heads/master' was rewritten
```

このようなログが出たら成功です。

これで履歴がすべて削除されました……と言いたいところですが、リポジトリにまだ情報が残りっぱなしになっている場合があるので、以下のコマンドでリポジトリを最適化します。

```:cmd.exe
git gc --aggressive --prune=now
```


あとは、これをリモートリポジトリに Push するだけです。
が、普通に Push しようとすると、

```:cmd.exe
To C:\Users\*****\Workspace\Git\Remote
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'C:\Users\*****\Workspace\Git\Remote'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
```

このようなエラーが出て Push 出来ません。
どうやら、ハッシュが別物になっていてコンフリクトしてしまっているようです。

ですので、Push する際は

```:cmd.exe
git push -f
```

のように、-f オプションを使って強制的に上書きすると、無事に Push 出来ます。


# 検証時に発生した問題の数々

自分が検証した際に発生した数々の問題を載せておきます。

## A previous backup already exists in refs/original/
**git filter-branch** を使って、別のファイルを消そうとしたところ、以下の様なエラーが出て消せないことがありました。


```cmd.exe
A previous backup already exists in refs/original/
Force overwriting the backup with -f
rm:cannnot remove 'C:/Users/*****/Workspace/Git/Remote/.git-rewrite/backup-refs': Permission denied
rm: cannnot remove directory 'C:/Users/*****/Workspace/Git/Remote/.git-rewrite':Directory not empty
```

これは refs/original/ が原因で処理が実行できていないみたいです。
もし、このエラーが出たら、.git-rewrite フォルダを削除した上で

```:cmd.exe
git update-ref -d refs/original/refs/heads/master
```

このコマンドを実行すると、refs/original/ がなくなります。

## リポジトリのサイズが減らない
**git gc** を実行してもリポジトリのサイズが減っていない場合がありました。
その場合、一度ローカルリポジトリを削除してクローンを作りなおした上で **git gc** を実行するとサイズが減ります。

また、その場合はリモートリポジトリのサイズも減っていないので、リモートリポジトリに対して **git gc** を行う必要があります。

# リモートリポジトリから直接履歴を消してみた
**git gc** がリモートリポジトリに対してできるのであれば、出来るのではないか？と思って検証してみました。

結果、リモートリポジトリから直接消すことも出来ました。
リモートリポジトリなので、Push することなく反映されていました。

ただし、その後ローカルリポジトリで Pull してみたところ、**消したはずのファイルがなぜか消えずに残っていました。**
ログを確認してみたところ、

```
Merge branch 'master' of C:\Users\*****\Workspace\Git\Remote

ファイル名：delete_file.txt
状態　　　：追加
```
のようにマージがかかっていて、そこで消したはずのファイルが追加されていました。
どうやらローカルリポジトリでそのファイルを追加したことになっているらしいです。

# 履歴を編集したら、Pull ではなく Clone し直す
リモートリポジトリから直接履歴を消した状態、すなわち……

**「リモートリポジトリでは履歴が消えていて」**
**「ローカルリポジトリでは履歴がまだが残っている」**

という状況はリモートリポジトリから直接消す以外に、**複数人の運用でも普通に発生し得る状況です。**
例えば、

1. Aさんが履歴を消して、リモートリポジトリに Push する。
1. Bさんがそのリモートリポジトリを Pull する。

とした場合、Aさんがリモートリポジトリに Push した時点でそのような状況になります。

ですので、基本的に履歴を消した場合、ローカルリポジトリに反映する際は **Pull ではなく Clone** で複製し直すのが良さそうです。

#参考サイト
Git のさまざまなツール - 歴史の書き換え
http://git-scm.com/book/ja/v1/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E6%AD%B4%E5%8F%B2%E3%81%AE%E6%9B%B8%E3%81%8D%E6%8F%9B%E3%81%88

git最強のオプション filter-branch
http://qiita.com/Spring_MT/items/f60c391b5dbf569a1d12

git filter-branchで大事なデータを消去・上書き
http://qiita.com/wnoguchi/items/62f5e64ef2ae14b4f0ee

レポジトリからディレクトリを削除
http://d.hatena.ne.jp/ramen26/20111215/1323958593

リモートブランチにコミットしたファイルをなかったことにする方法
http://hack.aipo.com/archives/3932/

gitのユーザ名やメールアドレスをコミット後に書き換える。
http://koseki.hatenablog.com/entry/20081115/gitFilterBranch