Git による巨大なリポジトリの対応方法

Nicola Paolucci
Nicola Paolucci
リストに戻る

コードベースの発展過程を記録して開発者間の協同作業を効率化する際は、git こそ真打です。しかし、記録対象のリポジトリがとてつもなく巨大なものになった際に、いったい何が起こるのでしょうか?

この投稿では、異なるカテゴリの巨大さに適切に対処するためのアイデアとテクニックをいくつか紹介します。

大規模リポジトリの 2 つのカテゴリ

考えてみると、リポジトリが巨大化する主な理由は大きく 2 つあります。

  • 非常に長い期間にわたって履歴が積み上げられた (プロジェクトが非常に長い期間継続的に拡大を続けたために開発成果が積み重なった) 場合
  • これらには、追跡してコードと組み合わせる必要がある巨大なバイナリ アセットが含まれています。
  • 上記の両方。

したがって、リポジトリは、作業ディレクトリのサイズ (つまり最新のコミット) と、蓄積された履歴全体のサイズという、2 つの直交する方向に大きくなる可能性があります。

既に廃止された古いバイナリ アーティファクトがまだリポジトリに保存されているため蓄積された履歴が肥大化する場合がありますが、そのためにはわりと簡単な (面倒かもしれませんが) 修正があります。次をご参照ください。

上に述べた二つの問題に対応するためのテクニックと手法は、相補的なものになる場合もあるものの、それぞれは異なるものですので、ひとつずつ取り上げていきましょう。

非常に長い履歴を有するリポジトリの取り扱い

リポジトリが大規模であると見なす境界はかなり高いのですが (たとえば、最新の Linux カーネルには 1,500 万行以上のコードがありますが、人々はいとわずにそれを読むようです)、規制上/法的な理由からそのまま保存しておく必要のある非常に古いプロジェクトは、クローン時に問題になる可能性があります (わかりやすくするために、Linux カーネルは履歴リポジトリとより新しいリポジトリに分割されており、完全な統一された履歴にアクセスするには簡単なグラフト設定が必要です)。

単純な解決法は shallow clone です

1 つ目の方法は、git によって shallow clone を行うことであり、これは開発者にとってもシステムにとっても時間とディスク領域の節約になる高速クローンの手法です。Shallow clone とは、コミット履歴中における最新の n 個のコミットのみを保持するリポジトリをクローンする手法です。

その方法は、単に - -depth オプションをつけるだけです。例:

git clone --depth depth remote-url

プロジェクトが 10 年以上にわたる歴史を持ち、その間にリポジトリが積み上げられてきたような場合 (たとえば Jira の場合、私たちは 11 年もの歴史を有するコードベースを git に移行しました)、この方法でかなりの時間を節約できます。

Jira のフル クローンは 677MB であり、さらに 320MB を超える作業ディレクトリがあるため、コミットの合計は 47,000 を超えます。Jira のチェックアウトを行って簡単に比較してみると、shallow clone の場合の所要時間は 29.5 秒であったのに対して、すべての履歴を含む完全なクローンの場合は 4 分 24 秒を要しました。この差は、過去にプロジェクトに組み込まれたバイナリ データの数にも比例して大きくなります。いずれにせよ、これはビルド システムにとって大きなメリットを有するテクニックです。

最近の git では shallow clone のサポートが改善されました

かつての Shallow clone は一部の機能がほとんどサポートされておらずgit の世界の問題児とでも言えるものでした。しかし、最近のバージョン (1.9 以降) において状況は大きく改善されており、現在では shallow clone からでもリポジトリへの pullpush を正しく行えます。

部分的な解決策は filter-branch です

間違ってコミットした大きなバイナリ データや今後使用しない古いデータを含む巨大リポジトリの場合は、filter-branch が非常に有用なソリューションです。このコマンドによってプロジェクトの履歴全体を調べて、あらかじめ設定したパターンに従ってファイルの抽出、修正、変更、除外などの処理を行えます。これは git を利用するプロジェクトにとって非常に強力なツールです。Git リポジトリ中のサイズの大きなオブジェクトを調べるためのヘルパー スクリプトも既に提供されており、簡単に利用できます。

filter-branch の使用例 (credit):

git filter-branch --tree-filter 'rm -rf /path/to/spurious/asset/folder' HEAD

filter-branch にはちょっとした問題点があります。即ち、一度 filter-branch を実行すると実質的にはプロジェクトのすべての履歴が書き換えられたことになり、すべてのコミット ID が変化します。このため、すべての開発者が実行後のリポジトリを再びクローンする必要があります。

したがって、filter-branch によってクリーンアップを行う予定がある場合は、それをチーム内に周知してその操作の実行中はリポジトリを短時間フリーズし、終了後は全員に対してリポジトリを再度 clone するように通知する必要があります。

shallow-clone の代替: 1 つのブランチのみをクローンする

2012 年 4 月にリリースされた git 1.7.10 から、次のようにクローンの対象を単一ブランチに制限できるようになりました。

git clone URL --branch branch_name --single-branch [folder]

この特定の解決策は、長期間実行されていて差異が多いブランチ、またはブランチの数が多い場合に役立ちます。差異がほとんどない少数のブランチしかない場合は、これを使用しても大きな違いは見られません。

Stack Overflow の記事をご参照ください。

巨大なバイナリ アセットを持つリポジトリの処理

2 つ目のタイプの巨大リポジトリは、巨大なバイナリ データを含むコードベースからなるリポジトリです。たとえば、ゲーム開発チームは巨大な三次元モデルを扱って、ウェブ開発チームは画像の生データを記録する必要が生じる場合があります。この必要性は、CAD 開発チームのバイナリ派生物の処理や記録でも同様です。即ち、git を利用するさまざまな分野のソフトウェア開発チームがこの問題に直面するのです。

Git のバイナリ データ処理能力は特に問題があるわけでも、特に優れているわけでもありません。初期設定では git はバイナリ データについて一連のバージョンのすべてを圧縮して格納しますが、これはバージョン数が多い場合は明らかに得策ではありません。

この状況を改善するには、いくつかの基本的な手法があります。たとえば、ガベージ コレクション git gc を実行する、.gitattributes でバイナリ タイプを指定して delta コミットを適用するなどです。

しかし、効果的なアプローチはそれぞれ異なるため、プロジェクトのバイナリ アセットの性質を考えることが重要です。たとえば、次では確認すべき 3 つのポイントがあります (発言してくれた Stefan Saasen に感謝します)。

  • ある種の単なるメタデータ ヘッダーに限らず、大きな変更のあるバイナリ ファイルについては、往々にして delta 圧縮は役に立ちません。したがって余計な delta 圧縮動作がリパック時に発生することを防止するために、delta off にすることを推奨します。
  • 上のシナリオに従うと、それらのファイルに対しては zlib 圧縮もあまり有効ではなく、したがって core.compression 0core.loosecompression 0 を指定して圧縮を無効にしてもよいです。ただし、これは圧縮が有効なすべての非バイナリ ファイルに悪影響を与える可能性のあるグローバル設定であるため、この推奨設定はバイナリ アセットを別のリポジトリに分離した場合のみ有用と言えます。
  • なお、git gc は「重複した」ルーズ オブジェクトを 1 個のパックファイルに変換しますが、ここでも結果として生成されるパック ファイルの圧縮効果は小さいと思われることにご留意ください。
  • core.bigFileThreshold の微調整を紹介します。.gitattributes における設定がない場合は 512MiB を超えるファイルは delta 圧縮されないため、この方法を試す価値はあります。

テクニック 1: sparse checkout

sparse checkout (Git 1.7.0 以降において提供) は、バイナリ データの問題に対処するためのちょっとした助けになります。このテクニックではチェックアウトするフォルダーを明示的に指定できるため、作業ディレクトリはクリーンに保たれます。残念なことにローカル リポジトリ全体のサイズは変わりませんが、フォルダー ツリーのサイズが巨大なものになっている場合は有用なテクニックです。

関係するコマンドは何でしょうか? ここに例があります (credit)

  • リポジトリ全体を一括クローンする: git clone
  • 機能をアクティブ化する: git config core.sparseccheckout true
  • アセット フォルダーを除外して、明確に必要なフォルダーのみを指定する
echo src/ › .git/info/sparse-checkout
  • ツリーを指定されたとおりに読み取る: git read-tree-m-u HEAD

上記操作を行った後は通常の git コマンドを使用できますが、作業ディレクトリには上で指定したフォルダーのみが含まれます。

テクニック 2: サブモジュールの使用

巨大なバイナリ アセット フォルダーを処理するもう 1 つの方法は、それらを別のリポジトリに分割してサブモジュールによってアセットをメイン プロジェクトにプルすることです。これによって、アセットを更新するタイミングを制御できるようになります。サブモジュールの詳細については、コア コンセプトとヒント代替案の投稿をご覧ください。

サブモジュールを利用する方法を取る場合は、巨大バイナリ ファイルの問題に対して参考となるいくつかのアプローチを説明したプロジェクトの依存関係の処理の複雑さを参照するとよいでしょう。

テクニック 3: git annex または git-bigfiles の使用

git においてバイナリ データを取り扱う第三のオプションは、適切なサード パーティ製拡張ツールの利用です。

最初に取り上げたいのが git-annex で、ファイルの内容をリポジトリにチェックすることなくバイナリ ファイルの管理が可能なツールです。git-annex ではファイルを特別な key-value ストアとして保存して、git にはシンボリックなリンクのみを追加して通常のファイルのようにバージョン管理を行えます。使用方法は簡単であり、利用例も理解しやすいでしょう

次に取り上げたいが git-bigfiles で、これは非常に大きなファイルを扱うプロジェクトにおいて Git を利用する開発者をサポートする git のフォークです。

[最新情報]... または、すべてスキップして Git LFS を使用できます

大きなファイルを定期的に扱う場合は、アトラシアンが 2015 年に GitHub と共同開発した LFS (大容量ファイル サポート) の活用を是非ご検討ください。ベスト ソリューション候補です。

当然、Git LFS はファイル自体をリポジトリに格納するのではなく、大きなファイルへのポインターをリポジトリ内に格納する拡張機能です。実際のファイルはリモート サーバーに保存されます。ご想像のとおり、これによってリポジトリのクローン作成にかかる時間が大幅に短縮されます。

GitHub と同様に Bitbucket は Git LFS をサポートしているため、あなたは既に Git LFS を利用できる可能性があります。これは、デザイナー、ビデオグラファー、ミュージシャン、CAD ユーザーなどのチームに特に役立ちます。

結論

リポジトリの履歴が巨大なものになっていたとしても、また巨大なアセットを取り扱わなければならないとしても、git のすばらしい機能が活用できないと諦める必要はありません。どちらの問題に対しても有用なソリューションがあるのです。

DVCS の活用に関する詳細は @durdn をフォローしてください。

Git を学習する準備はできていますか?

この対話式チュートリアルを利用しましょう。

今すぐ始める