LLVMプロジェクトのGitHubへの移行

現在の状況

私たちは2019年10月21日までにGitHubへの移行を完了する予定です。最新の情報とワークフローを移行する方法については、GitHub移行のステータスページを参照してください。

はじめに

これは、現在のリビジョン管理システムを、私たち自身がホストしているSubversionからGitHubに移行するという提案です。以下は、なぜそのような移行を提案するのか、そして人々(および検証インフラストラクチャ)がGitベースのLLVMでどのように作業を続けるかについての財政的および技術的な議論です。

この提案が対象としないこと

開発ポリシーの変更。

この提案は、ソースコードリポジトリのホスティングを、自社サーバーでホストされているSVNからGitHubでホストされているGitに移行することのみに関連しています。GitHubのissueトラッカー、プルリクエスト、コードレビューの使用は提案していません。

コントリビューターは、開発者ポリシーに基づいてオンデマンドでコミットアクセス権を取得し続けます。ただし、SVNのユーザー名/パスワードハッシュの代わりにGitHubアカウントが必要になります。

なぜGitなのか、そしてなぜGitHubなのか?

そもそもなぜ移行するのか?

この議論は、現在、私たちがボランティアベースでSubversionサーバーとGitミラーをホストしていることから始まりました。LLVM Foundationがサーバーをスポンサーし、限定的なサポートを提供していますが、できることには限りがあります。

ボランティアはシステム管理者ではなく、たまたまサーバーのホスティングについて少し知っているコンパイラエンジニアです。また、24時間年中無休のサポートはなく、SVNサーバーがダウンしているか応答しないために、継続的インテグレーションが壊れていることに気付いて目を覚ますことがあります。

私たちは、より優れたサービス(24時間年中無休の安定性、ディスク容量、Gitサーバー、コードブラウジング、フォーク機能など)を無料で提供する(GitHub、GitLab、BitBucketなどの)サービスの1つを利用する必要があります。

なぜGitなのか?

最近では、多くの新しいコーダーがGitから始め、多くの人がSVN、CVSなどを使用したことがありません。GitHubのようなウェブサイトは、オープンソースへの貢献の状況を変え、最初の貢献のコストを削減し、コラボレーションを促進しました。

Gitは、多くのLLVM開発者が使用しているバージョン管理でもあります。ソースはSVNサーバーに保存されていますが、これらの開発者はすでにGit-SVN統合を通じてGitを使用しています。

Gitを使用すると、次のことができます。

  • リモートサーバーに触れることなく、ローカルでコミット、スカッシュ、マージ、およびフォークできます。

  • ローカルブランチを維持し、複数の開発スレッドを有効にできます。

  • これらのブランチでコラボレーションできます(例:GitHub上のllvmの独自のフォークを通じて)。

  • インターネットにアクセスせずに、リポジトリの履歴(blame、log、bisect)を検査できます。

  • Gitホスティングサービスでリモートフォークとブランチを維持し、メインリポジトリに統合できます。

さらに、Gitは多くのOSSプロジェクトのバージョン管理システムに取って代わっているため、Gitの上に構築された多くのツールがあります。将来のツールは、最初に(そうでない場合は唯一)Gitをサポートする可能性があります。

なぜGitHubなのか?

GitHubは、GitLabやBitBucketと同様に、オープンソースプロジェクト向けの無料のコードホスティングを提供しています。これらのいずれも、現在私たちが持っているコードホスティングインフラストラクチャを置き換えることができます。

これらのサービスには、地域と負荷に応じてリポジトリの内容を監視、移行、改善、配布するための専任チームもあります。

GitHubには、GitLabとBitBucketよりも重要な利点が1つあります。リポジトリへの読み取り/書き込みSVNアクセスを提供します(https://github.com/blog/626-announcing-svn-support)。これにより、人々は、私たちのコードがまだ標準的にSVNリポジトリにあるかのように、移行後も作業を続けることができます。

さらに、GitHubにはすでに複数のLLVMミラーがあり、私たちのコミュニティの一部がすでにそこに落ち着いていることを示しています。

Gitでのリビジョン番号の管理について

現在のSVNリポジトリは、すべてのLLVMサブプロジェクトを互いに並べてホストしています。したがって、単一のリビジョン番号(例:r123456)は、すべてのLLVMサブプロジェクトの一貫したバージョンを識別します。

Gitは、シーケンシャルな整数リビジョン番号を使用するのではなく、各コミットを識別するためにハッシュを使用します。

シーケンシャルな整数リビジョン番号の損失は、Gitに関する過去の議論の難点でした。

  • 「私が最も気にかけている「ブランチ」はメインラインであり、(単調に増加するような数値で)「r1234で修正」と言う機能が失われるのは悲劇的な損失でしょう。」[LattnerRevNum]

  • 「私はこれらの結果を時間順にソートするのが好きで、時系列は明白である必要がありますが、タイムスタンプは非常に煩雑で、特定のチェックアウトが特定の結果セットと一致することを検証するのが困難です。」[TrickRevNum]

  • 「判読不能なバージョン番号による大きな回帰がまだあります。「Fixed in…」というBugzillaのトラフィック量からすると、それは些細な問題ではありません。」[JSonnRevNum]

  • 「シーケンシャルIDは、LNTとllvmlabバイセクションツールにとって重要です。」[MatthewsRevNum]

ただし、Gitは、この増加するリビジョン番号をエミュレートできます。git rev-list --count <commit-hash>。この識別子は単一のブランチ内でのみ一意ですが、これは、タプル(num, branch-name)が一意にコミットを識別することを意味します。

したがって、このリビジョン番号を使用して、たとえば、clang -vがユーザーフレンドリーなリビジョン番号(例:main-12345または4.0-5321)を報告するようにし、Gitのこの側面に関して上記で提起された異議に対処できます。

ブランチとマージはどうなりますか?

SVNとは対照的に、Gitではブランチが簡単になります。Gitのコミット履歴は、SVNの線形履歴からの逸脱であるDAGとして表されます。ただし、私たちは、標準的なGitリポジトリでのマージコミットを違法とするよう義務付けることを提案します。

残念ながら、GitHubは、そのようなポリシーを強制するためのサーバー側のフックをサポートしていません。私たちは、マージコミットのプッシュを避けるためにコミュニティに頼る必要があります。

GitHubにはステータスチェックと呼ばれる機能があります。ステータスチェックで保護されたブランチでは、プッシュが実行される前に、コミットを明示的に許可する必要があります。クライアント側で、プッシュされるコミットを許可する前に、履歴を実行およびチェックするプッシュ前フックを提供できます[statuschecks]。ただし、このソリューションはやや脆弱であり(すべての開発者マシンにインストールされたスクリプトをどのように更新しますか?)、リポジトリへのSVNアクセスを防ぎます。

コミットメールはどうなりますか?

各コミットのメールを送信するには、新しいボットが必要です。この提案では、コミットURL以外はメール形式は変更しません。

たたき台移行計画

ステップ1:移行前

  1. ドキュメントを更新して移行について言及し、人々が何が起こっているかを認識できるようにします。

  2. 現在のSVNリポジトリをミラーリングする、GitHubプロジェクトの読み取り専用バージョンを設定します。

  3. コミットメールと、傘下リポジトリの更新(マルチリポジトリが選択されている場合)、またはサブプロジェクトの読み取り専用Gitビュー(モノレポが選択されている場合)を実装するために必要なボットを追加します。

ステップ2:Git移行

  1. buildbotを更新して、GitHubリポジトリから更新とコミットを取得します。すべてのボットをこの時点で移行する必要はありませんが、インフラストラクチャのテストに役立ちます。

  2. Phabricatorを更新して、GitHubリポジトリからコミットを取得します。

  3. LNTとllvmlabを更新する必要があります。それらは、ブランチ全体で一意の単調増加整数に依存しています[MatthewsRevNum]

  4. ダウンストリームインテグレーターに、GitHubリポジトリからコミットを取得するように指示します。

  5. LLVMドキュメントの更新を確認して準備します。

この時点までは、開発者にとって何も変更されておらず、buildbotと他のインフラストラクチャの所有者にとっては多くの作業が必要になります。

すべての依存関係がクリアされ、すべての問題が解決されるまで、移行はここで一時停止します。

ステップ3:書き込みアクセス移行

  1. 開発者のGitHubアカウント情報を収集し、プロジェクトに追加します。

  2. SVNリポジトリを読み取り専用に切り替え、GitHubリポジトリへのプッシュを許可します。

  3. ドキュメントを更新します。

  4. GitをSVNにミラーリングします。

ステップ4:移行後

  1. SVNリポジトリをアーカイブします。

  2. viewvc/klaus/phabなどを指すLLVM Webサイトのリンクを更新して、代わりにGitHubを指すようにします。

GitHubリポジトリの説明

モノレポ

https://github.com/llvm/llvm-projectでホストされているLLVM gitリポジトリには、単一のソースツリーにすべてのサブプロジェクトが含まれています。多くの場合、モノレポと呼ばれ、現在のSVNリポジトリのエクスポートを模倣しており、各サブプロジェクトには独自のトップレベルディレクトリがあります。すべてのサブプロジェクトがツールチェーンの構築に使用されるわけではありません。たとえば、www/とtest-suite/はモノレポの一部ではありません。

すべてのサブプロジェクトを単一のチェックアウトに配置すると、プロジェクト間のリファクタリングが自然に簡単になります。

  • 新しいサブプロジェクトは、より優れた再利用やレイヤー化のために(例:LLVMへの依存関係を追加せずにランタイムでlibSupportやLITを使用できるように)簡単に分割できます。

  • LLVM で API を変更し、サブプロジェクトをアップグレードする作業は、常に単一のコミットで行われ、一時的なビルドの破損という一般的な原因を排除するように設計されます。

  • 単一のコミットで(例えばリファクタリング中に)サブプロジェクト間でコードを移動することで、コード変更履歴を追跡する際に正確な git blame が可能になります。

  • git grep に基づくツールは、サブプロジェクト間でネイティブに動作し、プロジェクト全体でのリファクタリングの機会を容易に見つけることができます(例えば、LLDB で最初に使われていたデータ構造を libSupport に移動して再利用するなど)。

  • すべてのソースが存在することで、API を変更する際に他のサブプロジェクトの保守が促進されます。

最後に、モノレポは、サブプロジェクトが同期して移動するという既存の SVN リポジトリの特性を維持し、単一のリビジョン番号(またはコミットハッシュ)によって、すべてのプロジェクトの開発状態を識別できます。

単一のサブプロジェクトをビルドする

単一のソースツリーが存在しますが、すべてのサブプロジェクトをまとめてビルドする必要はありません。単一のサブプロジェクトのビルドを構成するのは簡単です。

例えば

mkdir build && cd build
# Configure only LLVM (default)
cmake path/to/monorepo
# Configure LLVM and lld
cmake path/to/monorepo -DLLVM_ENABLE_PROJECTS=lld
# Configure LLVM and clang
cmake path/to/monorepo -DLLVM_ENABLE_PROJECTS=clang

未解決の疑問

読み取り専用のサブプロジェクトミラー

モノレポでは、既存の単一サブプロジェクトミラー(例えば https://git.llvm.org/git/compiler-rt.git)が今後も維持されるかどうかは未定です。

読み取り/書き込み SVN ブリッジ

GitHub はリポジトリの読み取り/書き込み SVN ブリッジをサポートしています。ただし、過去にこのブリッジが正しく動作しない問題があったため、今後もサポートされるかどうかは不明です。

モノレポの欠点

  • モノリシックリポジトリを使用すると、特に LLVM に依存しない libcxx や compiler-rt のようなランタイムに貢献している人にとって、オーバーヘッドが増加する可能性があります。現在、libcxx の新規クローンはわずか 15MB です(モノレポは 1GB です)。また、LLVM のコミット率が高いため、アップストリーム時に git push の衝突が頻繁に発生する可能性があります。影響を受ける貢献者は、SVN ブリッジまたは単一サブプロジェクトの Git ミラーを使用できる場合があります。ただし、これらのプロジェクトが今後も維持されるかどうかは未定です。

  • モノリシックリポジトリを使用すると、上記のディスク容量の問題により、スタンドアロンのサブプロジェクトに貢献していない場合でも、それを統合する人にとってオーバーヘッドが増加する可能性があります。サブプロジェクトの Git ミラーが利用可能であれば、この問題は解決します。

  • 既存の読み取り/書き込み SVN ベースのワークフローの維持は、GitHub SVN ブリッジに依存しており、これは追加の依存関係です。これを維持することは、私たちを GitHub に縛り付け、将来のワークフローの変更を制限する可能性があります。

ワークフロー

ワークフローの以前/以後

このセクションでは、エンドユーザーまたは開発者がさまざまなユースケースでリポジトリを操作する方法を示すことを目的として、ワークフローのいくつかの例を説明します。

コミット権限ありで単一プロジェクトをチェックアウト/クローンする

現在

# direct SVN checkout
svn co https://user@llvm.org/svn/llvm-project/llvm/trunk llvm
# or using the read-only Git view, with git-svn
git clone https://llvm.org/git/llvm.git
cd llvm
git svn init https://llvm.org/svn/llvm-project/llvm/trunk --username=<username>
git config svn-remote.svn.fetch :refs/remotes/origin/main
git svn rebase -l  # -l avoids fetching ahead of the git mirror.

コミットは、svn commit を使用するか、git commitgit svn dcommit のシーケンスで実行されます。

モノレポ バリアント

モノレポ バリアントでは、制約に応じていくつかのオプションがあります。まず、リポジトリ全体をクローンすることができます。

git clone https://github.com/llvm/llvm-project.git

この時点で、すべてのサブプロジェクト(llvm、clang、lld、lldb、…)が存在しますが、それらすべてをビルドする必要はありません。例えば、compiler-rt のみをビルドすることも可能です。この点では、現在 SVN で全てのプロジェクトをチェックアウトする人と違いはありません。

すべてのソースをチェックアウトしないようにするには、Git スパースチェックアウトを使用して他のディレクトリを隠すことができます。

git config core.sparseCheckout true
echo /compiler-rt > .git/info/sparse-checkout
git read-tree -mu HEAD

すべてのサブプロジェクトのデータは依然として .git ディレクトリにありますが、チェックアウトでは compiler-rt のみが表示されます。プッシュする前に、通常どおりフェッチとリベース (git pull –rebase) が必要になります。

フェッチすると、気にしていないサブプロジェクトへの変更がプルされる可能性が高いことに注意してください。スパースチェックアウトを使用している場合、他のプロジェクトのファイルはディスクに表示されません。唯一の効果は、コミットハッシュが変更されることです。

最後のフェッチでの変更がコミットに関連するかどうかは、以下を実行して確認できます。

git log origin/main@{1}..origin/main -- libcxx

このコマンドをスクリプトに隠すことで、git llvmpush がこれらのステップをすべて実行し、そのような依存関係のある変更が存在する場合にのみ失敗し、プッシュを妨げた変更をすぐに表示するようにできます。コマンドをすぐに繰り返すと、(ほぼ)確実にプッシュが成功します。SVN または git-svn では、競合が発生した場合を除き、コミット中に「リベース」が暗黙的に行われるため、このステップは不可能です。

コミット権限ありで複数のプロジェクトをチェックアウト/クローンする

特定の revision で llvm+clang+libcxx を組み立てる方法を見てみましょう。

現在

svn co https://llvm.dokyumento.jp/svn/llvm-project/llvm/trunk llvm -r $REVISION
cd llvm/tools
svn co https://llvm.dokyumento.jp/svn/llvm-project/clang/trunk clang -r $REVISION
cd ../projects
svn co https://llvm.dokyumento.jp/svn/llvm-project/libcxx/trunk libcxx -r $REVISION

または git-svn を使用して

git clone https://llvm.dokyumento.jp/git/llvm.git
cd llvm/
git svn init https://llvm.dokyumento.jp/svn/llvm-project/llvm/trunk --username=<username>
git config svn-remote.svn.fetch :refs/remotes/origin/main
git svn rebase -l
git checkout `git svn find-rev -B r258109`
cd tools
git clone https://llvm.dokyumento.jp/git/clang.git
cd clang/
git svn init https://llvm.dokyumento.jp/svn/llvm-project/clang/trunk --username=<username>
git config svn-remote.svn.fetch :refs/remotes/origin/main
git svn rebase -l
git checkout `git svn find-rev -B r258109`
cd ../../projects/
git clone https://llvm.dokyumento.jp/git/libcxx.git
cd libcxx
git svn init https://llvm.dokyumento.jp/svn/llvm-project/libcxx/trunk --username=<username>
git config svn-remote.svn.fetch :refs/remotes/origin/main
git svn rebase -l
git checkout `git svn find-rev -B r258109`

サブプロジェクトが増えるほど、リストは長くなることに注意してください。

モノレポ バリアント

リポジトリには、すべてのサブプロジェクトのソースが適切なリビジョンでネイティブに含まれているため、これは簡単です。

git clone https://github.com/llvm/llvm-project.git
cd llvm-projects
git checkout $REVISION

前述のように、この時点で clang、llvm、および libcxx は互いに並んでディレクトリに格納されます。

LLVM で API 変更をコミットし、サブプロジェクトを更新する

今日では、サブバージョンユーザーや git-svn ユーザーにとって一般的ではない(少なくとも文書化されていない)ものの、これは可能です。例えば、LLVM API を変更するときに、LLD や Clang を同じコミットで更新しようとする Git ユーザーはほとんどいません。

マルチレポ バリアントでは、これに対処していません。ユーザーは、個々のリポジトリごとに別々にコミットしてプッシュする必要があります。ユーザーがコミットメッセージに特別なトークンを追加し、それによって傘下のリポジトリのアップデーターボットがそれらをすべて単一のリビジョンにグループ化するプロトコルを確立することは可能です。

モノレポ バリアントは、これをネイティブに処理します。

ローカル開発または実験のためのブランチング/スタッシュ/更新

現在

SVN ではこのユースケースは許可されていませんが、現在 git-svn を使用している開発者はこれを行うことができます。複数のサブプロジェクトを扱う場合、実際にはどのような意味があるかを見てみましょう。

リポジトリを trunk の先端に更新するには

git pull
cd tools/clang
git pull
cd ../../projects/libcxx
git pull

新しいブランチを作成するには

git checkout -b MyBranch
cd tools/clang
git checkout -b MyBranch
cd ../../projects/libcxx
git checkout -b MyBranch

ブランチを切り替えるには

git checkout AnotherBranch
cd tools/clang
git checkout AnotherBranch
cd ../../projects/libcxx
git checkout AnotherBranch

モノレポ バリアント

すべてが単一のリポジトリにあるため、通常の Git コマンドで十分です。

リポジトリを trunk の先端に更新するには

git pull

新しいブランチを作成するには

git checkout -b MyBranch

ブランチを切り替えるには

git checkout AnotherBranch

バイセクト

開発者が clang(または lld、lldb、…)のバグを探していると仮定します。

現在

SVN には組み込みのバイセクトサポートはありませんが、サブプロジェクト間での単一のリビジョンにより、スクリプト化することが可能です。

既存の Git 読み取り専用ビューのリポジトリを使用すると、llvm リポジトリに対してネイティブの Git バイセクトスクリプトを使用し、いくつかのスクリプトを使用して clang リポジトリを llvm リビジョンに合わせて同期させることができます。

モノレポ バリアント

モノレポでのバイセクトは簡単で、上記の点と非常によく似ていますが、バイセクトスクリプトに git submodule update ステップを含める必要がない点が異なります。

同じ例で、clang-3.9 がクラッシュするが clang-3.8 がパスする回帰を引き起こすコミットを見つけると、次のようになります。

git bisect start releases/3.9.x releases/3.8.x
git bisect run ./bisect_script.sh

bisect_script.sh スクリプトは次のようになります。

#!/bin/sh
cd $BUILD_DIR

ninja clang || exit 125   # an exit code of 125 asks "git bisect"
                          # to "skip" the current commit

./bin/clang some_crash_test.cpp

また、モノレポは複数のプロジェクトにまたがるコミットの更新を処理するため、コミットが LLVM で API を変更し、別の後のコミットが clang でビルドを「修正」するようなビルドの失敗が発生する可能性が低くなります。

ローカルブランチをモノレポに移動する

既存の LLVM git ミラーに対して開発を行ってきたと仮定します。「最終的なモノレポ」に移行したい 1 つ以上の git ブランチがあります。

このようなブランチを移行する最も簡単な方法は、migrate-downstream-fork.py ツール(https://github.com/jyknight/llvm-git-migration)を使用することです。

基本的な移行

migrate-downstream-fork.py の基本的な手順は、Python スクリプトに記載されており、以下でより一般的なレシピに展開します。

# Make a repository which will become your final local mirror of the
# monorepo.
mkdir my-monorepo
git -C my-monorepo init

# Add a remote to the monorepo.
git -C my-monorepo remote add upstream/monorepo https://github.com/llvm/llvm-project.git

# Add remotes for each git mirror you use, from upstream as well as
# your local mirror.  All projects are listed here but you need only
# import those for which you have local branches.
my_projects=( clang
              clang-tools-extra
              compiler-rt
              debuginfo-tests
              libcxx
              libcxxabi
              libunwind
              lld
              lldb
              llvm
              openmp
              polly )
for p in ${my_projects[@]}; do
  git -C my-monorepo remote add upstream/split/${p} https://github.com/llvm-mirror/${p}.git
  git -C my-monorepo remote add local/split/${p} https://my.local.mirror.org/${p}.git
done

# Pull in all the commits.
git -C my-monorepo fetch --all

# Run migrate-downstream-fork to rewrite local branches on top of
# the upstream monorepo.
(
   cd my-monorepo
   migrate-downstream-fork.py \
     refs/remotes/local \
     refs/tags \
     --new-repo-prefix=refs/remotes/upstream/monorepo \
     --old-repo-prefix=refs/remotes/upstream/split \
     --source-kind=split \
     --revmap-out=monorepo-map.txt
)

# Octopus-merge the resulting local split histories to unify them.

# Assumes local work on local split mirrors is on main (and
# upstream is presumably represented by some other branch like
# upstream/main).
my_local_branch="main"

git -C my-monorepo branch --no-track local/octopus/main \
  $(git -C my-monorepo merge-base refs/remotes/upstream/monorepo/main \
                                  refs/remotes/local/split/llvm/${my_local_branch})
git -C my-monorepo checkout local/octopus/${my_local_branch}

subproject_branches=()
for p in ${my_projects[@]}; do
  subproject_branch=${p}/local/monorepo/${my_local_branch}
  git -C my-monorepo branch ${subproject_branch} \
    refs/remotes/local/split/${p}/${my_local_branch}
  if [[ "${p}" != "llvm" ]]; then
    subproject_branches+=( ${subproject_branch} )
  fi
done

git -C my-monorepo merge ${subproject_branches[@]}

for p in ${my_projects[@]}; do
  subproject_branch=${p}/local/monorepo/${my_local_branch}
  git -C my-monorepo branch -d ${subproject_branch}
done

# Create local branches for upstream monorepo branches.
for ref in $(git -C my-monorepo for-each-ref --format="%(refname)" \
                 refs/remotes/upstream/monorepo); do
  upstream_branch=${ref#refs/remotes/upstream/monorepo/}
  git -C my-monorepo branch upstream/${upstream_branch} ${ref}
done

上記の手順で、次のような状態になります。

U1 - U2 - U3 <- upstream/main
  \   \    \
   \   \    - Llld1 - Llld2 -
    \   \                    \
     \   - Lclang1 - Lclang2-- Lmerge <- local/octopus/main
      \                      /
       - Lllvm1 - Lllvm2-----

分岐した各コンポーネントは、モノレポの上に書き直され、すべてのコンポーネントは巨大なタコのマージによって統合されます。

追加のアクティブなローカルブランチを保持する必要がある場合は、my_local_branch への代入に続く上記の操作を、ブランチごとに行う必要があります。参照パスは、ローカルブランチを対応するアップストリームブランチにマップするように更新する必要があります。ローカルブランチに対応するアップストリームブランチがない場合、local/octopus/<ローカル ブランチ> の作成で、ルートコミットを特定するために git-merge-base を使用する必要はありません。適切なコンポーネントブランチ(例えば llvm/local_release_X)から分岐することもできます。

ローカル履歴の圧縮

タコマージは多くの場合、最適ではありません。あるコンポーネントの履歴を遡ると、他のコンポーネントはビルドできない可能性が高い履歴に固定されるためです。

一部のダウンストリームユーザーは、サブプロジェクトへのコミット順を、「傘」プロジェクトのようなもので追跡しています。これは、上記で提案したマルチリポジトリの傘と同様に、プロジェクトの Git ミラーをサブモジュールとしてインポートします。このような傘リポジトリは、次のようになります。

 UM1 ---- UM2 -- UM3 -- UM4 ---- UM5 ---- UM6 ---- UM7 ---- UM8 <- main
 |        |             |        |        |        |        |
Lllvm1   Llld1         Lclang1  Lclang2  Lllvm2   Llld2     Lmyproj1

縦線は、プロジェクトミラー内の特定のローカルコミットに対するサブモジュールの更新を表します。この例では、UM3 はサブモジュールの更新ではない、ローカルの傘リポジトリの状態のコミットです。おそらく README ファイルやプロジェクトのビルドスクリプトの更新でしょう。コミット UM8 は、ローカルプロジェクト myproj のサブモジュールを更新しています。

https://github.com/greened/llvm-git-migration/tree/zip にあるツール zip-downstream-fork.py を使用すると、傘リポジトリの履歴を、サブモジュールの更新によって暗示される順序でコミットされたモノリポジトリベースの履歴に変換できます。

U1 - U2 - U3 <- upstream/main
 \    \    \
  \    -----\---------------                                    local/zip--.
   \         \              \                                               |
  - Lllvm1 - Llld1 - UM3 -  Lclang1 - Lclang2 - Lllvm2 - Llld2 - Lmyproj1 <-'

U* コミットは、モノリポジトリのメインブランチへの上流コミットを表します。ローカルの UM* コミット内の各サブモジュール更新は、ローカルコミットでのサブプロジェクトツリーを取り込みました。L*1 コミットのツリーは、上流からのマージを表します。これにより、U* コミットから、対応する書き換えられた L*1 コミットへのエッジが生成されます。L*2 コミットは、上流からのマージを実行しませんでした。

U2 から Lclang1 へのマージは冗長に見えるかもしれませんが、例えば、U3 が上流の clang でいくつかのファイルを変更した場合、Llld1 コミットの後に出現する Lclang1 コミットは、実際には上流の clang 履歴で *より早い* clang ツリーを表します。local/zip ブランチに傘リポジトリの正確な状態を表したいので、エッジ U2 -> Lclang1 は、Lclang1 での clang のツリーが実際にどのように見えるかを視覚的に示すものです。

それでも、エッジ U3 -> Llld1 は、将来の上流からのマージで問題になる可能性があります。git は、U3 からすでにマージしたと認識しますが、clang ツリーの状態を除いて、実際にはマージされています。考えられる軽減策の 1 つは、U2U3 の間で clang を手動で差分し、それらの更新を local/zip に適用することです。もう 1 つの、おそらくより簡単な戦略は、ダウンストリームブランチでのローカル作業をフリーズし、zip-downstream-fork.py を実行する前に、最新の上流からすべてのサブモジュールをマージすることです。ダウンストリームが、介在するローカルコミットなしで、各プロジェクトを上流からロックステップでマージした場合、特別な操作なしで問題なく動作するはずです。これは一般的なケースであると予想されます。

Lclang1 の clang 外部のツリーは、傘履歴に参加していないすべての上流プロジェクトがコミット U3 を尊重する状態にあるため、U3 での状態を表します。llvm および lld のツリーは、それぞれコミット Lllvm1 および Llld1 を正しく表す必要があります。

コミット UM3 はサブモジュールに関連しないファイルを変更しており、それらを配置する場所が必要です。モノリポジトリのルートディレクトリに配置することは、モノリポジトリ内のファイルと競合する可能性があるため、一般的に安全ではありません。モノリポジトリ内の local ディレクトリに配置すると仮定しましょう。

例 1: 傘がモノリポジトリのように見える

この例では、各サブプロジェクトが、モノリポジトリと同様に、傘の独自のトップレベルディレクトリに表示されると仮定します。また、ディレクトリ myproj のファイルを local/myproj に表示させると仮定します。

上記の migrate-downstream-fork.py の実行を考慮すると、zip された履歴を作成する手順は次のとおりです。

# Import any non-LLVM repositories the umbrella references.
git -C my-monorepo remote add localrepo \
                              https://my.local.mirror.org/localrepo.git
git fetch localrepo

subprojects=( clang clang-tools-extra compiler-rt debuginfo-tests libclc
              libcxx libcxxabi libunwind lld lldb llgo llvm openmp
              parallel-libs polly pstl )

# Import histories for upstream split projects (this was probably
# already done for the ``migrate-downstream-fork.py`` run).
for project in ${subprojects[@]}; do
  git remote add upstream/split/${project} \
                 https://github.com/llvm-mirror/${subproject}.git
  git fetch umbrella/split/${project}
done

# Import histories for downstream split projects (this was probably
# already done for the ``migrate-downstream-fork.py`` run).
for project in ${subprojects[@]}; do
  git remote add local/split/${project} \
                 https://my.local.mirror.org/${subproject}.git
  git fetch local/split/${project}
done

# Import umbrella history.
git -C my-monorepo remote add umbrella \
                              https://my.local.mirror.org/umbrella.git
git fetch umbrella

# Put myproj in local/myproj
echo "myproj local/myproj" > my-monorepo/submodule-map.txt

# Rewrite history
(
  cd my-monorepo
  zip-downstream-fork.py \
    refs/remotes/umbrella \
    --new-repo-prefix=refs/remotes/upstream/monorepo \
    --old-repo-prefix=refs/remotes/upstream/split \
    --revmap-in=monorepo-map.txt \
    --revmap-out=zip-map.txt \
    --subdir=local \
    --submodule-map=submodule-map.txt \
    --update-tags
 )

 # Create the zip branch (assuming umbrella main is wanted).
 git -C my-monorepo branch --no-track local/zip/main refs/remotes/umbrella/main

傘に LLVM 以外のリポジトリへのサブモジュールがある場合、zip-downstream-fork.py はコミットを書き換えるためにそれらについて知っておく必要があります。そのため、上記の手順の最初の手順は、そのようなリポジトリからコミットをフェッチすることです。

--update-tags を指定すると、ツールは、zip された履歴にインライン化されたサブモジュールコミットを指すアノテーション付きタグを移行します。傘が、それを指すタグを持つ上流コミットを取り込んだ場合、そのタグは移行されますが、ほぼ間違いなくこれは意図したものではありません。タグは書き換え後に元のコミットに戻すか、--update-tags オプションを破棄して、ローカルタグを手動で移行することができます。

例 2: ネストされたソースレイアウト

このツールは、ネストされたサブモジュール (例: llvm は傘内のサブモジュールで、clang は llvm 内のサブモジュール) を処理します。ファイル submodule-map.txt は、1 行に 1 組のペアのリストです。最初のペアの項目は、傘リポジトリ内のサブモジュールへのパスを記述します。2 番目のペアの項目は、そのサブモジュールのツリーが zip された履歴に書き込まれるパスを記述します。

傘リポジトリが実際には llvm リポジトリであり、「ネストされたソース」レイアウト (tools/clang 内の clang など) でサブモジュールを持つとしましょう。また、projects/myproj が、いくつかのダウンストリームリポジトリを指すサブモジュールであるとしましょう。サブモジュールマップファイルは、次のようになります (myproj は以前と同じようにマッピングしたいとします)。

tools/clang clang
tools/clang/tools/extra clang-tools-extra
projects/compiler-rt compiler-rt
projects/debuginfo-tests debuginfo-tests
projects/libclc libclc
projects/libcxx libcxx
projects/libcxxabi libcxxabi
projects/libunwind libunwind
tools/lld lld
tools/lldb lldb
projects/openmp openmp
tools/polly polly
projects/myproj local/myproj

サブモジュールパスがマップに表示されない場合、ツールはモノリポジトリの同じ場所に配置する必要があると想定します。つまり、傘で「ネストされたソース」レイアウトを使用する場合は、傘内のすべてのプロジェクト (llvm を除く) のマップエントリを *必ず* 提供する必要があります。そうしないと、サブモジュール更新からのツリーが zip された履歴の llvm の下に表示されます。

llvm 自体が傘であるため、--subdir を使用して、そのコンテンツを zip された履歴の llvm に書き込みます。

# Import any non-LLVM repositories the umbrella references.
git -C my-monorepo remote add localrepo \
                              https://my.local.mirror.org/localrepo.git
git fetch localrepo

subprojects=( clang clang-tools-extra compiler-rt debuginfo-tests libclc
              libcxx libcxxabi libunwind lld lldb llgo llvm openmp
              parallel-libs polly pstl )

# Import histories for upstream split projects (this was probably
# already done for the ``migrate-downstream-fork.py`` run).
for project in ${subprojects[@]}; do
  git remote add upstream/split/${project} \
                 https://github.com/llvm-mirror/${subproject}.git
  git fetch umbrella/split/${project}
done

# Import histories for downstream split projects (this was probably
# already done for the ``migrate-downstream-fork.py`` run).
for project in ${subprojects[@]}; do
  git remote add local/split/${project} \
                 https://my.local.mirror.org/${subproject}.git
  git fetch local/split/${project}
done

# Import umbrella history.  We want this under a different refspec
# so zip-downstream-fork.py knows what it is.
git -C my-monorepo remote add umbrella \
                               https://my.local.mirror.org/llvm.git
git fetch umbrella

# Create the submodule map.
echo "tools/clang clang" > my-monorepo/submodule-map.txt
echo "tools/clang/tools/extra clang-tools-extra" >> my-monorepo/submodule-map.txt
echo "projects/compiler-rt compiler-rt" >> my-monorepo/submodule-map.txt
echo "projects/debuginfo-tests debuginfo-tests" >> my-monorepo/submodule-map.txt
echo "projects/libclc libclc" >> my-monorepo/submodule-map.txt
echo "projects/libcxx libcxx" >> my-monorepo/submodule-map.txt
echo "projects/libcxxabi libcxxabi" >> my-monorepo/submodule-map.txt
echo "projects/libunwind libunwind" >> my-monorepo/submodule-map.txt
echo "tools/lld lld" >> my-monorepo/submodule-map.txt
echo "tools/lldb lldb" >> my-monorepo/submodule-map.txt
echo "projects/openmp openmp" >> my-monorepo/submodule-map.txt
echo "tools/polly polly" >> my-monorepo/submodule-map.txt
echo "projects/myproj local/myproj" >> my-monorepo/submodule-map.txt

# Rewrite history
(
  cd my-monorepo
  zip-downstream-fork.py \
    refs/remotes/umbrella \
    --new-repo-prefix=refs/remotes/upstream/monorepo \
    --old-repo-prefix=refs/remotes/upstream/split \
    --revmap-in=monorepo-map.txt \
    --revmap-out=zip-map.txt \
    --subdir=llvm \
    --submodule-map=submodule-map.txt \
    --update-tags
 )

 # Create the zip branch (assuming umbrella main is wanted).
 git -C my-monorepo branch --no-track local/zip/main refs/remotes/umbrella/main

zip-downstream-fork.py の上部にあるコメントには、ツールの仕組みと、その操作に関するさまざまな意味合いが詳しく説明されています。

ローカルリポジトリのインポート

LLVM エコシステムと統合し、新しいツールで拡張する追加のリポジトリがある場合があります。このようなリポジトリが LLVM と密接に結びついている場合は、それらをモノリポジトリのローカルミラーにインポートすることが理にかなっている場合があります。

このようなリポジトリが、上記の zip 処理中に使用された傘リポジトリに参加した場合、それらは自動的にモノリポジトリに追加されます。傘の設定に参加しないダウンストリームリポジトリの場合、https://github.com/greened/llvm-git-migration/tree/import にある import-downstream-repo.py ツールが、それらをモノリポジトリに入れるのに役立ちます。手順は次のとおりです。

# Import downstream repo history into the monorepo.
git -C my-monorepo remote add myrepo https://my.local.mirror.org/myrepo.git
git fetch myrepo

my_local_tags=( refs/tags/release
                refs/tags/hotfix )

(
  cd my-monorepo
  import-downstream-repo.py \
    refs/remotes/myrepo \
    ${my_local_tags[@]} \
    --new-repo-prefix=refs/remotes/upstream/monorepo \
    --subdir=myrepo \
    --tag-prefix="myrepo-"
 )

 # Preserve release branches.
 for ref in $(git -C my-monorepo for-each-ref --format="%(refname)" \
                refs/remotes/myrepo/release); do
   branch=${ref#refs/remotes/myrepo/}
   git -C my-monorepo branch --no-track myrepo/${branch} ${ref}
 done

 # Preserve main.
 git -C my-monorepo branch --no-track myrepo/main refs/remotes/myrepo/main

 # Merge main.
 git -C my-monorepo checkout local/zip/main  # Or local/octopus/main
 git -C my-monorepo merge myrepo/main

LLVM プロジェクトのリリースとロックステップしていた場合、たとえば myrepo のリリースブランチなど、他の対応するブランチをマージすることもできます。

--tag-prefix は、import-downstream-repo.py に、指定されたプレフィックスでアノテーション付きタグの名前を変更するように指示します。fast_filter_branch.py の制限により、アノテーションなしのタグの名前を変更することはできません (fast_filter_branch.py はそれらをタグではなくブランチと見なします)。上流のモノリポジトリのタグは「llvmorg-」プレフィックスで書き換えられているため、名前の競合は問題になりません。--tag-prefix を使用すると、どのタグがさまざまなインポートされたリポジトリに対応するかをより明確に示すことができます。

次のリポジトリ履歴を考えます。

R1 - R2 - R3 <- main
     ^
     |
  release/1

上記の手順の結果、次のような履歴になります。

U1 - U2 - U3 <- upstream/main
 \    \    \
  \    -----\---------------                                         local/zip--.
   \         \              \                                                    |
  - Lllvm1 - Llld1 - UM3 -  Lclang1 - Lclang2 - Lllvm2 - Llld2 - Lmyproj1 - M1 <-'
                                                                           /
                                                               R1 - R2 - R3  <-.
                                                                    ^           |
                                                                    |           |
                                                             myrepo-release/1   |
                                                                                |
                                                                   myrepo/main--'

コミット R1R2、および R3 には、myrepo の blob *のみ* を含むツリーがあります。myrepo からのコミットを、ローカルプロジェクトブランチ (たとえば、上記の llvm1llvm2 などとインターリーブ) とインターリーブする必要があり、myrepo が傘リポジトリに表示されない場合は、新しいツールを開発する必要があります。このようなツールを作成するには、次のものが必要です。

  1. fast_filter_branch.py を変更して、独自に生成するのではなく、revlist を直接オプションで受け入れるようにします。

  2. いくつかの基準に基づいてローカルコミットのインターリーブされた順序を生成するツールを作成します(zip-downstream-fork.py は、傘履歴を基準として使用します)。

  3. このような順序を生成し、それを revlist として fast_filter_branch.py に供給します。

また、マージコミットを処理する際には、そのようなコミットの親が正しく移行されるように注意する必要があります。

ローカルモノリポジトリのスクラブ

移行、zip圧縮、インポートがすべて完了したら、クリーンアップの段階です。Pythonツールはgit-fast-importを使用するため、多くの不要なものが残ります。新しいモノレポミラーをできるだけ小さくしたいので、そのための1つの方法を以下に示します。

git -C my-monorepo checkout main

# Delete branches we no longer need.  Do this for any other branches
# you merged above.
git -C my-monorepo branch -D local/zip/main || true
git -C my-monorepo branch -D local/octopus/main || true

# Remove remotes.
git -C my-monorepo remote remove upstream/monorepo

for p in ${my_projects[@]}; do
  git -C my-monorepo remote remove upstream/split/${p}
  git -C my-monorepo remote remove local/split/${p}
done

git -C my-monorepo remote remove localrepo
git -C my-monorepo remote remove umbrella
git -C my-monorepo remote remove myrepo

# Add anything else here you don't need.  refs/tags/release is
# listed below assuming tags have been rewritten with a local prefix.
# If not, remove it from this list.
refs_to_clean=(
  refs/original
  refs/remotes
  refs/tags/backups
  refs/tags/release
)

git -C my-monorepo for-each-ref --format="%(refname)" ${refs_to_clean[@]} |
  xargs -n1 --no-run-if-empty git -C my-monorepo update-ref -d

git -C my-monorepo reflog expire --all --expire=now

# fast_filter_branch.py might have gc running in the background.
while ! git -C my-monorepo \
  -c gc.reflogExpire=0 \
  -c gc.reflogExpireUnreachable=0 \
  -c gc.rerereresolved=0 \
  -c gc.rerereunresolved=0 \
  -c gc.pruneExpire=now \
  gc --prune=now; do
  continue
done

# Takes a LOOOONG time!
git -C my-monorepo repack -A -d -f --depth=250 --window=250

git -C my-monorepo prune-packed
git -C my-monorepo prune

これで、整理されたモノレポが完成しました。それをGitサーバーにアップロードして、ハッピーハッキング!

参考文献