一.背景
經常面臨一些場景,想要把大代碼庫(repo)拆分成多個小的 repo,例如:
-
現有代碼庫體積龐大,且模塊管理混亂,經常容易錯改別人的東西
-
某個模塊需要單獨構建,比如 jQuery 項目中的 React 試點、Node 項目中的純前端部分、Electron 項目中的 UI 部分等等
-
某個模塊是黑盒依賴項,開發中僅依賴其構建後的版本,比如框架類庫等
針對諸如此類的情況,一般有 3 種解決方案:
-
npm package:把依賴項拆出去作為 npm package,代碼庫隨之獨立出去
-
monorepo:單 repo 體積龐大沒關係,分模塊管理好就行
-
git submodules:把依賴項拆分到多個獨立 repo,作為主 repo 的 submodule
npm package
npm package 的優勢在於成熟的依賴管理機制,規範且易用,缺點是主項目只能通過 package 版本號獲取獨立模塊的更新,在主項目需要與子模塊聯調的場景就會非常麻煩:
主項目:調不通啊
子模塊:有點問題,我改一下...改版本號 - 構建 - 發布 npm package
主項目:更新依賴,再試...還是調不通啊
子模塊:還有點問題...
頻繁發版比較蠢,可以本地修改構建再拷貝一份過去,但還是有些麻煩。當然,通常可以通過 mock 接口或數據把聯調依賴拆解开,但有時候 mock 全套 API 成本比較高,而且假的勢必沒有真的好用
monorepo
monorepo 主張不拆分 repo,而是在單 repo 裡統一管理各個模塊的構建流程、版本號等等,並且鼓勵改別人的代碼
這在模塊邊界清晰、owner 明確的項目中很合適(如 React、Babel 等),但實際應用中,業務 repo 很難保持清晰的模塊邊界與依賴關係,此時 monorepo 就變得理想化了
git submodules
git submodules 提供了一種類似於 npm package 的依賴管理機制,包括添加、刪除、更新依賴項等功能,區別在於前者所管理的依賴是子模塊的源碼,後者管理的是子模塊的構建產物。在這一點上,git submodules 與 monorepo 一致(都關心子模塊的源碼)
這樣主項目需要與子模塊頻繁聯調時的麻煩就不復存在了,因為主項目拉取到的 submodules 都是完整 repo,可以直接修改 - 構建 - 提交
二.submodules 與 monorepo
從結構上看,submodules 項目的主 repo 與 monorepo 很像,相當於把 monorepo 裡的各模塊抽離到了獨立 repo,僅記錄主 repo 所依賴的各模塊版本號(commit hash 形式)
具體的,monorepo 在單 repo 裡存放所有子模塊源碼(packages/xxx/src),例如:
react/
packages/
react-dom/
/src
react-reconciler/
/src
...
而submodules 只在主 repo 裡存放所有子模塊「索引」(repo url + branch name + commit hash),例如:
# 主 repo 的.gitmodules 文件
[submodule "react-dom"]
path = packages/react-dom
url = https://github.com/facebook/react.git
branch = master
[submodule "react-reconciler"]
path = packages/react-reconciler
url = https://github.com/facebook/react.git
branch = master
...
主 repo 裡只保留對應的空目錄作為子模塊的「坑」,並不存其放源碼:
react/
packages/
react-dom/ # 空目錄
react-reconciler/ # 空目錄
拉取所有 submodules 依賴後,實際目錄結構如下:
react/
packages/
react-dom/
/src
react-reconciler/
/src
...
主 repo 並不追蹤子模塊源碼,僅記錄其版本號(commit hash 形式):
# 輸出空,表示不追蹤子模塊 src
$ git ls-tree -r master | grep packages/react-dom/src
# 查看主 repo 裡被 git 追蹤的子模塊坑位
$ git ls-tree -r master | grep ' commit'
160000 commit 3edf340cee50fd4bc918a0a95b438a30447ae042 packages/react-dom
160000 commit 373f207b09a7bf900fa82c3188aeefdc9ce6146c packages/react-reconciler
...
P.S.git ls-tree 的輸出格式含義,見 Output Format
三.具體用法
git submodule 命令用來管理子模塊:
$ git submodule --help
git-submodule - Initialize, update or inspect submodules
# 初始化
git submodule init
# 增
git submodule add
# 刪
git submodule deinit
# 改(版本控制)
git submodule update
添加子模塊依賴
$ cd ./react
# 添加依賴
$ git submodule add -b master https://github.com/path-to/react-dom.git src/packages/react-dom
會在主 repo 創建一個 src/packages/react-dom 空目錄,作為子模塊的坑位。實際上,add 過程主要發生了 3 件事:
-
clone 一份子模塊 repo 到主 repo 的 git 緩存目錄裡,例如
.git/modules/src/packages/react-dom -
創建坑位空目錄,並把子模塊 repo 的最新 commit hash 與之關聯
-
在主 repo 根目錄按需創建
.gitmodules文件,記錄子模塊 repo 地址(url),分支名(branch)以及坑位路徑(path)
然後提交這些子模塊配置:
$ git add ./src/packages/react-dom ./.gitmodules
$ git commit -m "build: add react-dom submodule"
$ git push origin master
接下來本地拉取子模塊完成初始化:
# 初始化子模塊
$ git submodule update --init
會把子模塊 repo clone 到 src/packages/react-dom 目錄下,實際發生了 2 件事:
-
檢查緩存是否存在 clone 好的子模塊 repo(比如 clone 來的主 repo 並没
add過,就不存在緩存),按需 clone -
在子模塊 repo 根目錄創建
.git/config,記錄其 repo 地址(url)
初始化子模塊
在 clone 含有 submodules 的 repo 後,要進行初始化:
# 創建一些本地配置
$ git submodule init
# 拉取各子模塊 repo
$ git submodule update --init
也可以在 clone 主 repo 時,通過 --recursive 選項也能完成上面兩步工作:
$ git clone git://gihub.com/path-to/main-repo.git --recursive
拉取子模塊更新
更新所有子模塊:
$ git submodule update --remote
會拉取子模塊對應分支的最新代碼,如有更新,佔位目錄的 git 狀態會發生變化:
$ git status
modified: src/packages/react-dom (new commits)
實際上是 commit hash 發生了變化:
$ git diff
diff --git a/src/packages/react-dom b/src/packages/react-dom
index 3edf340cee..d056efbc62 160000
--- a/src/packages/react-dom
+++ b/src/packages/react-dom
@@ -1 +1 @@
-Subproject commit 3edf340cee50fd4bc918a0a95b438a30447ae042
+Subproject commit d056efbc62cbf976b4ef83e70d7019fba4506e85
P.S. submodules 裡的 commit hash 相當於 npm package 的 dependencies 版本號
控制依賴項版本
想要更新主 repo 所依賴的子模塊版本的話,提交這個 commit hash 變更:
$ git add src/packages/react-dom
$ git commit -m "build: update react-dom submodule"
$ git push origin master
否則不加 --remote 選項滾回當前依賴版本:
$ git submodule update
改動子模塊代碼
子模塊是獨立 repo,正常操作即可:
$ cd ./packages/react-dom
# 注意切分支,通常是 detached 狀態
$ git checkout master
$ git add .
$ git commit -m 'feat: xxx'
$ git push origin master
之後,主 repo 就能通過 git submodule update --remote 拉取到最新版本,再由主 repo 決定是否要升級其依賴的子模塊版本
對每個子模塊執行相同的 git 命令
提供了 foreach 命令來對子模塊進行批處理,例如:
# 進入每個子模塊目錄,並執行 git stash
$ git submodule foreach 'git stash'
# 統一開 feature 分支
$ git submodule foreach 'git checkout -b featureA'
存在多個子模塊依賴時,這個命令相當好用
P.S. 關於 submodules 的更多用法,見 7.11 Git Tools - Submodules
四.常見問題
子模塊分支處於 detached 狀態
每次執行 git submodule update --remote 後,子模塊會處於 detached 狀態,例如:
$ cd ./packages/react-dom
$ git branch
* (HEAD detached at ac4d1fc)
master
設計如此,沒有太好的解決辦法:
It's also important to realize that a submodule reference within the host repository is not a reference to a specific branch of that submodule's project, it points directly to a specific commit (or SHA1 reference), it is not a symbolic reference such as a branch or tag. In technical terms, it's a detached HEAD pointing directly to the latest commit as of the submodule add.
因此在改動子項目代碼之前,需要手動切換到 master 分支:
$ git checkout master
$ git add .
$ git commit -m 'feat: xxx'
$ git push origin master
本地子模塊緩存
當子模塊 repo 發生遷移時,進行 git submodule add 可能會遇到本地緩存的問題:
$ git submodule add ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git projets/fdf
A git directory for 'projets/fdf' is found locally with remote(s): origin ssh://git@XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git If you want to reuse this local git directory instead of cloning again from ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git use the '--force' option. If the local git directory is not the correct repo or you are unsure what this means choose another name with the '--name' option.
需要先刪掉原配置(第 2 第 3 步),再本地緩存的子模塊信息(第 1 第 4 步):
# 1. 刪掉 git 緩存及物理文件
$ git rm --cached path_to_submodule
$ rm -rf path_to_submodule
# 2. 刪掉.gitmodules 裡該子模塊的相關配置
$ vi .gitmodules
[submodule "path_to_submodule"]
path = path_to_submodule
url = https://github.com/path_to_submodule
# 3. 刪掉.git/config 裡該子模塊相關配置
$ vi .git/config
[submodule "path_to_submodule"]
url = https://github.com/path_to_submodule
# 4. 刪掉子模塊緩存
$ rm -rf .git/modules/path_to_submodule
清理完成之後重新 git submodule add 即可
P.S. 第 4 步中,子模塊的緩存位置可以通過如下命令查看:
$ cat path_to_submodule/.git
gitdir: ../.git/modules/path_to_submodule
P.S. 更多常見問題,見 Using Git Submodules
暫無評論,快來發表你的看法吧