16年8月底,公司新啟動了一個D項目(代號),從敲下第一行代碼到如今,剛好1個年頭,我們已經在VCS上經歷了多次調整。
第一階段(2016.9 - 2017.2)
在這個階段,開發(fā)同學是2個人,需求快速迭代,此時在一個倉庫上開發(fā),工作流簡單,代碼規(guī)范等約束成本非常低,這也是最舒適的一個階段。
本地feature, 不推遠程,rebase到dev,保持線性,需求變化很快,甚至經常在開發(fā)階段要經常變動,此時線性的git tree的優(yōu)勢非常明顯,快速定位到具體的提交,再執(zhí)行后續(xù)的操作。
第二階段(2017.2 - 2017.4)
在2月份時,團隊成員增長到4個開發(fā)同學(后來在5月份變成5個),因為模塊間沒有一個明顯的界線,所以在開發(fā)時可能會有意、無意的碰到其它模塊的代碼,merge、rebase解決沖突的次數明顯上漲。
此外項目增長到一定規(guī)模,模塊之間存在較嚴重的耦合,同時一些模塊相對于穩(wěn)定,一直存在于主工程并不合適。
更重要的是,任何一處改動都需要編譯整個項目,每次編譯的等待時間都足以讓你懷疑人生。
模塊化勢在必行
Android官方提供一種模塊化方案,即模塊以Module被主工程依賴,第一階段中,我們已經對一些模塊或者Library進行抽離,但是還存在以下問題:
1、 穩(wěn)定的Lib作為Module存在并不合適,每次主工程的編譯都會帶動這些Lib的編譯,導致編譯效率降低,急需將這些Lib上傳到maven,從project依賴轉換為坐標依賴
2、 一些模塊存在于主工程,僅以package分離,我們需要將這些遷移到Module
改造后的架構:

還不夠!
1、 即使在做了上面的工作后,依然無法解決模塊的追溯問題,我們只能從整個倉庫的git歷史去追溯模塊的commit,出現問題檢索時,解決速度并不高效
2、 鑒于Gradle的生命周期:同步時,會掃描整個工程.gradle文件并執(zhí)行,即使我只編譯某個子模塊!
雖然我們基于模塊開發(fā)的效率已經大幅度提高,但是因為上述Gradle的特性,我們依然浪費了一些構建時間
我們決定一步到位,將子模塊全部遷移到獨立倉庫中!
每個模塊保證有獨立的追溯歷史,模塊測試放到獨立的倉庫里進行。
這樣遷移后,項目架構如下:

理想很豐滿,現實很骨感
在遷移后,我們的工作流轉變成:
開發(fā)feature時,會在對應子模塊工程里開發(fā),然后發(fā)布到maven,主工程依賴新的子模塊坐標。
如上的工作流其實沒有什么問題,但是:
1、 因為我們App體量并沒有足夠的大,業(yè)務雖然沒有16年時變動那么大,但是還是常有跨多模塊的需求,這種情況下,我們多是在主工程來測試的,如果過程有問題,就需要子模塊重新發(fā)布,主工程重新同步、編譯,而Andorid的同步速度大家是知道,如果開發(fā)額不夠順利,上面的步驟會多次重復...
2、 團隊4個開發(fā)同學,但是算上主工程有8個業(yè)務模塊,平均1個人對應2個模塊,這種規(guī)模之下,把所以子模塊都遷移到獨立倉庫后,維護成本真的很高,模塊的完全解耦,讓開發(fā)同學感到不適
3、 檢索代碼麻煩,主工程因為是坐標依賴,想搜索模塊的代碼,或者搜索某個方法的調用鏈都成為非常麻煩的事,而打開對應模塊工程去搜索,耗時且麻煩
4、 很多情況我們需要一起切換分支或者一起checkout到某一個commit節(jié)點,之前在一個倉庫時checkout即可,但現在多個倉庫很頭痛
以上導致一個比較嚴重的問題:代碼沒有很好的在可控范圍內,Review代碼,查找BUG等都變得困難。
第三階段(2017.4 - 至今)
為了解決上面的痛點,我們找到2個東西: Repo,Git Submodule
Repo
Repo是Google管理Android源碼的工具,是基于Git之上的構建工具,可以看作是Git的Wrapper,它提供一些命令集來操作其關聯的模塊。它引入一個Repo的角色來管理各個倉庫,這意味著主工程和子模塊是同等地位的。
使用Repo的話,模塊關系如下:

這樣的話,開發(fā)方式和之前沒有任何區(qū)別,依然是子模塊發(fā)布maven,主工程坐標依賴,來保證主工程和子模塊的版本關系。
Repo只是幫我們打包了多倉庫的 檢出、切換等Git命令,并沒有很好的解決上面提到的問題,所以Repo目前并不能解決我們的痛點,現階段并不適合我們。
Git Submodule:
Git Submodule是Git提供的功能,它允許你將一個 Git 倉庫作為另一個 Git 倉庫的子目錄,它能讓你將另一個倉庫克隆到自己的項目中,同時還保持提交的獨立。
在Submodule中,當子模塊的倉庫有新的commit時,主倉庫就會產生一個該模塊的SHA值改變,該SHA值紀錄了子模塊的對應commit,以此來連接宿主關系。
使用Git Submodule的話,模塊關系如下:

這個關系圖太熟悉了,不正是Android標準模塊化的關系圖嗎?!所以我們決定在這個看似很完美的方案上摸索。
在經過Git Submodule的改造后,架構圖如下:

在經歷一段適應期后,會發(fā)現Git Submodule的設計還是非常智慧的,但不可否認,不經任何裝飾的Git Submodule直接用到App的工作流中,還是挺坑的。
比如當你主工程檢出分支到某個commit時,子模塊并不跟隨切換到對應模塊,需要手動執(zhí)行 submodule update命令來讓子模塊切換到對應commit,但是這些更新后的子模塊會在游離的HEAD上,在該狀態(tài)上的commit是沒有意義的。
除此之外還有很多其它的坑,如果讓團隊成員每個人都搞懂這套繁瑣的機制并不現實,同時多倉庫的存在讓Git flow也繁瑣很多,所以為了解決這些問題,我提供了一個腳本命令工具:ggsm
ggsm
ggsm可以看做是Repo + Git Flow的集合,它提供一些命令,可以方便打包操作多倉庫,目標是像操作一個Git倉庫一樣操作主工程以及其Submodules,而目前我們確實也做到了。

ggsm是Git Submodule的Wrapper,它并不會影響我們平時的開發(fā)習慣,你依舊在某個倉庫里add ,commit - 當你要開發(fā)某個feature時,start命令幫你把所有倉庫都切換到新的feature分支,當子模塊都開發(fā)完成后,主工程里提交對應子模塊的SHA(類似上傳maven,主工程依賴新的坐標),然后使用merge finish推送代碼到遠程倉庫,最后通過mr命令提交MR。
可以看到 我們把Repo和Git Flow集成在一起了,這樣團隊成員對Git Submodule幾乎是無感知的,同時git flow也規(guī)范化了。
可以做的更多
事實上,ggsm承載了更多的功能:
1、 容錯處理機制,檢測模塊commit的完整性
2、 Git hooks,鉤子的更新、安裝都放到了ggsm內,這一過程透明,目前我們做了commit msg檢查、代碼規(guī)范檢查
3、CodeReview,目前我們把Merge Request也集成到ggsm里,這樣開發(fā)同學完成某個feature后會自動通過GitLab API創(chuàng)建MR,發(fā)送郵件通知等
關于Repo和Git Submodule
如果你的項目各個模塊非常獨立,體量也比較大的話,Repo是非常好的管理工具,總之:
沒有最好的方案,只有更適合的方案
軟鏈接
我們使用了軟鏈接,它將子模塊的Module軟鏈接帶主工程內,從而可以使用Project依賴。
在開發(fā)跨模塊的需求上Project依賴會讓你的開發(fā)效率大幅度提高,對于單一模塊的需求,我們打開對應的子模塊直接在其內開發(fā)即可。

未來
我們所做的一切,都是為了在 高可維護性和開發(fā)舒適性 上找到一個更好的平衡點。
對待子模塊,我們并不拘泥于一種形式,比如 Msg和Pay這2個子模塊是比較穩(wěn)定的,那么這2個模塊倉庫就完全不需要使用Git Submodule關聯,使用坐標依賴。
模塊足夠獨立或穩(wěn)定的時候,Git Submodule是多余的,反而會拖慢集成主工程時的效率。
此外目前的階段有足夠的靈活性,未來某個模塊足夠穩(wěn)定或獨立時,rm -rf submodule就可以解除宿主Git Submodule關系,轉變?yōu)樽鴺艘蕾嚰纯伞?/p>
目前來看當前的VCS以及模塊化方案已經算是一個很好的平衡點,足以應對當前階段的D項目。
未來我們也會繼續(xù)在VCS、工作流、自動化等方面做更深入的探索。