將 LLVM 專案移至 GitHub

目前狀態

我們計劃在 2019 年 10 月 21 日前完成遷移至 GitHub 的作業。請參閱 GitHub 遷移狀態頁面以取得最新的更新資訊,以及關於如何遷移您的工作流程的指示。

簡介

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

本提案並非關於

變更開發政策。

本提案僅關於將我們的原始碼儲存庫從我們自己的伺服器上託管的 SVN 遷移到 GitHub 上託管的 Git。我們不打算使用 GitHub 的議題追蹤器、Pull Request 或程式碼審查。

貢獻者將繼續根據開發者政策按需獲得提交權限,但 GitHub 帳戶將取代 SVN 使用者名稱/密碼雜湊成為必要條件。

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

為何要遷移?

此討論的起因是我們目前以志願服務的方式託管我們自己的 Subversion 伺服器和 Git 鏡像。LLVM 基金會贊助伺服器並提供有限的支援,但其能力有限。

志工本身並非系統管理員,而是碰巧略懂伺服器託管的編譯器工程師。我們也沒有 24/7 的支援,而且我們有時醒來會發現持續整合因為 SVN 伺服器當機或無回應而中斷。

我們應該善用外部服務(GitHub、GitLab 和 BitBucket 等),它們免費提供更好的服務(24/7 穩定性、磁碟空間、Git 伺服器、程式碼瀏覽、Fork 功能等)。

為何選擇 Git?

現在許多新的程式設計人員都從 Git 入門,而且很多人從未使用過 SVN、CVS 或其他任何版本控制系統。像 GitHub 這樣的網站改變了開源貢獻的格局,降低了首次貢獻的門檻並促進了協作。

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

Git 讓您可以

  • 在本機提交、合併、Squash 和 Fork,而無需接觸遠端伺服器。

  • 維護本機分支,從而實現多個開發線程。

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

  • 在沒有網路連線的情況下檢查儲存庫歷史記錄(Blame、Log、Bisect)。

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

此外,由於 Git 似乎正在取代許多 OSS 專案的版本控制系統,因此有許多工具是基於 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]

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

  • 「不可讀的版本號仍然是一個主要的倒退。考慮到 Bugzilla 上關於『在...中修復』的大量流量,這是一個不容忽視的問題。」 [JSonnRevNum]

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

然而,Git 可以模擬這種遞增的修訂編號: git rev-list --count <commit-hash>。此識別碼僅在單個分支內是唯一的,但這表示元組 (編號, 分支名稱) 唯一地識別一個提交。

因此,我們可以使用此修訂編號來確保例如 clang -v 報告使用者友善的修訂編號(例如 main-123454.0-5321),從而解決上述關於 Git 此方面的異議。

分支和合併呢?

與 SVN 相比,Git 使分支變得容易。Git 的提交歷史記錄以 DAG(有向無環圖)表示,這與 SVN 的線性歷史記錄不同。但是,我們建議強制禁止在我們的標準 Git 儲存庫中進行合併提交。

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

GitHub 提供了一個名為「狀態檢查」的功能:受「狀態檢查」保護的分支要求在推送發生之前,提交必須被明確允許。我們可以在用戶端提供一個 pre-push Hook,它將在允許提交被推送之前執行並檢查歷史記錄 [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 儲存庫描述

單體儲存庫 (Monorepo)

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

將所有子專案放在單個 Checkout 中使得跨專案重構自然而然地變得簡單

  • 新的子專案可以輕鬆地拆分出來,以實現更好的重用和/或分層(例如,允許執行時期程式庫使用 libSupport 和/或 LIT,而無需依賴 LLVM)。

  • 在 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 橋接器。但是,過去此橋接器的運作一直存在問題,因此目前尚不清楚這是否是未來會支援的功能。

單體儲存庫的缺點

  • 對於那些貢獻獨立子專案的人來說,使用單體儲存庫可能會增加額外負擔,特別是在 libcxx 和 compiler-rt 等不依賴 LLVM 的執行時期程式庫上;目前,libcxx 的全新 Clone 只有 15MB(而單體儲存庫為 1GB),LLVM 的提交頻率可能會導致在上傳時更頻繁地發生 git push 衝突。受影響的貢獻者或許可以使用 SVN 橋接器或單個子專案 Git 鏡像。但是,這些專案是否會繼續維護尚待決定。

  • 對於那些整合獨立子專案的人來說,即使他們沒有貢獻它,使用單體儲存庫也可能會增加額外負擔,原因與上述磁碟空間問題相同。子專案 Git 鏡像的可用性將解決此問題。

  • 保留現有的基於讀/寫 SVN 的工作流程依賴於 GitHub SVN 橋接器,這是一個額外的依賴項。維護這一點會將我們鎖定在 GitHub 中,並可能限制未來工作流程的變更。

工作流程

遷移前/後的工作流程

本節將介紹幾個工作流程範例,旨在說明終端使用者或開發人員將如何與儲存庫互動以應對各種使用情境。

Checkout/Clone 單個專案,具有提交權限

目前

# 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 序列執行的。

單體儲存庫變體

使用單體儲存庫變體,有幾個選項,具體取決於您的限制。首先,您可以只 Clone 完整的儲存庫

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

此時,您擁有每個子專案(llvm、clang、lld、lldb、...),這並不意味著您必須建置所有這些專案。例如,您仍然可以只建置 compiler-rt。這樣做與今天使用 SVN Checkout 所有專案的人沒有什麼不同。

如果您想避免 Checkout 所有原始碼,可以使用 Git Sparse Checkout 隱藏其他目錄

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

所有子專案的資料仍然在您的 .git 目錄中,但在您的 Checkout 中,您只會看到 compiler-rt。在推送之前,您需要像往常一樣 Fetch 和 Rebase (git pull –rebase)。

請注意,當您 Fetch 時,您可能會拉取您不關心的子專案的變更。如果您使用 Sparse Checkout,則其他專案中的檔案不會出現在您的磁碟上。唯一的影響是您的提交雜湊會變更。

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

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

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

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。

多儲存庫變體無法解決此問題:必須在每個單獨的儲存庫中單獨提交和推送。可以建立一個協議,讓使用者在其提交訊息中新增一個特殊 Token,使總儲存庫的更新機器人將所有這些提交分組到單個修訂版本中。

單體儲存庫變體原生處理此問題。

為本機開發或實驗進行分支/Stash/更新

目前

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

二分法 (Bisecting)

假設開發人員正在尋找 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 分支。

遷移這些分支最簡單的方法是使用 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 後的操作。Ref 路徑將需要更新,以將本機分支對應到相應的上游分支。如果本機分支沒有相應的上游分支,則 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/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 提交沒有執行任何來自上游的合併。

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

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

clang 外部的 Lclang1 的樹將表示 U3 時的情況,因為未參與總括歷史記錄的所有上游專案都應處於尊重提交 U3 的狀態。llvmlld 的樹應分別正確表示提交 Lllvm1Llld1

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

範例 1:總括專案看起來像單體儲存庫

對於此範例,我們假設每個子專案都以其自己的頂層目錄出現在總括專案中,就像它們在單體儲存庫中所做的那樣。我們也假設我們希望 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

請注意,如果總括專案有指向非 LLVM 儲存庫的子模組,則 zip-downstream-fork.py 需要知道它們才能重寫提交。這就是為什麼上面的第一步是從這些儲存庫中 Fetch 提交。

透過 --update-tags,該工具將遷移指向已內聯到壓縮歷史記錄中的子模組提交的註解標籤。如果總括專案拉取了一個碰巧具有指向它的標籤的上游提交,則該標籤將被遷移,這幾乎肯定不是想要的結果。標籤始終可以在重寫後移回其原始提交,或者可以丟棄 --update-tags 選項,然後手動遷移任何本機標籤。

範例 2:巢狀原始碼佈局

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

假設您的總括儲存庫實際上是 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

如果子模組路徑未出現在地圖中,則該工具會假定應將其放置在單體儲存庫中的相同位置。這意味著如果您在總括專案中使用「巢狀原始碼」佈局,則必須為總括專案中的所有專案(llvm 除外)提供地圖條目。否則,來自子模組更新的樹將出現在壓縮歷史記錄中的 llvm 下方。

由於 llvm 本身就是總括專案,因此我們使用 –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 緊密耦合,則將它們匯入到您的單體儲存庫本機鏡像中可能是有意義的。

如果這些儲存庫參與了上述壓縮過程中使用的總括儲存庫,它們將會自動被添加到單一儲存庫中。對於不參與總括設定的下游儲存庫,位於 https://github.com/greened/llvm-git-migration/tree/importimport-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

您可能想要合併其他對應的分支,例如,如果 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 的 blobs。如果您需要 myrepo 的提交與本地專案分支上的提交交錯(例如,與上面的 llvm1llvm2 等交錯),並且 myrepo 沒有出現在總括儲存庫中,則需要開發一個新工具。創建這樣一個工具將涉及

  1. 修改 fast_filter_branch.py 以選擇性地直接接收 revlist,而不是自行生成

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

  3. 生成這樣的排序並將其饋送到 fast_filter_branch.py 作為 revlist

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

清理本地單一儲存庫

一旦所有遷移、壓縮和導入完成,就該清理了。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 伺服器,祝您駭客愉快!

參考文獻