在关于 monorepos 的文章和讨论中,有一个经常声称的关键好处:跨整个树的原子提交让您可以在一次提交中对库的实现和客户端进行更改。许多作者甚至声称这是 monorepos 的唯一好处。我喜欢 monorepos,但这种特殊的说法毫无意义!在大型 monorepo 中,您实际上不会进行向后不兼容的更改,例如接口重构。相反,该过程将是高度增量的,更像是以下内容:推送一次提交以更改库,使其支持具有不同接口的旧行为和新行为。一旦您确定不会恢复第 1 阶段的提交,请按 Ncommits 以切换 N 个客户端中的每一个以使用新界面。一旦你确定第 2 阶段的提交不会被恢复,pushone 提交以从库中删除旧的实现和接口。有很多原因可以解释为什么这是一个比单个原子提交更好的顺序,但它们主要是主题的变体:减轻风险。如果有什么东西坏了,你希望尽可能少的东西一下子坏掉,而且回滚到一个已知良好的状态也很简单。以下是在流程的各个阶段降低风险的方法: 第一次提交没有任何风险。它只是添加尚未被任何人使用的新代码。更改客户端的提交可以逐渐完成,从库所有者自己工作的提交、最有可能检测到错误的项目或最容易出错的客户端开始。根据更改的风险状况,您甚至可以将这些提交用作分阶段推出的一种形式,在发送下一批提交进行代码审查之前,您将等待查看以前的客户是否报告了生产中的任何问题。删除旧实现的最终提交只能破坏最少数量的客户端:在审查和推送删除提交之间刚刚开始使用库的客户端,并且确实使用了旧接口。理想的环境应该有适当的工具来防止这种倒退首先发生(例如,对已弃用接口的新用途发出警告)。如果在第 2 阶段出现任何问题,恢复只涉及几个文件的提交是微不足道的。相比之下,恢复跨越数百个项目的提交将非常痛苦,特别是如果 repo 具有任何类型的每个目录 ACL(我认为这对于大型 monorepo 是强制性的)。如果没有立即检测到损坏,情况会变得更糟,因为单个更改影响的代码越多,还原应用干净的可能性就越小。如果在第 3 阶段出现任何问题,那么在使用原子提交时也会出错。但是有了原子提交,第 3 阶段的破坏可能性要大得多,因为新用户自然会使用旧接口(在他们看来,新接口尚不存在),而且由于代码审查开始和提交之间的窗口会更广。再次,回滚会容易得多,提交只涉及库而不是客户端。还有一些额外的原因可以解释为什么巨大的提交会令人讨厌。例如,单个提交更改的项目越多,获得干净的预提交 CI 运行将变得越来越困难。当然,原子提交将节省一点工作,因为不需要让实现同时支持两个接口。但是,与为巨大的提交争吵的工作量相比,那一点点的节省并不是一个值得的权衡。当您离开库时,特别容易看到“整个存储库中的原子更改”故事是垃圾,并且还要考虑具有任何类型的更复杂部署生命周期的代码,例如通过 RPC 接口进行通信的服务和客户端二进制文件之间的交互。显然,在这种情况下您不能进行原子更改,因为您需要继续支持旧的服务器实现,直到所有客户端二进制文件都升级(并且是回滚安全的)。对数据库架构、命令行工具、同步的更改也是如此客户端 Javascript + 后端更改等。我认为 monorepos 确实使重构更容易。所以这不是问题。他们跨项目的原子提交也是正确的。但这两个事实没有任何关系。 Monorepo 使重构更简单的原因归结为组织中的每个人都对当前状态有一个共同的看法:在实践中,monorepo 将意味着基于主干的开发。你会知道每个人都真正在 HEAD 上,而不是实际上在某个已有一年历史的分支上进行开发。相反,您会知道库的每个用户都在从 HEAD 使用您的库,而不是将其固定到某个旧版本。找到所有当前调用者很简单,这样您就知道哪些客户端需要更新。 (当然,一旦您解决了大规模使用任何类型的 monorepo 工具这一非常重要的问题,当然。)理论上,假设有足够的工具支持、有关代码组织的纪律、在所有存储库,组织中所有存储库的主列表,默认为每个工程师都可以读取所有存储库,没有隐藏的孤岛。这在技术上都是可行的,但我怀疑首先使用多存储库在文化上不兼容。这种误解从何而来?它肯定存在于 Google monorepo 论文中,这在这方面有些自相矛盾。一方面,他们将这种形式的原子重构准确地描述为单体的一个好处:进行原子变化的能力也是单体模型的一个非常强大的特征。开发人员可以在单个一致的操作中对整个存储库中的数百或数千个文件进行重大更改。例如,开发人员可以在一次提交中重命名类或函数,而不会破坏任何构建或测试。但是当谈到实际的重构工作流程时,所描述的过程就大不相同了:Google 开发人员团队偶尔会进行一系列影响广泛的代码清理更改,以进一步维护代码库的健康。执行这些更改的开发人员通常将它们分为两个阶段。使用这种方法,首先进行大的向后兼容的更改。完成后,可以进行第二次较小的更改以删除不再引用的原始模式。我怀疑这里发生的事情是原子提交被认为是抽象的好处,重构被用作用例的说明。可以理解,这是一个关于如何使用 monorepo 的实际示例,这是可以理解的。在少数情况下,跨整个存储库的原子提交是正确的解决方案,但它必须非常罕见。例如,重命名具有数千个调用者的函数的示例可能通过临时别名函数或根据旧函数临时定义新函数来更好地处理。 (但这确实表明语言,无论是编程语言还是 IDL,都应该使尽可能多的构造的别名和间接访问变得容易)。是否有拥有大型单体仓库的组织,其中原子跨项目提交经常用于更改实现和客户端?