Edited at
GitDay 8

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

More than 1 year has passed since last update.

この記事はGit Advent Calendar 2015の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 以下に依存ファイルを取得・ビルドし、そのままコミットしたとします。

直後、.gitignorenode_modules を書き忘れていることに気づき、慌てて git rm -rf node_modules をしたとしてもダメです。

一度コミットしてしまったものは既に管理対象となって通常の方法では消しさることはできません。

(そう、消したい過去を消去することができないのと同じです。)

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


◆ 調査方法


1. Gitリポジトリをclone

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

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

$ git gc --auto


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

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

$ du -sh .git/objects

295M .git/objects


3. git_find_big.sh スクリプト

Maintaining a Git Repositoryにある git_find_big.sh というスクリプトを使用します。

このスクリプトはリポジトリにあるファイルの容量降順で一覧してくれます。

一覧はデフォルト10件表示となっていますが、件数を変更することも可能です。

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

↑ を ↓ のように head コマンドにライン数を渡します。

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


4. 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の使い方は簡単ですが、とても強力で破壊的です。

例)

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

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

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


0. バックアップを取る

# 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. 履歴抹消対象ファイル

# 配列で抹消ファイルをセットする

$ 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 を削除してください。

$ 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. 履歴改変後のものをプッシュする

$ git push origin --all --force

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


◆ 結果

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

[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」 の紹介でした。

Gitは使えば使うほどしっくり来ますが、たまにはリポジトリのメンテナンスもしてあげてください。

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