将LLVM项目迁移到GitHub

当前状态

我们计划在2019年10月21日前完成迁移到GitHub。请查看GitHub迁移状态页面,了解最新更新和迁移工作流程的说明。

简介

这是一个将我们当前的版本控制系统从我们自己托管的Subversion迁移到GitHub的提案。以下是关于我们为何提出此迁移以及人们(和验证基础设施)如何继续使用基于Git的LLVM的技术和财务论证。

此提案**不**涉及

更改开发策略。

此提案仅涉及将我们的源代码存储库的托管从我们自己的服务器上托管的SVN迁移到GitHub上托管的Git。我们不建议使用GitHub的问题跟踪器、拉取请求或代码审查。

贡献者将继续根据开发者政策按需获得提交权限,只是需要GitHub帐户而不是SVN用户名/密码哈希。

为什么选择Git,为什么选择GitHub?

为什么要迁移?

我们开始讨论这个话题,是因为我们目前以志愿者的身份托管我们自己的Subversion服务器和Git镜像。LLVM基金会赞助服务器并提供有限的支持,但它能做的也仅此而已。

志愿者本身并不是系统管理员,而是碰巧了解一些关于托管服务器知识的编译器工程师。我们也没有7×24小时的支持,有时我们醒来会发现持续集成坏了,因为SVN服务器要么宕机要么无响应。

我们应该利用现有的服务之一(GitHub、GitLab和BitBucket等),这些服务免费提供更好的服务(7×24小时稳定性、磁盘空间、Git服务器、代码浏览、分叉功能等)。

为什么选择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有一个重要的优势:它为存储库提供了读写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/不属于单存储库。

将所有子项目放在一个检出中,使得跨项目重构自然变得简单

  • 新的子项目可以轻松拆分以实现更好的重用和/或分层(例如,允许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 的全新克隆仅为 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 commitgit 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 时,此步骤是不可能的,因为“重新设置基准”会在提交时隐式发生(除非发生冲突)。

检出/克隆多个项目,有提交权限

让我们看看如何在给定修订版本下组装 llvm+clang+libcxx。

当前

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

或使用 git-svn

git clone https://llvm.net.cn/git/llvm.git
cd llvm/
git svn init https://llvm.net.cn/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.net.cn/git/clang.git
cd clang/
git svn init https://llvm.net.cn/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.net.cn/git/libcxx.git
cd libcxx
git svn init https://llvm.net.cn/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-migration 上的 migrate-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/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 是对 Lclang1 中 clang 树实际外观的视觉提醒。

即便如此,边 U3 -> Llld1 也可能对未来从上游的合并造成问题。git 会认为我们已经从 U3 合并了,而且我们确实合并了,除了 clang 树的状态。一种可能的缓解策略是手动比较 U2U3 之间的 clang,并将这些更新应用于 local/zip。另一种可能更简单的策略是冻结下游分支上的本地工作,并在运行 zip-downstream-fork.py 之前从最新的上游合并所有子模块。如果下游从上游同步合并每个项目,没有任何中间的本地提交,那么在没有采取任何特殊措施的情况下,一切应该都会正常。我们预计这将是常见情况。

除了 clang 之外,Lclang1 的树将代表 U3 时的事物状态,因为所有未参与伞形历史的上游项目都应该处于尊重提交 U3 的状态。llvm 和 lld 的树应分别正确地表示提交 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 需要了解它们才能重写提交。这就是上面第一步从这些仓库获取提交的原因。

使用 --update-tags,该工具将迁移指向已内联到压缩历史记录中的子模块提交的带注释标签。如果伞形结构提取了一个碰巧带有指向它的标签的上游提交,则该标签将被迁移,这几乎肯定不是我们想要的。在重写后,始终可以将标签移回其原始提交,或者可以丢弃 --update-tags 选项,然后手动迁移任何本地标签。

示例 2:嵌套源布局

该工具处理嵌套子模块(例如,llvm 是伞形结构中的一个子模块,clang 是 llvm 中的一个子模块)。文件 submodule-map.txt 是一个每行一对的列表。第一对项目描述了伞形结构仓库中子模块的路径。第二对项目描述了应在压缩历史记录中为该子模块写入树的路径。

假设您的伞形结构仓库实际上是 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)提供映射条目。否则,来自子模块更新的树将在压缩历史记录中的 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 的 Blob。如果您需要来自 myrepo 的提交与本地项目分支上的提交交错(例如,与上面的 llvm1llvm2 等交错),并且 myrepo 未出现在伞形结构仓库中,则需要开发一个新工具。创建此类工具将涉及

  1. 修改 fast_filter_branch.py 以选择性地直接获取 revlist,而不是自行生成它

  2. 创建一个工具,根据某些条件生成本地提交的交错排序(zip-downstream-fork.py 使用伞形结构历史记录作为其条件)

  3. 生成此类排序并将其作为 revlist 提供给 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 服务器,并快乐地进行开发吧!

参考