)
▲点击上方“DotNet NB”关注公众号回复“1”获取开发者路线图学习分享丨作者 / 郑 子 铭这是DotNet NB 公众号的第241篇原创文章原文 | Přemek Vysoký翻译 | 郑子铭术语让我们来看看本节剩余部分将用到的一些术语源代码/产品存储库– 当前开发存储库之一例如dotnet/runtime。不是 VMR。前向流程——将变更从产品存储库移动到虚拟存储库的过程。回流– 将变更从 VMR 移动到产品存储库的过程。代码流——指在虚拟存储库 (VMR) 和产品存储库之间传递变更的过程。这是一个通用术语既可以指正向流也可以指反向流。代码流 PR – 指在代码流流程中发起的包含代码变更的拉取请求。它可以是正向流 PR也可以是反向流 PR。双向代码流 v1代码流算法的第一版设计目标是每次需要提交变更时都必须能够在目标仓库中创建一个拉取请求。该拉取请求必须包含所需的变更但可能与目标分支冲突。我们将展示后来我们如何意识到这是一个错误的指导原则因为它引入了一些有趣的问题。简而言之该算法的工作原理是我们使用上述跟踪元数据来追踪双方之间的最后数据流。然后我们找到目标仓库中创建 PR 分支的正确位置提交将更改物化到该分支之上并创建一个拉取请求。我们必须向您保证如果双方之间存在冲突的更改这些冲突也会出现在 PR 中。这意味着 PR 分支必须基于足够旧的提交以便将源分支的提交和目标分支中的更改都引入到冲突状态。代码流算法的第一个版本用于发布大部分 .NET 10 预览版以及 10.0 正式版。该算法会考虑前一个代码流的方向并据此应用不同的策略。从技术上讲有四种情况需要考虑正向-正向、正向-反向、反向-正向、反向-反向但后两种情况是对称的因此我们不再单独讨论。相反方向的流动让我们先来看一个比前两种情况更复杂的场景——当有两个方向相反的流体流动时。本节中的图表使用以下符号 橙色– 文件内容转换。文件以内容开头AB - C表示提交将内容从更改B为C。 绿色– 之前的成功流程。显示正在执行的提交虚线以及另一端 PR 分支将形成的样子实线。 蓝色——当前正在讨论的趋势。 紫色– 正在传递到对应存储库的差异。⚫ 灰色– 与被跟踪文件无关的提交。提交记录按时间顺序编号。标记1和2通常表示之前的同步操作。代码流程图显示了存储库和 VMR 之间的两个连续流程每个流程的方向都不同。图中的变化流程如下1并2表示之前的某个同步点。4VMR 中的提交会将内容更改A为B。5从这一点开始发生回流。6仓库中创建了一个回流分支绿色。该分支基于上次同步的提交1。此流程的创建方式并非本图的重点。这里我们关注的是以下流程从该分支发起一个 PR。7Backflow PR 已合并有效地将仓库主分支中的更新A从A更新到。B8仓库中执行了一次提交操作将内容从更改B为C。9VMR 中进行了一项无关的提交。10从这一点开始正向流动就此开始。11在代码仓库中创建了一个前向流分支蓝色。该分支基于上次同步的5基本提交。从该分支打开了一个 PR。在前向流 PR 中进行了一次额外的提交将的内容更改A为D。12PR 已合并有效更新A为。BD您可以注意到以下几个特点没有出现 Git 冲突。这是因为这个具体示例只考虑了一个文件该文件按时间顺序逐步更改A。D在大部分更改都发生在单个代码仓库的情况下我们期望代码能够流畅地运行。整个流程类似于开发人员在单个代码仓库的开发分支上工作。然后开发人员向主分支在本例中为主代码仓库提交一个 PR。在单个代码仓库的情况下如果出现冲突这里也会出现冲突这是设计使然。11接下来需要讨论的是如何创建正向分支的提交 。我们知道7在上次回流 PR 合并后我们从仓库收到了作为提交一部分的增量。我们考虑到使用了压缩合并因此提交可能不再可用。在提交和6之间回流 PR 分支上也可能存在其他提交。当我们进行正向提交时需要流出的变更集实际上包括提交、、和。基本上就是仓库端所有尚未流入 VMR 的变更。它以和之间的紫色差异可视化。此差异正确地表示了增量因为6710367810106它包含了 VMR 的最后一个已知快照6自上次提交以来VMR 中发生的所有提交——提交3和7。8自同步以来VMR 中发生的其他提交10。正向分支的基础提交是上次回溯分支的基础提交因为我们将增量更新应用到该回溯分支上。如果提交9与增量更新存在冲突PR 会显示这些冲突开发人员需要解决这些冲突。两股流向相同方向当有两个连续的、方向相同的流时情况就简单一些代码流程图展示了从代码库到虚拟存储库的两个连续流程。当我们形成前向流提交10时我们知道自从上次将所有更新发送到 VMR 以来唯一发生的事情就是提交9和10。然后我们可以将此新增量应用到上次前向流提交之上8。冲突冲突是指同一文件的同一部分同时在代码仓库和虚拟代码库 (VMR) 中被以不同的方式修改的情况。此时需要人工干预来决定哪个修改生效。该算法的目标是确保在修改再次发生之前这些冲突能够被发现并得到解决。然而我们将展示冲突的引入方式如何影响正在进行的代码流程。我们还将展示即使没有进行任何冲突的修改冲突也可能出现。让我们考虑以下示例其中冲突是由正向流 PR 中一个无关的提交引入的代码流程图展示了代码流程 PR 中引入的冲突。在这种情况下第一个正向流 PR 中新增的提交6与仓库中的一个提交冲突10。由于没有回流仓库端无法获知此信息。后续的正向流也存在问题因为更改10无法11应用到 之上8。事实上我们甚至根本无法为 PR 分支创建任何提交在这种情况下唯一可行的办法是基于最后一个已知的有效提交 来创建 PR 分支2并重建之前的流程通过重新应用5这实际上是和然后应用和创建一个由于 冲突而与目标分支冲突的 PR 分支。之后用户将被指示将目标分支 合并到 PR 分支中以解决冲突。对于 中包含的与 中的更改基本相同的更改git 将透明地进行匹配只留下实际冲突的文件进行解决。下一次回流会将此解决方案带到仓库。1341011610958还有无数其他可能发生的冲突但这些冲突通常会在 PR 中表现为冲突。上面的例子更有意思因为正向流甚至一开始就无法创建 PR 分支。这是因为8之前的正向流提交包含6与冲突的10文件。需要注意的是冲突文件集不仅包含问题文件还包含跟踪上次同步提交的清单文件。这是因为清单文件在提交中被更新8其中包含提交的 SHA 值4而 PR 分支正在将同一行更新为11。即使我们知道源清单的预期内容也无法部分解决这个问题因为 Git 不允许部分合并解决。这意味着当真正的冲突迫使我们变基到较旧的提交时就会带来麻烦……冲突到处都是冲突一旦我们开始在实践中测试算法就逐渐意识到真正的复杂性在于问题在实际开发节奏中的动态变化。流程很少像乒乓球比赛那样来回往复变化以可预测的方式流畅进行。相反我们必须预料到流程会以各自的频率在两个方向上并行发生而且从开始到结束往往需要很长时间。让我们来看这样一个场景源代码库中的单个文件经历了一系列渐进式的变化虽然我们没有对它进行任何实际的冲突性更改但我们仍然会在代码流 PR 中看到冲突代码流程图展示了文件逐步更改过程中出现的问题。在这个例子中仓库中的一个文件的内容逐渐从Afile1变为Bfile2再变为file3 C。虚拟仓库 (VMR) 中没有对该文件进行任何更改。正向流和反向流并行启动并且都已成功合并。正向流成功地将 VMR 中的文件从file1更新为Afile2 B。现在我们来看第二个正向流理论上它应该将更改带到Bfile3。第二个正向流遵循上述C反向流的算法其 PR 分支基于最后一个反向流的提交。这意味着 PR 分支实际上包含了从file1到file2的更改。然而目标分支已经包含了从file1到file2的更改。当我们尝试合并 PR 时Git 会发现从file1到file2PR 分支的更改与从file1到file2目标分支的更改之间存在冲突因此合并失败。ACABACAB在这个例子中可以看到即使文件本身没有发生任何冲突性的更改流程的交错方式仍然导致了冲突。为了解决这个问题我们可以利用之前两个流程而不仅仅是最后一个流程的信息将目标分支合并到 PR 分支中并根据我们对文件最终状态的理解以编程方式解决冲突。然而这种方法仅在文件没有发生任何我们意料之外的更改时才有效。此外一旦出现真正的冲突合并分支就再次变得不可能。这意味着用户不仅要处理真正的冲突还要处理原本不应该发生冲突的、逐渐变化的文件中的冲突例如源清单文件会在每个流程中发生变化并且在这种情况下总是会导致冲突。开发人员随后必须自行解决跟踪数据中的冲突但这并非理想之选因为它很容易导致灾难性的后果。这种情况并不少见。引入新文件并在后续 PR 中快速修改的工作流程很常见例如本地化。回滚问题上述问题虽然可行但也暴露出该方法的一些固有局限性。压垮骆驼的最后一根稻草促使我们重新思考部分目标就是所谓的“回滚问题”。这个想法与之前描述的场景类似但不同之处在于我们不是逐步修改文件而是先修改文件然后再撤销修改。如果我们设法将修改和撤销操作分开处理而第二次包含撤销操作的流程最终也导致冲突那么这种“完美风暴”将阻止我们自动合并分支最终导致撤销操作完全丢失代码流程图展示了文件更改回滚时出现的问题。在这个例子中我们可以观察到以下几点文件B被添加后又被删除还原同时文件A收到不相关的冲突更改。正向流 PR 分支包含所有三个更改其中还原操作会否定原始更改表现为文件B根本没有改变。文件冲突导致我们无法将 PR 分支建立在提交上次回溯A之上而必须将其建立在提交之上同时重新创建之前的流程。82由于新重新基于 PR 分支的更改在技术上不包含对文件的任何更改B更改及其还原相互抵消因此当我们合并 PR 时还原操作将完全丢失文件B将保留在 VMR 中而从原始存储库中删除。此外这不仅适用于整个文件的还原也适用于任何更改无论多么微小即使之后被还原。太可怕了尽管需要多个条件同时满足但这种情况在实践中仍然可能发生。具体来说我们曾在代码中引入临时解决方案或功能标志之后又将其移除时遇到过这种情况。在繁忙的仓库中当 PR 被完全回滚时也会出现这种情况因为实际冲突可能频繁发生。无论如何我们不能接受就这样悄无声息地丢失更改。一切都得从头再来重新调整我们的方法至此我们已经穷尽了所有通过简单的分支和合并实现的可能性。我们提出的每一个变通方案和想法都会有一个反例将其彻底推翻改变游戏规则这促使我们重新考虑“始终能够在目标仓库中创建包含一定内容的 PR”这一目标。由于我们无法部分解决冲突因此必须在与 GitHub PR 用户界面不同的环境中进行解决。能否在开发人员的本地机器上进行我们能否允许用户运行一个命令在本地执行流程使本地仓库处于冲突状态解决已知的非冲突例如源清单更改然后让用户处理实际问题不同的游戏我们尝试遵循的另一项设计指南是比较我们的双仓库方案与只有一个仓库但有多个分支的方案有何不同。我们的工作流程和 Git 操作与在功能分支上进行功能 PR 的日常工作有何不同人们通常如何处理冲突当冲突发生时流程又是怎样的这促使我们探索一种完全不同的方法。我们现在要讨论的是流程git rebase。让我们互动起来结合以上理念我们得到了一种全新的代码流程体验它更具交互性并且在出现冲突时需要用户进行不同的干预。新流程与常规的 Git 变基流程并没有太大区别代码流服务仍然以与以前相同的方式计算更改——考虑以前的流程根据最后一个流程构建分支等等。然后我们尝试将 PR 分支变基到目标分支的最新版本。当存在冲突时此操作会失败并导致代码库处于冲突状态。如果没有发生冲突则 rebase 操作将被提交并推送到新的 PR 中这样就完成了。如果发生冲突代码流服务将无法继续运行。它会创建一个空的 PR并指示用户使用自定义工具在本地执行该流程。然后该服务会通过自定义状态检查来阻止 PR 合并直到看到所需的更改推送到 PR 分支为止。定制工具获取必要信息并在本地执行相同的代码流程。如果出现冲突它会解决任何已知的冲突这些冲突可能是由于如上所述的双向并行数据流造成的。然后它会将实际的冲突留给用户解决。用户随后提交并推送更改。最后该服务验证推送的内容并解除 PR 的合并限制它会批准自定义状态检查。这个 PR展示了 PR 的示例。我们能够部分解决已知冲突同时将实际冲突留给用户处理这是成功的关键。由于我们可以利用之前所有流程的信息正确计算增量因此不再受回滚问题的困扰。而且由于我们已经位于目标分支之上因此也可以纠正任何遗漏的回滚。我们可以通过尝试回滚上次流程的更改并查看哪些文件回滚失败来检测遗漏的回滚。这种情况只会在文件缺少更改时发生。我们在去年 12 月推出了这项新功能到目前为止使用体验非常好。我们仍在研究一项实验性改进方案不在目标仓库中创建工作分支而是直接在目标分支上应用补丁。然而这种方法在 Git 中无法直接实现因为某些操作例如修改 Git 未知的文件比如文件在一侧被修改而在另一侧被删除会导致应用失败。目前来看先在目标仓库的工作分支中提交所需的更改然后再将其变基到目标分支上会更容易因为 Git 在变基时可以利用提交图中的更多信息。但是如果我们能够解决这些不支持的情况将大大简化流程因为创建工作分支本身就是对之前流程的重构。当前挑战我们一路走来历经艰辛但若认为已经抵达终点那就太天真了。没错我们确实利用虚拟仓库VMR成功发布了十几个版本而且它也确实带来了诸多成果例如让我们能够更早地完成发布构建同时还能在开发流程的后期接受最后的修复。令人惊讶的是即使从仓库依赖树切换到扁平拓扑结构也没有对我们日常的 .NET 10 开发造成实质性的影响。经过周密的计划我们仅用了几个小时就完成了迁移。尽管如此我们还是必须承认这一过程中也遇到了一些小问题。分支与产品生命周期.NET 是一个庞大的平台包含众多不同的产品这些产品通常以各自的节奏发布。例如Visual Studio、Aspire、.NET MAUI 和 Entity Framework 等它们拥有不同的生命周期模型需要不同的开发节奏而这种节奏并不总是与 .NET SDK 的节奏一致。简而言之根据产品生命周期代码库可以分为几个主要类别以SDK 版本为中心的存储库——例如dotnet/sdk每个 SDK 版本都会提供不同的变体。它们的分支方式与 VMR 本身相同例如release/10.0.1xx或release/10.0.2xx。共享组件/运行时——例如dotnet/runtimedotnet/aspnetcore提供多个 SDK 版本之间共享组件的存储库。它们通常会按主版本进行分支例如 1.0release/10.0或 2.0 release/11.0。VS 中心型——例如dotnet/roslyn将组件与 Visual Studio 版本紧密耦合的存储库。它们通常会根据 Visual Studio 版本进行分支例如 1.v1release/17.14或 1.v2 release/dev18.0。您可以阅读VMR SDK Bands 文档详细了解有关我们分支策略的更多信息。当代码库需要与遵循 SDK 分支中心模型的 VMR 错开分支时间时生命周期中的这些差异就开始显现。代码流算法的设计初衷仅是为了处理两个分支之间的同步。实际上这意味着当代码库需要将不同的分支与给定的 VMR 分支同步时我们必须手动重置 VMR 的内容使其与代码库保持一致。这是一个复杂的过程因为我们必须确保在此过程中不会丢失对 VMR 所做的任何更改。在 .NET 10 产品周期中类似上述情况已经发生过数十次因为我们正忙于发布 .NET 10.0.200 版本。我们仍在努力解决这个问题计划检测代码流配置的变化并发布一个自动内容重置 PR以尝试优雅地处理这种过渡。卡扣式释放分支另一种问题情况是当我们为了发布而合并分支时每个产品仓库的合并时间可能不同。例如在开发新的 .NET 主要版本例如 .NET 10期间我们每个月都会将main开发分支合并到相应的预览发布分支中release/10.0.1xx-previewN。假设某个仓库在虚拟主分支 (VMR) 合并之前就合并了其分支代码流程图展示了当分支顺序错误时出现的问题。注意提交是如何流入虚拟主仓库 (VMR) 的35并且根据每个仓库快照其分支的时间它们之间可能存在父子关系也可能不存在。在图中它们在仓库中没有关联但在虚拟主仓库中却存在父子关系。这显然是错误的因为两者都可能进行冲突的更改但在各自分支的历史记录中这种关联是完全有效的。我们通过集中管理 snap 包来避免这种情况的发生首先从 VMR 开始。VMR 会优先处理这些 snap 包我们会从中找到每个产品仓库中仍然适用的最新提交。然后我们会在该提交中创建发布分支。元数据损坏我们遇到的另一个挑战涉及同步跟踪数据。产品代码库中常见的做法是合并不同的发布分支。例如对某个分支所做的更改release/10.0.1xx通常可以合并到更高版本release/10.0.2xx分支中。在此过程中如果跟踪元数据被覆盖则可能会出现不一致的情况。代码库的 2xx 分支最终会引用一个与 1xx 分支同步的 VMR 提交。我们目前正在尝试使用Git Notes作为跟踪元数据的替代存储机制。Git Notes 将元数据附加到提交中而不会修改提交本身这有助于我们避免将元数据作为工作树的一部分而产生的一些问题。未涵盖的内容这篇文章篇幅增长得很快对此我深表歉意。尽管如此还有许多相关的有趣方面值得进一步探讨因为它们对于整体的成功与同步算法本身同样至关重要开发者体验——我们在用户体验方面所做的一切旨在帮助开发者浏览代码流程 PR 并跟踪他们的更改同步到哪里。监控和可观测性——我们如何跟踪代码流状态和健康状况检测卡住的流程或对跨存储库的问题发出警报。工具– 我们构建了哪些自定义工具来帮助开发人员执行本地代码流程、解决冲突以及在推送之前验证更改。如果您对这些主题中的任何一个感兴趣并希望了解更多详细信息请告诉我们。结论如果您读到这里——首先非常感谢——其次希望您能从我们的发展历程中有所了解从基于 tarball 的源代码构建到完全同步的单体仓库我们始终保持着数百名开发者在数十个仓库中的高效工作并确保每月不间断地发布新版本。虚拟单体仓库 (VMR) 已成为 .NET 基础架构的基石使我们能够统一并简化构建和发布流程同时保留各个仓库及其社区的灵活性和自主性。然而这些优势也带来了仓库同步的复杂性。如果您也正在经历类似的转型或许我们目前的架构只是通往完整单体仓库的垫脚石我们希望我们的经验和心得能够对您有所帮助。欢迎随时联系我们这是一个比较小众的问题我们很乐意与您交流资源统一构建设计文档代码流程算法的原始设计文档代码流程的实现原文链接How We Synchronize .NET’s Virtual Monorepo推荐阅读【译】 我们如何同步 .NET 的虚拟单体仓库一【译】 如何使用 .NET MAUI 构建 Android 小部件【译】 GitHub Copilot Testing for .NET 将 AI 驱动的单元测试引入 Visual Studio 2026Maomi.MQ 功能强大的 .NET RabbitMQ 消息队列通讯模型框架来了推荐一个开源的 .NET 工作流引擎和审批流项目推荐一个基于 .NET 10 开源的 RBAC权限体系的通用后台管理系统点击下方卡片关注DotNet NB一起交流学习▲点击上方卡片关注DotNet NB一起交流学习请在公众号后台回复【路线图】获取.NET 2024开发者路线回复【原创内容】获取公众号原创内容回复【峰会视频】获取.NET Conf大会视频回复【个人简介】获取作者个人简介回复【年终总结】获取作者年终回顾回复【加群】加入DotNet NB 交流学习群长按识别下方二维码或点击阅读原文。和我一起交流学习分享心得。