跳到主要內容
黯羽輕揚每天積累一點點

理解 Git Submodules

免費2018-10-12#Tool#submodules vs. monorepo#submodules vs. npm packages#git子模块#git多repo管理#git multi repo

比起 npm package、monorepo 等依賴管理方案,submodules 有什麼優勢?

一.背景

經常面臨一些場景,想要把大代碼庫(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

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論