はじめに
GitHub.com は巨大なファイルを含んだコミットに対して Push 制限が設けられています。Working with large files
具体的には、次のようなメッセージが表示されることがあります。
- コミットに 50MB を超えるファイルが含まれている場合は Warning が表示されます
- warning は表示されるものの、リモートへの転送自体は成功します。(ただし非推奨)
remote: warning: Large files detected.
remote: warning: File big_file is 55.00 MB; this is larger than GitHub's recommended maximum file size of 50 MB
- コミットに 100MB を超えるファイルが含まれている場合は Error が表示されます
- リモートへの転送に失敗します
remote: warning: Large files detected.
remote: error: File giant_file is 123.00 MB; this exceeds GitHub's file size limit of 100 MB
上記の問題は、以下のいずれかの対応をすることで解決できます。
- 対象ファイルが必要な場合は、Git-LFS を利用する Versioning large files
- 対象ファイルが不要な場合は、リポジトリから取り除く Removing files from a repository's history
この記事では、2. の巨大なファイルをリポジトリから取り除く手順を 2 通り紹介します。
具体例
プロジェクトで利用するために、外部で構成管理されているリポジトリを GitHub.com にインポートするケースについて考えてみます。
例えば Android オープンソースプロジェクト(以下 AOSP)は android.googlesource.com で公開されていて 1,000 を超える Git リポジトリから構成されています。これらのリポジトリの中には、「巨大なファイルが含まれている、または過去に含まれていた」リポジトリがあり、GitHub.com でそのままインポートすることができません。
- 実際、見つかった巨大なファイルの多くは、通常のソースコードではなくビルド済みバイナリであったりテストデータであったりします。
次のステップでは、実在する外部管理のリポジトリを GitHub.com にインポートする処理を行います。
インポート時に git-push が失敗することの確認
巨大なファイルが含まれていることがあらかじめ判明している AOSP の platform/system/core.git を題材として、GitHub.com へのインポートに失敗することを確認します。
まずは以下の手順でインポートを試みます。
$ git clone --mirror https://android.googlesource.com/platform/system/core
$ git remote add github $YOUR_GITHUB_URL
$ git push --mirror github
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: be6e0a48b32da493f6a767241ba85e9b
remote: error: See http://git.io/iEPt8g for more information.
remote: error: File libunwindstack/tests/files/offline/jit_debug_x86_32/libartd.so is 219.06 MB; this exceeds GitHub's file size limit of 100.00 MB
! [remote rejected] android-9.0.0_r16 -> android-9.0.0_r16 (pre-receive hook declined)
ここで上記のエラーが出ました。エラー内容から 200MB を超える共有ライブラリ libartd.so
がコミットされていたため push に失敗したことがわかります。
次のステップでは、巨大なファイルの正体を特定します。
1. 対象ファイルの特定する
200MB を超える共有ライブラリがコミットが含まれていたため、 GitHub.com への push に失敗しました。ところが実際は、この共有ライブラリは指定のパスには既に libartd.so
は存在しません。
$ test -e libunwindstack/tests/files/offline/jit_debug_x86_32/libartd.so
次に、既に削除済みファイルであることを確認します
$ git log --diff-filter=D --summary -- libunwindstack/tests/files/offline/jit_debug_x86_32/libartd.so
delete mode 100644 libunwindstack/tests/files/offline/jit_debug_x86_32/libartd.so
つまり、例え削除済みであっても、巨大ファイルを含んだコミットがあったことには変わりないため、push に失敗したことがわかりました。このように push に失敗する前に、リポジトリの中に含まれた巨大なファイルをあらかじめ確認するためには、Atlassian の Reduce repository size で紹介されている git_find_big.sh
を活用すると良いことが知られています。このスクリプトでは Git 内部の パックファイル を走査して、巨大なファイルのサイズ順に表示してくれます。今回扱う platform/system/core.git リポジトリを対象に 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
224312 60015 70352dbb7a51e338096bb1305ea63641a39200cd libunwindstack/tests/files/offline/jit_debug_x86_32/libartd.so
28051 7497 e72e673ad74ff69060291ddab1a3989a7c6a8c07 libunwindstack/tests/files/offline/jit_debug_x86_32/libarttestd.so
10380 3225 92ed9915d4f9ea96a4accfcad82c657ff72f17fd libunwindstack/tests/files/offline/jit_debug_x86/libartd.so
8127 3048 ef1a6a1857bc2613a9ae162dd56f2cb67858eb3a libbacktrace/testdata/arm64/libskia.so
5968 2422 05278936be5fda0f62b5bd37b6951bcdbb5e8cd2 libunwindstack/tests/files/offline/jit_debug_arm/libart.so
5952 2401 09ba49532205981ff638c6935436cbb09d678aba libunwindstack/tests/files/offline/art_quick_osr_stub_arm/libart.so
5400 2175 bed8e3595aa8cab7f381cfcb7aa05d10f010e687 libbacktrace/testdata/arm/libart.so
5294 2610 855905655e4c822794b6d66657c657b5307f17c9 libunwindstack/tests/files/offline/jit_debug_arm/libartd.so
5076 1392 092fc3aec90445662e23c2fdca8207e031602793 libunwindstack/tests/files/offline/straddle_arm64/libunwindstack_test
4134 1369 871f6dc89e9fb88b8758f5ef2deb2224295f8411 libbacktrace/testdata/arm/libGLESv2_adreno.so
上記の結果から、 50MB を超える巨大なファイルは libartd.so
だけであることが明らかになりました。
したがって、
- 過去に管理されていた巨大なファイルが参照できなくても問題ないこと
- 対象コミット以降のすべてのコミットハッシュ(SHA-1)が変わっても問題ないこと
上記のポイントを理解したうえで、それでも問題がなければ、次のステップで、対象コミットを削除するためにコミット履歴を書き換えます。
2. コミット履歴の書き換え
2a. コミット履歴の書き換え: git-filter-branch
を用いた場合
git-filter-branch コマンドを用いることで、コミット履歴を書き換えることができます。
すべてのブランチ・タグから対象の巨大なファイルを rm する場合は、次のコマンドで実現できます
$ time git filter-branch --index-filter 'git rm --ignore-unmatch libunwindstack/tests/files/offline/jit_debug_x86_32/libartd.so' --tag-name-filter 'cat' -- --all
オプションの説明:
-
--index-filter <command>
- 各コミットハッシュに対して
<command>
を実行します
- 各コミットハッシュに対して
-
--tag-name-filter <command>
- コミット履歴の書き換えに際し、新しいコミットハッシュに対してタグを上書きするオプションです
-
<command>
の規則にしたがってタグ名を上書きします- 元のタグ名のままタグを上書きする場合
--tag-name-filter 'cat'
- すべて大文字でタグを上書きする場合
--tag-name-filter 'cat | tr [:lower:] [:upper:]'
- 元のタグ名のままタグを上書きする場合
- タグを上書きしても元のメッセージやタイムスタンプ、Tagger は変化しませんが、 タグにつけられた GPG 署名はストリップされます
-
-- --all
-
git-filter-branch
の対象とするコミットハッシュを取得する際のgit-rev-list
に渡すオプションです - 今回の目的はすべてのブランチ・タグから巨大なファイルを削除したいので
--all
とします
-
ファイル削除の確認手順
上記 git-filter-branch
を実行後、 libartd.so
の削除が正しく行われたことを確認します。
$ git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
-
git-filter-branch
によって退避されたrefs/original
のすべての参照を git-update-ref で削除します
$ git reflog expire --expire=now --all
- アクセスできないすべての無効な reflog エントリを削除します
$ git gc --prune=now
- git-gc で GC を実行し、古いオブジェクトを削除して、パックファイルを更新します
その後 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
28051 7497 e72e673ad74ff69060291ddab1a3989a7c6a8c07 libunwindstack/tests/files/offline/jit_debug_x86_32/libarttestd.so
10380 3200 92ed9915d4f9ea96a4accfcad82c657ff72f17fd libunwindstack/tests/files/offline/jit_debug_x86/libartd.so
8127 3048 ef1a6a1857bc2613a9ae162dd56f2cb67858eb3a libbacktrace/testdata/arm64/libskia.so
5968 2417 05278936be5fda0f62b5bd37b6951bcdbb5e8cd2 libunwindstack/tests/files/offline/jit_debug_arm/libart.so
5952 2401 09ba49532205981ff638c6935436cbb09d678aba libunwindstack/tests/files/offline/art_quick_osr_stub_arm/libart.so
5400 2170 bed8e3595aa8cab7f381cfcb7aa05d10f010e687 libbacktrace/testdata/arm/libart.so
5294 2607 855905655e4c822794b6d66657c657b5307f17c9 libunwindstack/tests/files/offline/jit_debug_arm/libartd.so
5076 1391 092fc3aec90445662e23c2fdca8207e031602793 libunwindstack/tests/files/offline/straddle_arm64/libunwindstack_test
4134 1362 871f6dc89e9fb88b8758f5ef2deb2224295f8411 libbacktrace/testdata/arm/libGLESv2_adreno.so
3695 1604 7a30bfa440efc24d651b544163e5574d6b027a63 libunwindstack/tests/files/offline/offset_arm/libunwindstack_test
上記のとおりようやく 50MB を超える libartd.so
が存在しなくなっていることが確認できました。
ところがここで一つ課題があります。
今回の作業は Azure D2V2 (Core=8, RAM=28GB, SSD=400GB) インスタンスで行いましたが、実行時間は real 15m48.335s
と 15 分以上要しました。実行時間をより短くするために、次のステップでは、 BFG という機能を絞ったシンプルかつ高速なツールを紹介します。
2b. コミット履歴の書き換え: BFG
を用いた場合
BFG は Scala 製でシンプルかつ高速なツールです。 git-filter-branch
ほど複雑な処理はできませんが、今回の目的に最も適した --strip-blobs-bigger-than <MB>
という便利なオプションがあります。再度取得し直した platform/system/core.git にこのツールを適用してみます。
$ java -jar bfg.jar --strip-blobs-bigger-than 50M platform/system/core.git
オプションの説明:
-
strip-blobs-bigger-than 50M
- 50MB より大きなすべての blob を削除します
実行結果
Scanning packfile for large blobs: 140503
Scanning packfile for large blobs completed in 1,009 ms.
Found 1 blob ids for large blobs - biggest=229696508 smallest=229696508
Total size (unpacked)=229696508
Found 1897 objects to protect
Found 687 tag-pointing refs : refs/tags/afw-test-harness-1.5, refs/tags/afw-test-harness-2.1, refs/tags/android-1.6_r1, ...
Found 190 commit-pointing refs : HEAD, refs/heads/master, refs/remotes/origin/HEAD, ...
Protected commits
-----------------
These are your protected commits, and so their contents will NOT be altered:
* commit f80c326d (protected by 'HEAD')
Cleaning
--------
Found 45661 commits
Cleaning commits: 100% (45661/45661)
Cleaning commits completed in 4,047 ms.
Updating 89 Refs
----------------
Ref Before After
---------------------------------------------------------------------------
refs/heads/master | f80c326d | 9a90afc4
refs/remotes/origin/master | f80c326d | 9a90afc4
refs/remotes/origin/master-cuttlefish-testing-release | fd088948 | 9c55624d
refs/remotes/origin/nougat-iot-release | 044e0276 | 52cd619d
refs/remotes/origin/o-mr1-iot-preview-7 | 2b49abe4 | 8ff6ed0b
refs/remotes/origin/o-mr1-iot-preview-8 | 22dc27b9 | 219cb8c9
refs/remotes/origin/oreo-mr1-1.2-iot-release | c53a0e91 | 8e1c9575
refs/remotes/origin/oreo-mr1-iot-release | f307efcf | 9ee5bb5f
refs/remotes/origin/pie-cts-dev | 2ef5c2ef | 1be06911
refs/remotes/origin/pie-cts-release | bfe37429 | 0710a778
refs/remotes/origin/pie-cuttlefish-testing | 17b9e8e0 | 7135e773
refs/remotes/origin/pie-dev | 820ef150 | c9998b31
refs/remotes/origin/pie-dr1-dev | 93d837f3 | bfe27d17
refs/remotes/origin/pie-dr1-release | 47e38865 | 92fcac25
refs/remotes/origin/pie-gsi | afc32531 | 788ecf39
...
Updating references: 100% (89/89)
...Ref update completed in 55 ms.
Commit Tree-Dirt History
------------------------
Earliest Latest
| |
.....................................................DDmmmmm
D = dirty commits (file tree fixed)
m = modified commits (commit message or parents changed)
. = clean commits (no changes to file tree)
Before After
-------------------------------------------
First modified commit | 150db124 | ecbcdafc
Last dirty commit | e242a97d | cd89e6b1
Deleted files
-------------
Filename Git id
--------------------------------
libartd.so | 70352dbb (219.1 MB)
BFG run is complete! When ready, run: git reflog expire --expire=now --all && git gc --prune=now --aggressive
上記のような実行レポートのサマリーが得られます。
-
Deleted files
から何が削除されたかも明らかになりました。
より詳細な実行レポートはリポジトリ脇のディレクトリ配下に次の形式で残されています。
cache-stats.txt
deleted-files.txt
object-id-map.old-new.txt
そして実行結果はわずか real 0m9.403s
で、 git-filter-branch
よりも 100 倍前後高速に結果が得られました。また git_find_big.sh
の内容も Step2a と同一でした。
3. インポート成功確認
最後に GitHub.com にインポートできることを確認します。
$ git push --mirror github
今度は無事成功しました。
上記のどちらの手法を用いても巨大なファイルは削除され、 git-push
できるようになったことが確認できました。
削除できない巨大なファイルが含まれている場合は
今回の例では、見つかった巨大なファイルが不要なため削除できるケースでしたが、削除できない場合はどうでしょうか。まずは Git LFS を利用するのが最も素直な解になります。それでもプロジェクトの都合で Git LFS が利用できない場合は巨大なファイルを分割したり、圧縮したりする回避策もあるかもしれません。
管理方法
$ md5sum $LARGEFILE > $LARGEFILE.md5
$ split $LARGEFILE -b 10M $LARGEFILE-
- 元ファイルのチェックサムと数 MB ごとに分割したファイルとして管理します
復元方法
$ cat $LARGEFILE-* > $LARGEFILE
$ md5sum -c $LARGEFILE.md5
-
githooks の
post-merge
やビルドシステムなどで fetch 後に復元処理と元ファイルとの完全性を確認します
ただし what-is-my-disk-quota にもあるように GitHub ではリポジトリのサイズは 1GB 以下を推奨されているため、分割管理をしたとしてもサイズの総和の制約は回避できません。
まとめ
この記事では、不要かつ巨大なファイルが含まれたリポジトリを GitHub.com にインポートする手順についてまとめました。git-filter-branch
でも BFG
でも不要なファイルを扱ったコミットを除去できることが確認できました。単純に巨大なファイルを削除するだけが目的なら BFG
を用いて、タグを一意の命名規則に沿って上書きしたり、ブランチ別に処理を分ける等、より複雑な操作をするときは git-filter-branch
を用いると良いと思います。