Gitリポジトリをメンテナンスして軽量化する

この記事は[Git Advent Calendar 2015](http://qiita.com/advent-calendar/2015/git)の8日目の記事です。

## Gitリポジトリのメンテ？

Gitリポジトリにあるファイルは `.git` がバージョン管理をしています。
今回はその `.git` をメンテナンスする話です。

## はじめに

- リポジトリに容量の大きいファイルをコミットしてしまった
- `git clone` がやたらと時間がかかる（知らない間に容量の大きいファイルがコミットされている可能性がある）
- 複数あるリポジトリを統合したい

こんな悩みを持ったことはないでしょうか。大型のプロジェクトでないと発生しないと思うので、個人プロジェクトではなかなか遭遇することはないでしょう。

今回は上記を解消するための **リポジトリメンテナンス方法** をご紹介します。

## **!! 注意 !!**

Gitリポジトリのメンテナンスは破壊的なため、Gitのコマンドを理解している方のみ行ってください。
この記事を読んで実行した結果、大切なリポジトリが壊れても当方は責任を負いかねます。

## Gitオブジェクト（Git管理ファイル）の削減

Gitオブジェクトは `.git/objects` の中身（00~FF）を指します。これが肥大化していると `git clone` にかなりの時間がかかることになります。
リポジトリにあるファイルの差分や履歴を管理しているオブジェクトで、コミットが多くなればなるほど容量は膨れていきます。

このオブジェクトには知らず知らずに**ゴミ**をコミットしてしまっていることがあります。 :put_litter_in_its_place:
そして、そのゴミが意図せずオブジェクトの容量を肥大化させている一因になることも多いです。

良くある一例で、`npm install` を行い `node_modules` 以下に依存ファイルを取得・ビルドし、そのままコミットしたとします。
直後、`.gitignore` に `node_modules` を書き忘れていることに気づき、慌てて `git rm -rf node_modules` をしたとしてもダメです。
一度コミットしてしまったものは既に管理対象となって通常の方法では消しさることはできません。
（そう、消したい過去を消去することができないのと同じです。）

このようなリポジトリは最初の方は問題ないですが、ファイルが増えてくると `git clone` に時間がかかってくるので履歴を消去したほうが時間の節約になります。

## ◆ 調査方法

### 1. Gitリポジトリをclone

```sh
$ git clone git@github.com:kaneshin/dotfiles.git ~/dotfiles
```

既にリポジトリをcloneしている場合はガベコレをします。

```sh
$ git gc --auto
```

### 2. `.git/objects` のファイルサイズ

`du` コマンドを使用して、 `.git/objects` のファイルサイズを測ります。

```sh
$ du -sh .git/objects
295M    .git/objects 
```

### 3. [git_find_big.sh](https://confluence.atlassian.com/bitbucket/files/321848291/321979854/1/1360604134990/git_find_big.sh) スクリプト

[Maintaining a Git Repository](https://confluence.atlassian.com/bitbucket/maintaining-a-git-repository-321848291.html)にある [git_find_big.sh](https://confluence.atlassian.com/bitbucket/files/321848291/321979854/1/1360604134990/git_find_big.sh) というスクリプトを使用します。

このスクリプトはリポジトリにあるファイルの容量降順で一覧してくれます。
一覧はデフォルト10件表示となっていますが、件数を変更することも可能です。

```sh
objects=`git verify-pack -v .git/objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head`
```
 ↑ を ↓ のように `head` コマンドにライン数を渡します。

```sh
objects=`git verify-pack -v .git/objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head -n 100`
```

### 4. [git_find_big.sh](https://confluence.atlassian.com/bitbucket/files/321848291/321979854/1/1360604134990/git_find_big.sh) スクリプト実行

コミット履歴があればあるほど処理に時間がかかります。

```
$ ./git_find_big.sh
All sizes are in kB's. The pack column is the size of the object, compressed, inside the pack file.
size   pack   SHA                                       location
34442  31741  AAAAAAb732e07f17cf71f1d8584a48aXXXXXXXXX  dotfiles/node_modules/protractor/selenium/selenium-server-standalone-2.45.0.jar
10784  3428   AAAAAAe5f77536e4b078fd5d1957f87XXXXXXXXX  dotfiles/node_modules/chromedriver/lib/chromedriver/chromedriver
```

上記のような一覧を得ることが出来ます。
普段、あまり気にせずにGitへコミットしていると思いますが、今一度無駄なファイルが入っていないかの確認も面白いので試してみるのもいいと思います。

## ◆ ファイルを歴史から抹消する方法

歴史の書き換えで`git filter-branch`コマンドを使用します。これを使いこなすことが出来ればあなたもリポジトリ~~クラッシャー~~メンテナーになることができます。

この`filter-branch`の使い方は簡単ですが、とても強力で破壊的です。

例）

```sh
git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD
```

`HEAD`を対象としてコマンドを実行することができます。`--all` とすれば全てのブランチに対して実行します。

さて、実際のメンテの流れです。

### 0. バックアップを取る

```sh
# clone
$ git clone git@github.com:foo/bar.git /tmp/bar
$ cd /tmp/bar

# リモートのブランチを全てチェックアウト
$ git branch -r | sed -e "s#origin/##" | xargs -I{} git checkout -b {} origin/{}

# バックアップとして全てのブランチを別リモート先へプッシュ
$ git remote add tmp git@github.com:foo/bar-tmp.git
$ git push --force tmp --all
```

### 1. 履歴抹消対象ファイル

```bash
# 配列で抹消ファイルをセットする
$ TARGETS=(
  "shared-local-instance.db"
  "dump.rds"
  "*.log"
  "node_modules/"
)

# 半角スペースでjoinする
$ target=$(printf " %s" "${TARGETS[@]}")
$ target=${target:1}
```

### 2. 履歴抹消する

全てのブランチを対象にするため `-- --all` で実施する
recursiveで行いたくない場合は `git rm -r` の `-r` を削除してください。

```sh
$ git filter-branch --index-filter "git rm -r --cached --ignore-unmatch ${target}" -- --all
```

実行すると、削除対象ファイルが存在した場合に下記のようなログが出力されます。

```
Rewrite 1a8b35d383ae6472ef7ab8591aca3540f0771744 (3806/29089)rm 'include/swift/ABI/Class.h'
rm 'include/swift/ABI/MetadataValues.h'
rm 'include/swift/AST/AST.h'
rm 'include/swift/AST/ASTContext.h'
rm 'include/swift/AST/ASTVisitor.h'
rm 'include/swift/AST/ASTWalker.h'
rm 'include/swift/AST/Attr.def'
rm 'include/swift/AST/Attr.h'
rm 'include/swift/AST/Builtins.def'
rm 'include/swift/AST/Builtins.h'
rm 'include/swift/AST/Component.h'
rm 'include/swift/AST/Decl.h'
rm 'include/swift/AST/DeclContext.h'
rm 'include/swift/AST/DeclNodes.def'
rm 'include/swift/AST/DiagnosticEngine.h'
rm 'include/swift/AST/Diagnostics.def'
...
```

### 3. 履歴改変後のものをプッシュする

```sh
$ git push origin --all --force
```

もしくは、新しくリポジトリを作成して新しい方へプッシュするのもアリです。

## ◆ 結果

これは実際に実行したときの結果ログです。

```sh
[kaneshin@ip-172-0-0-0] /tmp                                                    
$ git clone git@github.com:foo/bar.git                               
Cloning into 'bar'...                                                        
remote: Counting objects: 202026, done.                                               
remote: Compressing objects: 100% (2547/2547), done.                                  
remote: Total 202026 (delta 4158), reused 3859 (delta 3859), pack-reused 195620       
Receiving objects: 100% (202026/202026), 292.02 MiB | 9.04 MiB/s, done.               
Resolving deltas: 100% (116884/116884), done.                                         
Checking connectivity... done.                                                        

# ... リモートからバックアップを消去する

[kaneshin@ip-172-0-0-0] /tmp                                                    
$ git clone git@github.com:foo/bar.git                               
Cloning into 'bar'...                                                        
remote: Counting objects: 140976, done.                                               
remote: Compressing objects: 100% (3913/3913), done.                                  
remote: Total 140976 (delta 8047), reused 7926 (delta 7926), pack-reused 129137       
Receiving objects: 100% (140976/140976), 148.73 MiB | 3.59 MiB/s, done.               
Resolving deltas: 100% (90626/90626), done.                                           
Checking connectivity... done.      
```                                                  

ピックアップして見てみると

```
Receiving objects: 100% (202026/202026), 292.02 MiB | 9.04 MiB/s, done.               
↓
Receiving objects: 100% (140976/140976), 148.73 MiB | 3.59 MiB/s, done.               
```

削減した結果、`292MB` から `148MB` になりました。
およそ半分にまで削減できたので、うれしい限りです。無駄なファイルをコミットしたまま放置はダメですね。


## おわりに

今回の話はAtlassianが紹介している 「[Maintaining a Git Repository](https://confluence.atlassian.com/bitbucket/maintaining-a-git-repository-321848291.html)」 の紹介でした。 
Gitは使えば使うほどしっくり来ますが、たまにはリポジトリのメンテナンスもしてあげてください。

複数リポジトリ統合の話しは別の機会に