Gitリポジトリの歴史改変（履歴を書き換える）

## 概要
最近担当した、SVNからGitへの移行の中でぶち当たった壁について。
今回、SVNから変換したリポジトリをGitHubに移行したのですが、その際、変換したそのままではエラーになってpushできませんでした。
どうやらGitHubにアップロードできるファイルの上限は100MBだそうで、そこに引っかかったみたいです。端的に言えば、そのエラーの元凶たる100MB超えのファイルをリポジトリから削除すればいいのですが、こちらに関してGit標準の機能を用いて解決できましたのでご紹介します。

## 作業環境
OS：Windows10
利用ソフトウェア：Git for Windows, PowerShell

## 100MB超えのファイルがありエラーになる場合の対応について
まずは、100MBを超えたファイルをGitHubにアップロードしようとしてエラーになった場合の対処法についてお話ししておきましょう。方法は大きく２つあります。
- LFSを使用してアップロードする
- アップロードを諦めて削除する

私のケースではエラーの元凶になっているファイルがそもそも不要なものであったため、今回は削除することにしました。
ちなみに、LFSはLarge File Storageのことで、デカいファイルをリポジトリとは別の場所の保存しておいて、リポジトリにはそのポインタを保存しておくという手法でGitHubの制限を回避することができるサービスおよびツールのことを指します。
###
参考：[Git Large File Storageについて](https://docs.github.com/ja/repositories/working-with-files/managing-large-files/about-git-large-file-storage)

## 「Gitに上げたファイルを削除する」とはどういうことか
削除コミットすればいいんだろ？と思ったそこのあなた、残念。
履歴と一緒にファイルの実体もリポジトリにいるのでそれも全部削除する必要があります。
そんなときはfilter-branchコマンドを使用しましょう。
こいつは簡単にいうと「特定の条件に合致する履歴に対して任意の処理が行える」コマンドです。
これを使って、**容量の大きいファイル入ったディレクトリがコミットされた履歴**を**削除**していきましょう。


```:構文
git filter-branch [オプション] '[実行したいコマンド]'... [git rev-listの引数]
```

### よくつかうオプション
- filter-branch最速
indexをもとに検索するので一番早く結果が見られるようになる[オプション](https://git-scm.com/docs/git-filter-branch#Documentation/git-filter-branch.txt---index-filterltcommandgt)です。（それでも遅いが）
```shell
git filter-branch --index-filter
```

- tagにも紛れている時に使うオプション
tag名を書き換えるオプションですが、実行するコマンドをcatのみとすることでtag名は書き換えないけど履歴は書き換えてくれるようになる[みたい](https://git-scm.com/docs/git-filter-branch#Documentation/git-filter-branch.txt---tag-name-filterltcommandgt)です。
```shell
git filter-branch --tag-name-filter cat
```

- 絶対改変マン
強制的に改変されます。
```shell
git filter-branch --force
```

- ゴミ掃除も一緒に
もし、容量の大きいファイル**だけ**をコミットしていた場合、このオプションがないと空のコミットが履歴に[残ってしまうそうです](https://git-scm.com/docs/git-filter-branch#Documentation/git-filter-branch.txt---prune-empty)。入れておきましょう。
```shell
git filter-branch --prune-empty
```

- git rmのオプション　その１
作業ツリーのファイルは削除しない[オプション](https://git-scm.com/docs/git-rm#Documentation/git-rm.txt---cached)です。速度変わるんでしょうか？
```shell
git rm --chached /bigSizefilePath
```

- git rmのオプション　その２
条件に一致しない時は何もしない[オプション](https://git-scm.com/docs/git-rm#Documentation/git-rm.txt---ignore-unmatch)です。こちらは速度変わってきそう。
```shell
git rm --ignore-unmatch /bigSizefilePath
```

### git rev-listの引数って？
構文の末尾に、「git rev-listの引数」というのがありますがこれは履歴の先祖を辿っていくための[コマンドです](https://git-scm.com/docs/git-rev-list)。
filter-branchで対象を見つけた時に「ここの親コミットにもいるんだろ？」と探してくれます。
「どこからどこまで辿るか？」や、「ここからは辿れないやつ」といった条件が指定できますが、今回やりたいのは不要ファイル全部削除することですので、指定する引数は１つです。
```shell
git filter-branch --index-filter 'git rm -rf /bigSizefilePath' -- --all
```
一番後ろの「--all」ですね。
とりあえず全部辿れ！ということです。潔い。
ちなみに、その前の「--」はfilter-branchとの境だよーという記号です。
filter-branchの最後にrev-listの引数が引っ付けられるルールなのでここでfilter-branch終わり！ってことですね。

### 早速消してみよう
実行する時はローカルでやりましょう。（いきなりリモート、ダメ絶対。）
上記を参考に、もしくは[こちら](https://git-scm.com/)も参考にして自分の環境にあったコマンドを作成して実行してみましょう。時間はかかりますので余裕ある時か、別の端末で。

## 改変結果をリモートに反映
filter-branchで改変された歴史はそれ以前とはまったくの別物になります。故に原則pushできません。「別モンやでー」と怒られます。
なので別で空のリポジトリを作成してそちらに移行するか -fで強制的にpushしましょう。

```shell
git push -f
```

## refs/original？なにそれ
filter-branchを実行した時に、実行前の状態がバックアップとして退避されます。それがrefs/originalです。
もしpushしようとした時にまだエラーが出ている場合、これが残っていることが原因の場面がほとんどでしょう。
.git/refs/originalのフォルダを削除すれば消えてなくなりますのでひとおもいにやっちゃってください。

## あとしまつ
gitはお利口でreflogという操作ログを残しているのでそれを削除します。
不要なコミットなどが消されて、到達不可能になった現時点より古いログも削除するように[expire-unreachableオプション](https://git-scm.com/docs/git-reflog#Documentation/git-reflog.txt---expire-unreachablelttimegt)もつけておきましょう。
```shell
git reflog expire --expire-unreachable=now --all
```

最後にガベージコレクションできれいさっぱり
```shell
git gc --prune=now
```

もし複数回filter-branchを行う場合は、この２点の掃除法を毎filter-branch行ってください。
さもないとなんか変な感じになってゴミが残ります。（よくわかってない）

## まとめ
めっちゃおそい

## 参考文献
[git filter-branchで過去の全てのcommitから画像ファイルの追加/変更をなかったことにしてリポジトリを軽量化する](https://dskd.jp/archives/46.html)

※めちゃくちゃ助かりましたありがとうございました。