由于谷歌在 Monorepo 上的實(shí)踐,Monorepo 受到了越來越多的關(guān)注。Monorepo 意味著把所有項(xiàng)目的所有代碼統(tǒng)一維護(hù)在一個(gè)單一的代碼版本庫中,和多代碼庫方案相比,兩者各有優(yōu)劣,需要根據(jù)公司文化和產(chǎn)品特性進(jìn)行取舍。原文:What is monorepo? (and should you use it?)[1]
Monorepos(單一代碼庫)有助于加快開發(fā)工作流程,在本文中,我們將幫助你認(rèn)識(shí)這一代碼組織模型是否適合你的團(tuán)隊(duì)和公司。
什么是 monorepo?
Monorepo 的意思是在版本控制系統(tǒng)的單個(gè)代碼庫里包含了許多項(xiàng)目的代碼。這些項(xiàng)目雖然有可能是相關(guān)的,但通常在邏輯上是獨(dú)立的,并由不同的團(tuán)隊(duì)維護(hù)。
有些公司將所有代碼存儲(chǔ)在一個(gè)代碼庫中,由所有人共享,因此 Monorepos 可以非常大。例如,理論上谷歌擁有有史以來最大的代碼庫,每天有成百上千次提交,整個(gè)代碼庫超過 80 TB。其他已知運(yùn)營大型單一代碼庫的公司還有微軟、Facebook 和 Twitter。
Monorepos 有時(shí)被稱為單體代碼庫(monolithic repositories),但不應(yīng)該與單體架構(gòu)(monolithic architecture)相混淆,單體架構(gòu)是一種用于編寫自包含應(yīng)用程序的軟件開發(fā)實(shí)踐。這方面的一個(gè)例子就是 Ruby on Rails,它可以處理 Web、API 和后端工作。
單一代碼庫(monorepos) vs 多代碼庫(multirepos)
與單一代碼庫相反的是多代碼庫(multirepos),每個(gè)項(xiàng)目都儲(chǔ)存在一個(gè)完全獨(dú)立的、版本控制的代碼庫中。多代碼庫是很自然的選擇——我們大多數(shù)人在開始一個(gè)新項(xiàng)目時(shí)都愿意開一個(gè)新的代碼庫,畢竟,誰都喜歡從 0 開始.
從多代碼庫到單一代碼庫的變化就意味著將所有項(xiàng)目移到一個(gè)代碼庫中。

當(dāng)然,這只是開始。當(dāng)我們開始重構(gòu)和整合時(shí),困難的工作就來了。
$ mkdir monorepo
復(fù)制代碼
多代碼庫不是微服務(wù)(microservices)的同義詞,兩者之間并沒有耦合關(guān)系。事實(shí)上,我們稍后將討論將單一代碼庫和微服務(wù)結(jié)合起來的例子。只要仔細(xì)設(shè)置用于部署的 CI/CD 流水線[2],單一代碼庫就可以托管任意數(shù)量的微服務(wù)。
單一代碼庫的好處
乍一看,單一代碼庫和多代碼庫之間的選擇似乎不是什么大問題,但這是一個(gè)會(huì)深刻影響到公司開發(fā)流程的決定。至于單一代碼庫的好處,可以列舉如下:
可見性(Visibility):每個(gè)人都可以看到其他人的代碼,這樣可以帶來更好的協(xié)作和跨團(tuán)隊(duì)貢獻(xiàn)——不同團(tuán)隊(duì)的開發(fā)人員都可以修復(fù)代碼中的 bug,而你甚至都不知道這個(gè) bug 的存在。
更簡單的依賴關(guān)系管理(Simpler dependency management):共享依賴關(guān)系很簡單,因?yàn)樗心K都托管在同一個(gè)存儲(chǔ)庫中,因此都不需要包管理器。
唯一依賴源(Single source of truth):每個(gè)依賴只有一個(gè)版本,意味著沒有版本沖突,沒有依賴地獄。
一致性(Consistency):當(dāng)你把所有代碼庫放在一個(gè)地方時(shí),執(zhí)行代碼質(zhì)量標(biāo)準(zhǔn)和統(tǒng)一的風(fēng)格會(huì)更容易。
共享時(shí)間線(Shared timeline):API 或共享庫的變更會(huì)立即被暴露出來,迫使不同團(tuán)隊(duì)提前溝通合作,每個(gè)人都得努力跟上變化。
原子提交(Atomic commits):原子提交使大規(guī)模重構(gòu)更容易,開發(fā)人員可以在一次提交中更新多個(gè)包或項(xiàng)目。
隱式 CI(Implicit CI):因?yàn)樗写a已經(jīng)統(tǒng)一維護(hù)在一個(gè)地方,因此可以保證持續(xù)集成[3]。
統(tǒng)一的 CI/CD(Unified CI/CD):可以為代碼庫中的每個(gè)項(xiàng)目使用相同的 CI/CD[4]部署流程。
統(tǒng)一的構(gòu)建流程(Unified build process):代碼庫中的每個(gè)應(yīng)用程序可以共享一致的構(gòu)建流程[5]。
單一代碼庫的缺陷
隨著單一代碼庫的發(fā)展,我們?cè)诎姹究刂乒ぞ摺?gòu)建系統(tǒng)和持續(xù)集成流水線方面達(dá)到了設(shè)計(jì)極限。這些問題可能會(huì)讓一家公司走上多代碼庫的道路:
性能差(Bad performance):單一代碼庫難以擴(kuò)大規(guī)模,像 git blame 這樣的命令可能會(huì)不合理的花費(fèi)很長時(shí)間執(zhí)行,IDE 也開始變得緩慢,生產(chǎn)力受到影響,對(duì)每個(gè)提交測試整個(gè) repo 變得不可行。
破壞主線(Broken main/master):主線損壞會(huì)影響到在單一代碼庫中工作的每個(gè)人,這既可以被看作是災(zāi)難,也可以看作是保證測試既可以保持簡潔又可以跟上開發(fā)的好機(jī)會(huì)。
學(xué)習(xí)曲線(Learning curve):如果代碼庫包含了許多緊密耦合的項(xiàng)目,那么新成員的學(xué)習(xí)曲線會(huì)更陡峭。
大量的數(shù)據(jù)(Large volumes of data):單一代碼庫每天都要處理大量的數(shù)據(jù)和提交。
所有權(quán)(Ownership):維護(hù)文件的所有權(quán)更有挑戰(zhàn)性,因?yàn)橄?Git 或 Mercurial 這樣的系統(tǒng)沒有內(nèi)置的目錄權(quán)限。
Code reviews:通知可能會(huì)變得非常嘈雜。例如,GitHub 有有限的通知設(shè)置,不適合大量的 pull request 和 code review。
你可能已經(jīng)注意到,這些問題中的大多數(shù)都和技術(shù)有關(guān)。在下面的章節(jié)中,我們將了解堅(jiān)持使用單一代碼庫的公司是如何通過投資工具、添加集成以及編寫定制解決方案來解決大部分問題的。
這不僅僅是技術(shù)問題
選擇代碼庫策略不僅是一個(gè)技術(shù)問題,也是關(guān)于人們?nèi)绾谓涣鞯膯栴}。正如康威定律所言,溝通對(duì)于創(chuàng)造偉大的產(chǎn)品至關(guān)重要:
任何組織所設(shè)計(jì)的系統(tǒng)(此處的定義比信息系統(tǒng)寬泛得多)架構(gòu),都不可避免的反映為該組織溝通結(jié)構(gòu)的副本?!低?/strong>
雖然多代碼倉庫允許每個(gè)團(tuán)隊(duì)獨(dú)立管理他們的項(xiàng)目,但同時(shí)也阻礙了協(xié)作。它們就像眼罩一樣,讓開發(fā)人員只關(guān)注自己所擁有的部分,而忽略了整體。
另一方面,單一代碼庫就像一個(gè)樞紐、一個(gè)廣場,每個(gè)開發(fā)人員、工程師、測試人員和業(yè)務(wù)分析師都可以在這里會(huì)面和交談。單一代碼庫鼓勵(lì)對(duì)話,幫助我們消除“豎井”。
Monorepo 文化
Monorepos 已經(jīng)存在很長時(shí)間了。三十年來,F(xiàn)reeBSD 一直使用 CVS 和后來的 subversion monorepos[6]進(jìn)行開發(fā)和包分發(fā)。
許多開源項(xiàng)目已經(jīng)成功使用了單一代碼庫。例如:
Laravel[7]:一個(gè)用于 Web 開發(fā)的 PHP 框架。
Symfony[8]:一個(gè)用 PHP 編寫的 MVC 框架。有趣的是,他們已經(jīng)為每個(gè) Symfony 工具和庫創(chuàng)建了只讀存儲(chǔ)庫,這種方法被稱為分庫(split-repo)。
NixOS[9]:一個(gè)用單一代碼庫發(fā)布包的 Linux 發(fā)行版
Babel[10]:一個(gè)用戶 Web 開發(fā)的流行的 JavaScript 編譯器,其單一代碼庫包含了完整的項(xiàng)目及其所有插件。
此外,React[11]、Ember[12]和 Meteor[13]等前端框架都使用單一代碼庫。
然而,真正的問題是商業(yè)軟件是否能從單一代碼庫中獲益??紤]到這些優(yōu)點(diǎn)和缺點(diǎn),讓我們來看一些已經(jīng)嘗試過的公司的經(jīng)驗(yàn)。
Segment,告別多代碼庫
Alex Noonan 講述了一個(gè)關(guān)于告別多代碼庫的故事[14]。她所在的公司 Segment.com 提供活動(dòng)收集和轉(zhuǎn)發(fā)服務(wù),每個(gè)客戶都需要使用一種特殊格式的數(shù)據(jù)。因此,工程團(tuán)隊(duì)最初決定混合使用微服務(wù)和多代碼庫。
這一策略效果很好——隨著客戶基數(shù)的增長,他們擴(kuò)大了規(guī)模,沒有出現(xiàn)問題。但是,當(dāng)轉(zhuǎn)發(fā)目的地的數(shù)量超過 100 個(gè)時(shí),事情開始變得糟糕起來。維護(hù)、測試和部署超過 140 個(gè)代碼庫(每個(gè)代碼庫都有數(shù)百個(gè)日益分化的依賴關(guān)系)的管理負(fù)擔(dān)太高了。
“最終,團(tuán)隊(duì)發(fā)現(xiàn)他們無法取得進(jìn)展,三個(gè)全職工程師花費(fèi)了大部分時(shí)間來維持系統(tǒng)的運(yùn)行?!?/strong>
對(duì)于 Segment 來說,補(bǔ)救辦法就是合并,將所有的服務(wù)和依賴遷移到一個(gè)單一代碼庫中。他們必須協(xié)調(diào)共享庫并且測試所有內(nèi)容,雖然花了很大的代價(jià),但遷移非常成功,最終的結(jié)果是降低了復(fù)雜性,增加了可維護(hù)性。
“更快的速度就是證據(jù)。[…] 我們?cè)谶^去 6 個(gè)月里對(duì)庫的改進(jìn)比 2016 年全年都要多?!?/strong>
多年后,當(dāng)一個(gè)小組詢問她在微服務(wù)方面的經(jīng)驗(yàn)時(shí)[15],Alex 解釋了她遷移到單一代碼庫的原因:
“這并沒有像我們想象的那樣成為我們的優(yōu)勢。我們改變的主要?jiǎng)訖C(jī)是因?yàn)槭〉臏y試會(huì)影響到不同的東西。 [..] 把它們分成單獨(dú)的代碼庫只會(huì)讓情況變得更糟,因?yàn)橛锌赡苣銜?huì)過了 6 個(gè)月才集成某個(gè)庫的更新,結(jié)果測試完全被破壞了,而你又不想花時(shí)間去修復(fù)。我見過的成功分解為獨(dú)立代碼庫而不是服務(wù)的案例之一是,當(dāng)我們有一塊代碼在多個(gè)服務(wù)之間共享時(shí),我們想讓它成為一個(gè)共享庫。除此之外,我們發(fā)現(xiàn),即使轉(zhuǎn)向微服務(wù),我們?nèi)匀桓矚g單一代碼庫。”
Airbnb 和 monorail
Airbnb 的基礎(chǔ)設(shè)施工程師延斯·范德海格(Jens Vanderhaeghe)也講述了微服務(wù)和單一代碼庫是如何幫助他們向全球擴(kuò)張的[16]。
Airbnb 最初的版本被稱為“monorail”,是一個(gè)獨(dú)立的 Ruby on Rails 應(yīng)用程序。當(dāng)公司開始指數(shù)級(jí)增長時(shí),代碼庫也隨之增長。當(dāng)時(shí),Airbnb 實(shí)施了一項(xiàng)名為“民主發(fā)布(democratic releases)”的新發(fā)布政策,意味著任何開發(fā)者都可以在任何時(shí)候發(fā)布產(chǎn)品。
隨著 Airbnb 的擴(kuò)張,民主程序的限制也受到了考驗(yàn),合并更改變得越來越困難。Jens 的團(tuán)隊(duì)實(shí)施了一些緩解措施,比如合并隊(duì)列和增強(qiáng)監(jiān)控。這在一段時(shí)間內(nèi)有幫助,但從長遠(yuǎn)來看還是不夠。
Airbnb 的工程師們?yōu)榫S持 monorail 系統(tǒng)進(jìn)行了英勇的斗爭,但最終,經(jīng)過數(shù)周的辯論,他們決定將該應(yīng)用分割為多個(gè)微服務(wù)。因此,他們創(chuàng)建了兩個(gè)單一代碼庫:一個(gè)用于前端,一個(gè)用于后端。兩者都包含數(shù)百個(gè)服務(wù)、文檔、用于部署的 Terraform 和 Kubernetes 資源以及所有維護(hù)工具。
當(dāng)被問及單一代碼庫的優(yōu)勢時(shí),Jens 說道:
“我們不想處理所有這些微服務(wù)之間的版本依賴關(guān)系。[使用單一代碼庫]你可以通過一個(gè)提交在兩個(gè)微服務(wù)之間做出改變。[..] 我們可以圍繞一個(gè)代碼庫構(gòu)建所有工具。最大的賣點(diǎn)是你可以同時(shí)在多個(gè)微服務(wù)上做出改變。我們通過腳本檢測單一代碼庫中的哪些應(yīng)用程序會(huì)受到影響,然后部署這些應(yīng)用程序。我們獲得的主要好處是源代碼控制。”
Uber,來來回回
來自優(yōu)步(Uber)的艾米?盧西多(Aimee Lucido)描述了從單一代碼庫到多代碼庫再切換回來的過程[17]。
當(dāng)時(shí),她正在 Android 客戶團(tuán)隊(duì)工作。他們從一開始就使用單一代碼庫,但是經(jīng)過 5 年的快速發(fā)展,單一代碼庫的問題開始顯現(xiàn)出來。
“我們開始遭遇可怕的 IDE 死鎖,我們甚至不能在 Android Studio 中翻看代碼,否則 IDE 就會(huì)沒有任何反應(yīng)?!?/strong>
問題并不只是發(fā)生在 IDE,也影響到 Git,構(gòu)建過程一拖再拖。更糟糕的是,他們經(jīng)常會(huì)破壞主線,這讓他們無法構(gòu)建任何東西。
“公司規(guī)模越大,就越頻繁地遇到主線被破壞的情況?!?/strong>
當(dāng) Uber 達(dá)到中等規(guī)模時(shí),團(tuán)隊(duì)決定切換到多代碼庫,這解決了很多問題。Uber 的工程師們喜歡這樣的狀態(tài):他們可以擁有一部分代碼,并且只對(duì)這部分代碼負(fù)責(zé)。
“如果你只構(gòu)建地圖應(yīng)用,那么你可以很快構(gòu)建出來,這太棒了?!?/strong>
但故事并沒有到此結(jié)束。又過了一段時(shí)間,多代碼庫策略開始顯示出它的弱點(diǎn)。這一次不僅僅是關(guān)于技術(shù)問題,而是關(guān)于人們?nèi)绾魏献?。團(tuán)隊(duì)被分解成多個(gè)豎井,管理數(shù)千個(gè)代碼庫的開銷消耗了大量寶貴的時(shí)間。
每個(gè)團(tuán)隊(duì)都有自己的編碼風(fēng)格、框架和測試實(shí)踐。管理依賴關(guān)系也變得更加困難,依賴關(guān)系的惡魔抬起了它丑陋的頭,這使得最終將所有內(nèi)容整合到一個(gè)產(chǎn)品中變得非常困難。
就在這個(gè)時(shí)候,Uber 的工程師們?cè)俅魏献鳎瑳Q定再給單一代碼庫一次機(jī)會(huì)。有了更多的資源并且提前知道他們將面臨的問題,他們選擇投資于工具:他們修改了 IDE,實(shí)現(xiàn)了合并隊(duì)列,并使用增量構(gòu)建來加快部署。
“當(dāng)你發(fā)展到一個(gè)大公司的規(guī)模時(shí),可以投入資源,讓你的大公司感覺像一個(gè)小公司,把缺點(diǎn)變成優(yōu)點(diǎn)。”
Pinterest,全力投資單一代碼庫
讓我們以一家正在經(jīng)歷三年轉(zhuǎn)型的公司作為總結(jié):Pinterest。他們的努力包括了兩個(gè)方面。首先,將 1300 多個(gè)代碼庫切換到四個(gè)單一代碼庫中。其次,將數(shù)百個(gè)依賴項(xiàng)整合到一個(gè)完整的 Web 應(yīng)用程序中。
他們?yōu)槭裁匆@么做?Eden JnBaptiste[18]解釋說,多代碼庫使得他們很難重用代碼。情況是一樣的:代碼太過分散,每個(gè)團(tuán)隊(duì)都有自己的倉庫,有自己的風(fēng)格和結(jié)構(gòu)。構(gòu)建過程質(zhì)量標(biāo)準(zhǔn)是高度可變的,構(gòu)建和部署變得非常困難。
Pinterest 發(fā)現(xiàn),基于主干開發(fā)[19]配合單一代碼庫有助于取得進(jìn)展?;谥鞲傻拈_發(fā)的基礎(chǔ)是只使用臨時(shí)分支,盡可能頻繁的合并到主分支上,從而減少合并沖突的機(jī)會(huì)。
“將所有代碼放在一個(gè)倉庫中有助于我們減少(構(gòu)建系統(tǒng)中的)反饋循環(huán)。”
對(duì)于 Pinterest,單一代碼庫提供了一致的開發(fā)工作流。發(fā)布實(shí)踐的自動(dòng)化、簡化和標(biāo)準(zhǔn)化允許他們減少文檔,讓開發(fā)人員專注于編寫代碼。
投資于工具
如果我們必須從所有這些故事中吸取一個(gè)教訓(xùn),那就是正確的工具是有效的單一代碼庫的關(guān)鍵——需要重寫思考構(gòu)建和測試。我們可以使用智能構(gòu)建系統(tǒng),而不是在每次更新時(shí)重新構(gòu)建完整的倉庫,它需要了解項(xiàng)目結(jié)構(gòu),并且只對(duì)自上次提交以來變更的部分進(jìn)行操作。
[圖片上傳失敗...(image-20f388-1671887905137)]
我們大多數(shù)人都沒有谷歌或 Facebook 的資源。那我們能做什么呢?幸運(yùn)的是,許多大公司的構(gòu)建系統(tǒng)都是開源的:
Bazel[20]:由谷歌發(fā)布,部分基于他們自己的構(gòu)建系統(tǒng)(Blaze)。Bazel 支持多種語言,并支持大規(guī)模構(gòu)建和測試。
Buck[21]: Facebook 開源的快速構(gòu)建系統(tǒng),支持在多種語言和平臺(tái)上進(jìn)行不同的構(gòu)建。
Pands[22]:Pands 構(gòu)建系統(tǒng)是與 Twitter、Foursquare 和 Square 合作創(chuàng)建的。目前只支持 Python,后續(xù)還將支持更多的語言。
RushJS[23]:微軟用于 JavaScript 的可擴(kuò)展的單一代碼庫管理器,能夠從一個(gè)代碼庫構(gòu)建和部署多個(gè)包。
Monorepos 正在獲得越來越多的關(guān)注,尤其是在 JavaScript 方面,如下項(xiàng)目所示:
Lerna[24]:JavaScript 的單一代碼庫管理器,可以與 React、Angular 或 Babel 等流行框架集成。
Yarn workspace[25]:用一個(gè)命令在多個(gè)地方安裝和更新 Node.js 的依賴項(xiàng)。
ultra-runner[26]:JavaScript 單一代碼庫管理腳本,支持 Yarn、pnpm 和 Lerna 插件,支持并行構(gòu)建。
Monorepo builder[27]:通過單一代碼庫安裝和更新 PHP 包。
擴(kuò)展代碼庫
源代碼控制是單一代碼庫的另一個(gè)痛點(diǎn),這些工具可以幫助您擴(kuò)展存儲(chǔ)庫:
Virtual Filesystem for Git(VFS)[28]:為 Git 添加流支持。VFS 可以根據(jù)需要從 Git 倉庫下載對(duì)象。這個(gè)項(xiàng)目最初是為了管理 Windows 代碼庫(最大的 Git 倉庫)而創(chuàng)建的,只能在 Windows 上使用,但已經(jīng)宣布要支持 MacOS。
Large File Storage[29]:Git 的開源擴(kuò)展,為大文件提供了更好的支持。安裝了這個(gè)擴(kuò)展,就可以跟蹤任何類型的文件,并將它們無縫的上傳到云存儲(chǔ)中,從而釋放代碼庫的空間,使 push 和 pull 的速度更快。
Mercurial[30]:Git 的替代方案,Mercurial 是一個(gè)分布式版本控制工具,專注于速度。Facebook 使用 Mercurial,多年來已經(jīng)發(fā)布了許多提高速度的補(bǔ)丁[31]。
Git CODEOWNERS[32]:允許定義哪個(gè)團(tuán)隊(duì)擁有代碼庫中的子目錄,當(dāng)有人提交 pull request 或 push 代碼到受保護(hù)的分支時(shí),代碼所有者會(huì)自動(dòng)被要求進(jìn)行審查。GitHub 和 GitLab 都支持這一特性。
Monorepo 管理最佳實(shí)踐
基于這些單一代碼庫的故事,我們可以定義一套最佳實(shí)踐:
定義一個(gè)便于探索的統(tǒng)一的目錄組織。
維護(hù)分支整潔,保持較小的分支,考慮采用基于主干的開發(fā)。
為每個(gè)項(xiàng)目使用固定的依賴項(xiàng),一次升級(jí)所有依賴項(xiàng),迫使每個(gè)項(xiàng)目都跟上依賴項(xiàng)。只為真正的例外情況保留例外。
如果正在使用 Git,學(xué)習(xí)如何使用 shallow clone[33]和 filter-branch[34]來處理大容量代碼庫。
貨比三家,尋找像 Bazel 或 Buck 這樣的智能構(gòu)建系統(tǒng),以加速構(gòu)建和測試。
如果需要限制對(duì)某些項(xiàng)目的訪問,請(qǐng)使用 CODEOWERS。
使用 Semaphore[35]等 CI/CD 云平臺(tái)來大規(guī)模測試和部署應(yīng)用程序。
你應(yīng)該使用單一代碼庫嗎?
看情況而定,沒有適合所有用例的答案,有些公司可能會(huì)暫時(shí)選擇單一代碼庫,然后決定需要轉(zhuǎn)向多代碼庫,或者相反,而其他人可能會(huì)選擇混合代碼庫。當(dāng)你有疑問的時(shí)候,考慮一下從單一代碼庫到多代碼庫通常比反過來要更容易。但千萬不要忽視這一點(diǎn),最終,這與技術(shù)無關(guān),而是與工作文化和溝通有關(guān)。所以,根據(jù)你想要的工作方式來做決定。
References:
[1] https://semaphoreci.com/blog/what-is-monorepo
[2] https://semaphoreci.com/blog/cicd-pipeline
[3] https://semaphoreci.com/continuous-integration
[4] https://semaphoreci.com/cicd
[5] https://semaphoreci.com/blog/build-stage
[6] https://docs.freebsd.org/en_US.ISO8859-1/articles/committers-guide/article.html
[9] https://github.com/NixOS/nixpkgs/
[10] https://github.com/babel/babel/blob/master/doc/design/monorepo.md
[11] https://github.com/facebook/react/tree/master/packages
[12] https://github.com/emberjs/ember.js/tree/master/packages
[13] https://github.com/meteor/meteor/tree/devel/packages
[14] https://segment.com/blog/goodbye-microservices/
[15] https://www.infoq.com/articles/microservices-from-trenches-lessons-challenges/
[16] https://www.youtube.com/watch?v=sakGeE4xVZs
[17] https://www.youtube.com/watch?v=lV8-1S28ycM
[18] https://www.youtube.com/watch?v=r5KHQnS6uP8
[19] https://trunkbaseddevelopment.com/
[20] https://bazel.build/
[21] https://buck.build/
[22] http://www.pantsbuild.org/
[23] https://rushjs.io/
[24] https://github.com/lerna/lerna
[25] https://classic.yarnpkg.com/en/docs/workspaces/
[26] https://github.com/folke/ultra-runner
[27] https://github.com/Symplify/MonorepoBuilder
[29] https://git-lfs.github.com/
[30] https://www.mercurial-scm.org/
[31] https://engineering.fb.com/2014/01/07/core-data/scaling-mercurial-at-facebook/
[32] https://help.github.com/articles/about-codeowners/
[33] https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/
原文鏈接:【https://xie.infoq.cn/article/4f870ba6a7c8e0fd825295c92】。