將 LLVM 專案移至 GitHub

目前狀態

我們計劃在 2019 年 10 月 21 日前完成遷移至 GitHub 的工作。有關最新更新和如何遷移工作流程的說明,請參閱 GitHub 遷移 狀態頁面

簡介

這是一個將我們目前的版本控制系統從我們自行託管的 Subversion 遷移至 GitHub 的提案。以下是有關我們為何提出此遷移的財務和技術論點,以及人們(和驗證基礎設施)將如何繼續使用基於 Git 的 LLVM。

本提案的關事項

變更開發政策。

本提案僅與將我們的原始碼儲存庫的託管從我們自己伺服器上託管的 SVN 遷移至 GitHub 上託管的 Git 有關。我們不建議使用 GitHub 的議題追蹤器、拉取請求或程式碼審查。

貢獻者將繼續根據開發者政策按需獲得提交權限,但需要 GitHub 帳戶而不是 SVN 使用者名稱/密碼雜湊。

為何選擇 Git,為何選擇 GitHub?

為何要遷移?

之所以開始討論這個問題,是因為我們目前是在自願服務的基礎上託管我們自己的 Subversion 伺服器和 Git 鏡像。LLVM 基金會贊助伺服器並提供有限的支援,但它能做的事情只有這些。

志願者本身並不是系統管理員,而是碰巧對託管伺服器有所了解的編譯器工程師。我們也沒有全天候的支援,而且我們有時會在醒來時發現持續整合壞掉了,因為 SVN 伺服器當機或沒有回應。

我們應該利用其中一種服務(GitHub、GitLab 和 BitBucket 等),這些服務免費提供更好的服務(全天候穩定性、磁碟空間、Git 伺服器、程式碼瀏覽、分支功能等)。

為何選擇 Git?

如今,許多新的程式設計師都是從 Git 開始的,而且很多人從未用過 SVN、CVS 或其他任何東西。像 GitHub 這樣的網站改變了開源貢獻的格局,降低了首次貢獻的成本,並促進了協作。

Git 也是許多 LLVM 開發者使用的版本控制系統。儘管原始碼儲存在 SVN 伺服器中,但這些開發者已經透過 Git-SVN 整合使用 Git。

Git 允許您

  • 在不觸碰遠端伺服器的情況下,在本地進行提交、壓縮、合併和分支。

  • 維護本地分支,實現多線程開發。

  • 在這些分支上協作(例如,透過您自己在 GitHub 上的 LLVM 分支)。

  • 在沒有網際網路連線的情況下檢查儲存庫歷史記錄(追溯、日誌、二分搜尋)。

  • 在 Git 託管服務上維護遠端分支和分支,並整合回主儲存庫。

此外,由於 Git 似乎正在取代許多開源專案的版本控制系統,因此有許多工具是建立在 Git 之上的。 未來工具可能優先(甚至僅)支援 Git。

為什麼選擇 GitHub?

GitHub 與 GitLab 和 BitBucket 一樣,為開源專案提供免費的程式碼託管。 任何一個都可以取代我們今天擁有的程式碼託管基礎設施。

這些服務還擁有一個專門的團隊,負責根據地區和負載監控、遷移、改進和分發儲存庫的內容。

GitHub 相較於 GitLab 和 BitBucket 有一個重要的優勢:它提供對儲存庫的讀寫 SVN 存取權 (https://github.com/blog/626-announcing-svn-support)。 這將使人們能夠在遷移後繼續工作,就像我們的程式碼仍然規範地位於 SVN 儲存庫中一樣。

此外,GitHub 上已經有多個 LLVM 鏡像,這表明我們的一部分社群已經在那裡安頓下來。

關於使用 Git 管理版本號碼

目前的 SVN 儲存庫將所有 LLVM 子專案託管在一起。 因此,單個版本號碼(例如 r123456)標識所有 LLVM 子專案的一致版本。

Git 不使用連續的整數版本號碼,而是使用雜湊來標識每次提交。

在過去關於 Git 的討論中,連續整數版本號碼的丟失一直是一個症結

  • 「我最關心的「分支」是主線,而失去說「在 r1234 中修復」(使用某種單調遞增的數字)的能力將是一個巨大的損失。」 [LattnerRevNum]

  • 「我喜歡按時間排序的結果,並且時間順序應該很明顯,但時間戳非常麻煩,並且難以驗證給定的結帳是否與給定的結果集匹配。」 [TrickRevNum]

  • 「版本號碼不可讀仍然是一個主要的問題。 考慮到 Bugzilla 中「已修復於...」的流量,這是一個不容忽視的問題。」 [JSonnRevNum]

  • 「順序 ID 對於 LNT 和 llvmlab 二分工具很重要。」 [MatthewsRevNum]

但是,Git 可以模擬這個遞增的版本號碼:git rev-list --count <commit-hash>。 此標識符僅在單個分支內是唯一的,但这意味着元组 (num, branch-name) 唯一地標識了一次提交。

因此,我們可以使用這個版本號碼來確保例如 clang -v 報告一個對使用者友好的版本號碼(例如 main-123454.0-5321),解決上面提到的關於 Git 這方面的问题。

分支和合併怎麼辦?

與 SVN 不同,Git 使分支變得容易。 Git 的提交歷史記錄表示為 DAG,這與 SVN 的線性歷史記錄不同。 但是,我們建議強制在我們規範的 Git 儲存庫中禁止合併提交。

不幸的是,GitHub 不支援伺服器端鉤子來執行此類策略。 我們必須依靠社群來避免推送合併提交。

GitHub 提供了一項稱為 狀態檢查 的功能:受 狀態檢查 保護的分支要求在推送發生之前明確允許提交。 我們可以在客戶端提供一個推送前鉤子,它將在允許推送提交之前運行並檢查歷史記錄 [statuschecks]。 然而,這種解決方案有點脆弱(如何更新安裝在每個開發人員機器上的腳本?)並且會阻止 SVN 存取儲存庫。

那提交電子郵件呢?

我們將需要一個新的機器人來為每次提交發送電子郵件。除了提交 URL 之外,此提案不會更改電子郵件格式。

初步遷移計畫

步驟 #1:遷移前

  1. 更新文件以提及遷移,以便人們了解正在發生的事情。

  2. 設定 GitHub 專案的唯讀版本,鏡像我們目前的 SVN 儲存庫。

  3. 新增必要的機器人來實作提交電子郵件,以及傘型儲存庫更新(如果選擇多儲存庫)或子專案的唯讀 Git 視圖(如果選擇單一儲存庫)。

步驟 #2:Git 遷移

  1. 更新建置機器人以從 GitHub 儲存庫取得更新和提交。並非所有機器人都必須在此時遷移,但這將有助於提供基礎設施測試。

  2. 更新 Phabricator 以從 GitHub 儲存庫取得提交。

  3. LNT 和 llvmlab 必須更新:它們依賴於跨分支的唯一單調遞增整數 [MatthewsRevNum]

  4. 指示下游整合者從 GitHub 儲存庫取得提交。

  5. 審查並準備 LLVM 文件的更新。

到目前為止,對於開發人員來說,一切都沒有改變,這只會讓建置機器人和其他的基礎設施擁有者增加大量工作。

遷移將在此暫停,直到所有依賴項都已清除且所有問題都已解決。

步驟 #3:寫入權限遷移

  1. 收集開發人員的 GitHub 帳戶資訊,並將其新增至專案。

  2. 將 SVN 儲存庫切換為唯讀,並允許推送至 GitHub 儲存庫。

  3. 更新文件。

  4. 將 Git 鏡像至 SVN。

步驟 #4:遷移後

  1. 封存 SVN 儲存庫。

  2. 將 LLVM 網站上指向 viewvc/klaus/phab 等的連結更新為指向 GitHub。

GitHub 儲存庫說明

單一儲存庫

託管於 https://github.com/llvm/llvm-project 的 LLVM git 儲存庫在單一原始程式碼樹中包含所有子專案。它通常被稱為單一儲存庫,並模擬目前 SVN 儲存庫的匯出,每個子專案都有自己的頂層目錄。並非所有子專案都用於建置工具鏈。例如,www/ 和 test-suite/ 不是單一儲存庫的一部分。

將所有子專案放在單一Checkout中,自然可以簡化跨專案的重構

  • 可以輕鬆地拆分新的子專案,以實現更好的重複使用和/或分層(例如,允許執行階段使用 libSupport 和/或 LIT,而無需新增對 LLVM 的依賴)。

  • 在單一提交中完成 LLVM 中的 API 變更和子專案升級,從而設計出一種常見的臨時建置中斷來源。

  • 在單一提交中跨子專案移動程式碼(例如在重構期間)可以在追蹤程式碼變更歷史記錄時實現準確的 git blame

  • 基於 git grep 的工具可以在子專案中原生運作,從而更容易找到跨專案的重構機會(例如,通過將資料結構移動到 libSupport 中來重複使用最初在 LLDB 中的資料結構)。

  • 擁有所有來源鼓勵在變更 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

未解決的問題

唯讀子專案鏡像

使用 Monorepo,尚未決定是否會繼續維護現有的單一子專案鏡像(例如 https://git.llvm.org/git/compiler-rt.git)。

讀/寫 SVN 橋接

GitHub 支援其存放庫的讀/寫 SVN 橋接。 然而,過去這個橋接在正常運作方面一直存在問題,所以不清楚這是否是未來會支援的功能。

Monorepo 的缺點

  • 使用單體式存放庫可能會增加獨立子專案貢獻者的負擔,特別是在 libcxx 和 compiler-rt 等不依賴 LLVM 的執行時期;目前,libcxx 的全新複製只有 15MB(相對於 monorepo 的 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 commitgit commitgit svn dcommit 的順序執行。

Monorepo 變體

使用 monorepo 變體,根據您的限制,有幾個選項。 首先,您可以複製整個存放庫

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 目錄中,但在您的 checkout 中,您只會看到 compiler-rt。在您推送之前,您需要像往常一樣先提取並重定基底 (git pull –rebase)。

請注意,當您提取時,您可能會提取到您不關心的子專案的變更。如果您使用的是稀疏 checkout,則來自其他專案的檔案將不會顯示在您的磁碟上。唯一的影響是您的提交雜湊會變更。

您可以透過執行以下命令來檢查上次提取中的變更是否與您的提交相關:

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

這個命令可以隱藏在腳本中,以便 git llvmpush 會執行所有這些步驟,僅在存在此類相依變更時才會失敗,並立即顯示阻止推送的變更。立即重複該命令(幾乎)肯定會導致推送成功。請注意,今天使用 SVN 或 git-svn 時,此步驟是不可能的,因為「重定基底」會在提交時隱式發生(除非發生衝突)。

具有提交權限的多個專案的 Checkout/Clone

讓我們看看如何在給定的修訂版中組裝 llvm+clang+libcxx。

目前

svn co https://llvm.dev.org.tw/svn/llvm-project/llvm/trunk llvm -r $REVISION
cd llvm/tools
svn co https://llvm.dev.org.tw/svn/llvm-project/clang/trunk clang -r $REVISION
cd ../projects
svn co https://llvm.dev.org.tw/svn/llvm-project/libcxx/trunk libcxx -r $REVISION

或者使用 git-svn

git clone https://llvm.dev.org.tw/git/llvm.git
cd llvm/
git svn init https://llvm.dev.org.tw/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.dev.org.tw/git/clang.git
cd clang/
git svn init https://llvm.dev.org.tw/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.dev.org.tw/git/libcxx.git
cd libcxx
git svn init https://llvm.dev.org.tw/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 變更並更新子專案

今天這是可能的,即使對於 subversion 使用者和 git-svn 使用者來說並不常見(至少沒有記錄)。例如,很少有 Git 使用者會嘗試在與變更 LLVM API 相同的提交中更新 LLD 或 Clang。

多儲存庫變體無法解決這個問題:必須在每個單獨的儲存庫中分別提交和推送。可以建立一個協定,讓使用者在他們的提交訊息中添加一個特殊標記,讓傘式儲存庫的更新機器人將所有提交訊息組合成一個單一修訂版。

單一儲存庫變體本身就能處理這個問題。

用於本地開發或實驗的分支/儲藏/更新

目前

SVN 不允許這種使用情況,但目前使用 git-svn 的開發人員可以做到。讓我們在實務中看看在處理多個子專案時這意味著什麼。

要將儲存庫更新到主幹的尖端

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 命令就足夠了,因為所有東西都在單一儲存庫中

要將儲存庫更新到主幹的尖端

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 鏡像進行開發。您希望將一個或多個 git 分支遷移到“最終單一儲存庫”。

遷移此類分支的最簡單方法是使用 https://github.com/jyknight/llvm-git-migrationmigrate-downstream-fork.py 工具。

基本遷移

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/<local branch> 不需要使用 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/zipzip-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 提交沒有進行任何來自上游的合併。

請注意,從 U2Lclang1 的合併看起來是多餘的,但是,例如,如果 U3 更改了上游 clang 中的某些檔案,則出現在 Llld1 提交之後的 Lclang1 提交實際上表示上游 clang 歷史記錄中*較早*的 clang 樹。 我們希望 local/zip 分支準確地表示我們的 umbrella 歷史記錄狀態,因此邊緣 U2 -> Lclang1 是 clang 的樹在 Lclang1 中實際樣子的視覺提醒。

儘管如此,邊緣 U3 -> Llld1 對於未來從上游合併可能會產生問題。 git 會認為我們已經從 U3 合併了,除了 clang 樹的狀態之外,我們確實合併了。 一種可能的解決方案是手動比較 U2U3 之間的 clang 並將這些更新套用到 local/zip。 另一個可能更簡單的策略是在執行 zip-downstream-fork.py 之前凍結下游分支上的本地工作,並從最新的上游合併所有子模組。 如果下游在没有任何中間本地提交的情況下從上游鎖定步驟合併每個專案,那麼應該可以正常進行,而無需任何特殊操作。 我們預計這將是常見的情況。

clang 之外的 Lclang1 的樹將表示 U3 的狀態,因為所有未參與 umbrella 歷史記錄的上游專案都應處於尊重提交 U3 的狀態。 llvm 和 lld 的樹應該分別正確表示提交 Lllvm1Llld1

提交 UM3 更改了與子模組無關的檔案,我們需要將它們放在某個地方。 通常將它們放在單一儲存庫根目錄中是不安全的,因為它們可能會與單一儲存庫中的檔案衝突。 讓我們假設我們希望將它們放在單一儲存庫中的目錄 local 中。

範例 1:Umbrella 看起來像單一儲存庫

對於此範例,我們假設每個子專案都出現在 umbrella 中自己的頂級目錄中,就像它們在單一儲存庫中一樣。 我們還假設我們希望目錄 myproj 中的檔案出現在 local/myproj 中。

根據上述執行 migrate-downstream-fork.py 的結果,以下是建立壓縮歷史記錄的流程

# 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

請注意,如果 umbrella 儲存庫包含指向非 LLVM 儲存庫的子模組,zip-downstream-fork.py 需要知道它們才能重寫提交。這就是為什麼上述第一步是從這些儲存庫擷取提交的原因。

使用 --update-tags 選項,該工具將遷移指向已內嵌到壓縮歷史記錄中的子模組提交的標註標籤。如果 umbrella 儲存庫提取了一個恰好有標籤指向它的上游提交,則該標籤將被遷移,這幾乎肯定不是我們想要的。標籤始終可以在重寫後移回其原始提交,或者可以捨棄 --update-tags 選項,然後手動遷移任何本地標籤。

範例 2:巢狀來源配置

該工具可以處理巢狀子模組(例如,llvm 是 umbrella 儲存庫中的子模組,而 clang 是 llvm 中的子模組)。submodule-map.txt 檔案是一個配對列表,每行一對。第一個配對項目描述 umbrella 儲存庫中子模組的路徑。第二個配對項目描述在壓縮歷史記錄中應該寫入該子模組樹狀結構的路徑。

假設您的 umbrella 儲存庫實際上是 llvm 儲存庫,並且它在「巢狀來源」配置中具有子模組(clang 位於 tools/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

如果子模組路徑未出現在映射中,則該工具假設應將其放置在 monorepo 中的相同位置。這意味著,如果您在 umbrella 儲存庫中使用「巢狀來源」配置,則*必須*為 umbrella 儲存庫中的所有專案(llvm 除外)提供映射項目。否則,來自子模組更新的樹狀結構將出現在壓縮歷史記錄中 llvm 的下方。

因為 llvm 本身就是 umbrella 儲存庫,所以我們使用 --subdir 將其內容寫入壓縮歷史記錄中的 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 生態系統。如果這些儲存庫與 LLVM 緊密耦合,則將它們匯入到您的本地 monorepo 鏡像中可能是有意義的。

如果這些儲存庫參與了上述壓縮過程中使用的 umbrella 儲存庫,則它們將自動添加到 monorepo 中。對於未參與 umbrella 設定的下游儲存庫,https://github.com/greened/llvm-git-migration/tree/import 中的 import-downstream-repo.py 工具可以幫助您將它們添加到 monorepo 中。流程如下

# 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

您可能希望合併其他對應的分支,例如,如果 myrepo 發佈分支與 LLVM 專案發佈保持同步,則可以合併它們。

--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--'

提交 R1R2R3 的樹狀結構「僅」包含來自 myrepo 的 Blob。 如果您需要將來自 myrepo 的提交與本地專案分支上的提交交錯(例如,與上面的 llvm1llvm2 等交錯),並且 myrepo 沒有出現在傘型儲存庫中,則需要開發新的工具。 建立這樣的工具將涉及

  1. 修改 fast_filter_branch.py,使其選擇性地直接採用修訂列表,而不是自行生成

  2. 建立一個工具,根據某些標準生成本地提交的交錯順序 (zip-downstream-fork.py 使用傘型歷史記錄作為其標準)

  3. 生成這樣的順序並將其作為修訂列表提供給 fast_filter_branch.py

還可能需要注意處理合併提交,以確保此類提交的父項正確遷移。

清理本地單一儲存庫

完成所有遷移、壓縮和匯入後,就可以開始清理了。 Python 工具使用 git-fast-import,它會留下很多垃圾,我們希望盡可能縮小新的單一儲存庫鏡像。 以下是一種方法

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 伺服器並享受編程的樂趣!

參考文獻