Gitリポジトリから容量の大きいファイルを履歴から抹消する

こんにちは。社内でGCP(特にGAE)を布教しているkaneshinです。


プロジェクトのバージョン管理としてGitを利用しているチームは多いと思います。
最近はエンジニア以外にもコミットをしてもらってプロジェクトのチーム全体でコミットすることも少なくないですが、Gitにそこまで詳しくない人が間違えてサイズの大きいファイルをコミットしてしまうとfetchやcloneするのに時間がかかるようになってしまいます。

一度コミットしてしまったファイルを無かったことにするにはコミット履歴から抹消してやる必要があります。

今回は「容量の大きいファイル特定」と「履歴からの抹消方法」を紹介させていただきます。

Gitオブジェクト

git-logo

Gitの履歴から特定のファイルを特定/抹消を行うためにGitオブジェクトを扱わなければなりません。このGitオブジェクトは .git/objects の中身を指し、このディレクトリが肥大化していると git clone にかなりの時間がかかることになります。
Gitオブジェクトがリポジトリにあるファイルの差分や履歴を管理しているため、コミットが多くなればなるほどオブジェクトの容量は膨れていきます。

そして、このオブジェクトには自分の知らないところで誰かがゴミファイルをコミットしていることもあり、そのゴミファイルが意図せずオブジェクトの容量を肥大化させている一因になっています。

Gitリポジトリの調査

ファイルサイズの調査〜大容量のファイルの特定

Gitオブジェクトに全ての履歴が格納されているので、これに対して下記のように調査を行います。

調査したいGitリポジトリをclone

$ git clone https://github.com/kaneshin/pigeon.git

既にcloneしているリポジトリで実施する場合は git gc を行い、オブジェクトファイルをパッキング(packfile作成)します。

$ cd /path/to/repository
$ git gc
.git/objects 全体のファイルサイズ

du コマンドを .git/objects に対して行えばGitオブジェクトのファイルサイズを測ることができます。

$ du -sh .git/objects
3.4M    .git/objects

git_find_big.shスクリプト実行

ここではAtlassianの Maintaining a Git Repository にある git_find_big.sh というスクリプトを使用します。
※時々利用したくなるので、PATHの通ったディレクトリにおいておくと便利です。

このスクリプトは履歴にある(Gitオブジェクトが管理している)ファイルの容量を降順でリストとして出力してくれます。
一覧はデフォルトで10件となっていますが、スクリプトを少し書き換えれば件数を変更することが可能です。

$ diff -u git_find_big.sh.orig git_find_big.sh
--- git_find_big.sh.orig        2016-04-29 10:19:09.659462261 +0000
+++ git_find_big.sh     2016-04-29 10:19:16.119461040 +0000
@@ -11,7 +11,7 @@
 IFS=$'\n';

 # list all objects including their size, sort by size, take top 10
-objects=`git verify-pack -v .git/objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head`
+objects=`git verify-pack -v .git/objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head -n 100`

 echo "All sizes are in kB's. The pack column is the size of the object, compressed, inside the pack file."

このスクリプトを実行して容量の大きいファイルを特定します。実行に際して、リポジトリのコミット履歴が多ければ多いほど処理に時間がかかります。

$ ./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
1293  1272  0948e00c671cecb934b7596fa14bbd6ff2f4ed47  assets/pigeon-app.gif
178   178   b357b895bc03db2f776aa0917092daa2ae81f843  assets/pigeon.png
101   101   da50182fbe55023692cf1c5719e2d5f1d825e429  assets/img/go-cloud-vision-api.png
57    54    81ef9fd972062b4e45ca2b195e501ac8ac1e1073  assets/pigeon-cmd.gif
31    31    3dc4b84f5e5e9e5fe8234e565757f474a7188e6e  assets/lenna.jpg
7     1     fe6b54c1a4c5b2379b323e0291c3226ff987da04  assets/lenna-face.json
7     2     30366a6e9837b9c3b160a7b785e780e065870436  stylesheets/normalize.css
7     2     00fca15cdbc0d16a6ecf7ec398840e46b13cd5e2  tools/cmd/pigeon-app/bindata.go
5     2     5978f291fbbb6034e29ab8414e62b91c13dd1bc8  README.md
5     1     b5f20c235ffda37acca9d177d47f34c7f77aab84  stylesheets/stylesheet.css

今一度無駄なファイルが入っていないかの確認をするだけでも面白いので試してみるのも良いと思います。

ファイルを履歴から抹消


!! WARNING !!

履歴改変は破壊的な変更です。Gitコマンドを十分理解している方のみ行ってください。

記事を読んで実行した結果、大切なリポジトリが壊れても責任は負いかねます。


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

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

例)

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

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

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

バックアップを取る

重要です。復元可能なように別リポジトリに全てのブランチをプッシュしておきます。

# 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

履歴抹消実行

スクリプトに書いておいて、ミスのないようにしましょう。(マジで)

#!/bin/bash

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

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

# 指定したファイルの履歴を抹消する
$ git filter-branch --index-filter "git rm -r --cached --ignore-unmatch ${target}" -- --all

git filter-branchでは全てのブランチを対象にするため -- --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'
...

無事、全ての履歴抹消が完了したらリモートに全てのブランチをプッシュします。

$ 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は使えば使うほどしっくり来ますが、たまにはリポジトリのメンテナンスをして綺麗に保つことが重要です。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

Androidで”動くスライドショー”を実装する〜Ken Burns Effect〜

Pairsの検索機能をElasticsearchにリプレースした話