git は、コードベースの発展過程を記録し、開発者間の協同作業を効率化する強力なツールです。でも、記録対象のリポジトリがとてつもなく巨大なものになったときは何が起こるのでしょうか?
この記事では、いくつかの異なる意味での巨大化に正しく対処するためのアイデアと手法を少し紹介してみたいと思います。
二種類の巨大リポジトリ
よく考えてみると巨大リポジトリが生ずる理由はおおまかに言って二つあります:
- 非常に長い期間にわたって履歴が積み上げられた (プロジェクトが非常に長い期間継続的に拡大を続けたために開発成果が積み重なった) 場合
- 巨大でしかも履歴の記録が必要なバイナリ データが存在し、それがコードに反映される場合
- その両方の場合
即ち、リポジトリの巨大化は二つの異なる方向に向かって起こることになります。それは、作業ディレクトリのサイズ (即ち直近のコミットのサイズ) の問題と全体の履歴の積み重ねの程度の問題の二つです。
後者の問題は時にリポジトリ中に残る古くてもはや使われていないバイナリ データが引き起こす問題と一緒に起こることがありますが、それが起こったとしても以下に示すように比較的簡単な解決策があるのです。
上に述べた二つの問題に対応するためのテクニックと手法は、相補的なものになることもあるものの、それぞれは異なるものですので、ひとつずつ取り上げていきましょう。
非常に長い履歴を有するリポジトリの取り扱い
リポジトリが巨大であるかどうかを判定するのは難しい (例えば最新の Linux カーネルのコードは 1,500 万行を超える程巨大だが技術者はその全体を使いこなしている) のですが、非常に歴史の長いプロジェクトで規制または法令によって変更が禁止されている場合はクローンを作ることも困難であるほどの大きさになることもあります (正確に言うと、Linux カーネルは、長い歴史を有するリポジトリと比較的最近開発されたリポジトリに分けられますが、簡単な接続設定によってそれらをひとつのものとして全体にアクセスすることができます)。
簡易ソリューション: shallow clone
第一の方法は、git を用いて shallow clone を行うことであり、これは開発者にとってもシステムにとっても時間とディスク領域の節約になる高速クローンの手法です。Shallow clone とは、コミット履歴中における最新の n 個のコミットのみをクローンする手法を意味します。
その方法は、次の例のように単に - -depth オプションを利用するだけです:
git clone --depth depth remote-url
プロジェクトが 10 年以上にわたる歴史を持ち、その間にリポジトリが積み上げられてきたような場合 (例えば JIRAhttp://www.atlassian.com/ja/software/jira)、この方法によって実現されるさまざまなクローン時間短縮を合計すると顕著な効果が得られます。
JIRA の全体をクローンするとサイズは 677MB にもなり、47,000 を超えるコミットが可能となるように作業ディレクトリにはさらに 320MB を超える領域が必要です。JIRA のチェックアウトを行って簡単に比較してみると、shallow clone の場合の所要時間は 29.5 seconds であったのに対し、すべての履歴を含む完全なクローンの場合は 4 minutes 24 seconds を要しました。この差は、過去にプロジェクトに組み込まれたバイナリ データの数にも比例して大きくなります。いずれにせよこれはビルドシステムにとって大きな利点を有するテクニックです。
最新の git における shallow clone サポートの強化
かつての Shallow clone は、いくつかの機能のサポートがほとんどなく、git の世界の問題児とでも言えるものでした。しかし、最近のバージョン (1.9 以降) において状況は大きく改善されており、現在では shallow clone からでもリポジトリへの pull や push を正しく行うことができます。
部分的なソリューション: filter-branch
間違ってコミットした大きなバイナリ データや今後使用することのない古いデータを含む巨大リポジトリの場合は、filter-branch が非常に有用なソリューションです。このコマンドを使用すると、プロジェクトの履歴全体を調べて、あらかじめ設定したパターンにしたがってファイルの抽出、修正、変更、除外などの処理を行うことができます。これは Git を利用するプロジェクトにとって非常に強力なツールです。リポジトリ中のサイズの大きなオブジェクトを調べるためのヘルパースクリプトもすでに提供されており、簡単に利用できます。
filter-branch の使用例 (クレジット):
git filter-branch --tree-filter 'rm -rf /path/to/spurious/asset/folder' HEAD
filter-branch にはマイナーな問題点があります。即ち、一度 filter-branch を実行すると実質的にはすべての履歴が書き換えられたことになり、すべてのコミット ID が変化します。このため、すべての開発者が実行後のリポジトリを再度クローンする必要が生じます。
したがって、filter-branch を使用してクリーンアップを行う予定がある場合、それをチーム内に周知し、その操作の実行中は短時間ではあるがリポジトリをフリーズし、終了後は全員に対してリポジトリを再度 clone するように通知する必要があります。
shallow-clone の代替: 単一ブランチのクローン
2012 年 4 月にリリースされた git 1.7.10 から、次のようにクローンの対象を単一ブランチに制限することができるようになりました:
git clone URL --branch branch_name --single-branch [folder]
この方法は、特にブランチが長くて分岐が多い場合やブランチの数が多い場合に有用です。ブランチの数が少なくてしかもそれらが似通っている場合はこの方法の効果は小さいです。
巨大なバイナリ データを含むリポジトリの取り扱い
第二の種類の巨大リポジトリは、巨大なバイナリ データを含むコードベースからなるリポジトリです。例えばゲーム開発チームは巨大な三次元モデルを扱わなければならないし、ウェブ開発チームは画像の生データを記録しなければならないことがあります。CAD 開発チームはバイナリ派生物の処理や記録を行う場合が生じます。 即ち、git を利用するさまざまな分野のソフトウェア開発チームがこの問題に直面するのです。
Git のバイナリ データ処理能力に特段の問題があるわけではないですが、その点で Git が特別優れているわけでもありません。デフォルトでは git はバイナリ データについて一連のバージョンのすべてを圧縮して格納しますが、これはバージョン数が多い場合は明らかに得策ではありません。
ガベージコレクション git gc の実行や.gitattributes において指定したいくつかのバイナリタイプに対して delta 圧縮 (差分圧縮) を使用したコミットを適用するなど、この状況を改善する基本的な手法がいくつかあります。
しかし王道は存在しないため、バイナリ データの特性を個々に考慮することが重要となります。例として、確認するべき点を 3 つ挙げます (Stefan Saasen に意見を頂きました):
- ある種のメタデータヘッダーに限らず、大きな変更のあるバイナリファイルについては多くの場合 delta 圧縮は有効ではなく、したがって余計な delta 圧縮動作がリパック時に発生することを防止するために delta off とすることを推奨します。
- 上のシナリオに従うと、それらのファイルに対しては zlib 圧縮もあまり有効ではなく、したがって core.compression 0 や core.loosecompression 0 を指定して圧縮を無効にしてもよいです。ただし、これは圧縮が有効なすべての非バイナリファイルに悪影響を与える可能性のあるグローバル設定であるため、この推奨設定はバイナリ データを別のリポジトリに分離した場合のみ有用と言えます。
- なお、git gc は「重複した」ルーズオブジェクトを 1 個のパックファイルに変換しますが、ここでも結果として生成されるパックファイルの圧縮効果は小さいと思われることに留意してください。
- core.bigFileThreshold の微調整。 .gitattributes における設定がない場合は512 MiB より大きなファイルは delta 圧縮されないため、この方法を試す価値はあるでしょう。
テクニック 1: sparse checkout
sparse checkout (Git 1.7.0] 以降において提供) は、バイナリ データの問題に対処するためのちょっとした助けになります。このテクニックではチェックアウトするフォルダーが指定できるため、作業ディレクトリはクリーンに保たれます。残念ながらこれはローカルリポジトリ全体のサイズには影響しないものの、フォルダーツリーのサイズが巨大なものになっている場合は有用なテクニックです。
これに関連するコマンドの例を挙げましょう (クレジット):
- リポジトリ全体を一度だけクローンする: git clone
- sparse checkout を有効にする: git config core.sparsecheckout true
-
データフォルダーを除外し、必要なフォルダーのみを指定する:
echo src/ ? .git/info/sparse-checkout
指定に従ってツリーを読み込む: git read-tree -m -u HEAD
上記操作を行った後は通常の git コマンドを使用することができますが、ただし作業ディレクトリには上で指定したフォルダーのみが含まれます。
テクニック 2: サブモジュールの使用
巨大なバイナリデータフォルダーを取り扱う別の方法として、それらを別のリポジトリに分離し、サブモジュールを使用してメインのリポジトリにプルするという手法があります。この手法では、データをアップデートしたときの管理も可能です。サブモジュールについては次の記事、 Git Submodules: コアコンセプト、ワークフロー、コツ および Git Submodule の代替: Git Subtreeが参考となるでしょう。
submodules を利用する方法をとる場合は、巨大バイナリファイルの問題に対して参考となるいくつかのアプローチを説明した complexities of handling project dependencies も参照するとよいでしょう。
テクニック 3: git annex または git-bigfiles の活用
git においてバイナリ データを取り扱う第三のオプションは適切なサードパーティー製拡張ツールを利用することです。
最初に取り上げたいのが git-annex で、ファイルの内容をリポジトリにチェックすることなくバイナリファイルの管理が可能なツールです。git-annex ではファイルを特別な key-value ストアとして保存し、git にはシンボリックなリンクのみをチェックして通常のファイルのようにバージョン管理を行うことができます。使用方法は簡単であり、利用例も理解しやすいでしょう。
取り上げたい二つ目は git-bigfilesであり、これは非常に大きなファイルを扱うプロジェクトにおいて Git を利用する開発者を支援することを意図した git のフォークです。
結論
リポジトリの履歴が巨大なものになっていたとしても、また巨大なデータを取り扱わなければならないとしても、git のすばらしい機能が活用できないと諦める必要はありません。どちらの問題に対しても有用なソリューションがあるのです。
分散型バージョン管理システムの詳しいことは、
@durdn や@AtlDevtoolsで私や開発チームフォローしてください。
本稿は、Atlassian Blogs 日本語版の転載です。本文中の日時などはAtlassian Blogs 英語版での投稿当時のものですのでご了承ください。