Git 可以做到 repository 中嵌入其他 git repositories, 相當於版本控制原本的 repository 之外,也把內嵌的 repositories 也納入版本控制的範圍,這功能被稱為 submodule 。
不過 submodule 並不是將內嵌 repository 的所有檔案都做額外的版本控制,而是類似於做一個指標(pointer),將 submodule 的版本指向內嵌的 repository 的某 1 個 commit id 。
Submodule 適合應用在多個 repository 共享 library, utility functions 的情境,可以節省各個 repository 重複開發的成本,例如下圖, repo A 與 repo B 共用同一個 submodule, 所以 repo A 與 repo B 都只要共同維護一份 submodule 即可:
p.s. 另 1 種管理方式是將 submodule 變成可供安裝的套件,例如 npm 套件、 pip 套件等等⋯⋯
另外,由於 submodule 的運作類似於做一個指標(pointer),將 submodule 的版本指向內嵌的 repository 的某 1 個 commit id, 所以 repo A 與 repo B 可以指向各自的版本,也就是不同的 commit id, 所以不會因為某人更新了 submodule 之後,導致其他 repository 的版本跑掉(除非某人以暴力 rebase 或者砍掉 submodule 導致其他 repository 找不到對應的 commit id),本文稍後將會以實際的 GitHub 範例展示指向 commit id 的部分。
新增 submodule
新增 submodule 的指令為:
$ git submodule add <repository> <path>
例如以下範例將 mysubmodule
指向 repository https://github.com/username/gitsubmodule
:
$ git submodule add https://github.com/username/gitsubmodule mysubmodule
上述指令成功之後,會多出一個檔案 .gitmodules
& 一個檔案 mysubmodule
:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .gitmodules
new file: mysubmodule
.gitmodules
內容裡面會紀錄 submodule 的路徑以及指向的 repository, 例如:
[submodule "mysubmodule"]
path = mysubmodule
url = https://github.com/username/gitsubmodule
mysubmodule 內則是 https://github.com/username/gitsubmodule
repository 的內容。
git submodule add
指令建立檔案之後, submodule 的新增步驟仍未結束,仍需要進行 commit ,如此才能讓 Git 紀錄 repository 底下有多 1 個 submodule:
$ git commit -m 'commit submodule'
接著,我們可以將 repository 推送到(push)到 GitHub:
$ git push origin main
最後就可以在 GitHub 的頁面看到類似以下的畫面,其中 mysubmodule @ 4c2c45d
代表 submodule 的版本定在 https://github.com/username/gitsubmodule
這個 repository 的 commit id 4c2c45d
, 如果點進去該 submodule, 就會跳到相對應的 repository 與其相對應的 commit id:
更新 submodule
submodule 的更新就跟更新 Git repository 的方式一樣,都是在 submodule 內做完變更之後 commit, 例如以下更新 mysubmodule
內的 README.md 後,進行 commit:
$ cd mysubmodule
$ cat >> README.md
Hello
$ git commit -m 'update README.md'
這時候 mysubmodule 的所指向 commit id 仍未改變,所以執行 git status
指令後會看到類似以下的結果, Git 顯示 submodule 有新的變更:
$ cd ..
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: mysubmodule (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
只要透過 git add
與 git commit
指令將變更提交就可完成 submodule 的更新:
$ git add mysubmodule
$ git commit -m 'update submodule'
$ git push origin main
最後就可以在 GitHub 頁面看到 mysubmodule @
之後的 commit id 變了:
將 Submodule 指定到特定 commit id 或 tag
如果想將 submodule 指定到特定的 commit id 或 tag, 可以進到 submodule 後,再使用 git checkout <commit id 或 tag>
指令,將版本切換過去:
$ git checkout <commit id 或 tag>
例如:
$ cd mysubmodule
$ git checkout v1.0
接著回到原 repository 後,輸入指令 git status
, 就能夠看到 mysubmodule
已經被變更:
$ cd ..
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: mysubmodule (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
最後將 mysubmodule 提交,即完成將 submodule 指定到特定 commit id 或 tag:
$ git add mysubmodule
$ git commit -m 'stick submodule to v1.0'
Clone 有 submodule 的 repository
如果是 repository 早已有 submodule 的情況下,通常會遇到一種情況是——「 Clone repository 後, submodule 裡面沒有任何資料」
例如,當我們另外以指令 clone 前述範例之後:
$ git clone https://github.com/username/gittest.git
進到 mysubmodule 資料夾底下後,會發現沒有任何資料:
$ cd gittest
$ ls mysubmodule
這是由於 submodule 需要額外經過 2 道指令才可運作:
git submodule init
將 submodule 初始化git submodule update
將 submodule 下載回來
git submodule init
The default behavior of git submodule init is to copy the mapping from the
.gitmodules
file into the local ./.git/config file.
git submodule init
的作用在於將 . gitmodules
的設定寫道 repository 的 config 內,也就是寫入 ./.git/config
檔案內:
$ git submodule init
Submodule 'mysubmodule' (https://github.com/username/gitsubmodule) registered for path 'mysubmodule'
執行完上述指令,會發現 .git/config
內多了 submodule 的設定:
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/username/gittest.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[submodule "mysubmodule"]
active = true
url = https://github.com/username/gjtsubmodule
不過此時 submodule 的資料仍是空的,需要執行第 2 道指令才行。
git submodule update
Update the registered submodules to match what the superproject expects by cloning missing submodules, fetching missing commits in submodules and updating the working tree of the submodules.
git submodule update
的作用在於將 submodule 內的檔案/commit 等 clone 回來:
$ git submodule update
Cloning into '/Users/user/workspace/gittest/mysubmodule'...
Submodule path 'mysubmodule': checked out 'b1d45f5b18f760cb611347c3d2c8ede952e760d5'
懶人方法 git submodule update –init –recursive
git submodule init
與 git submodule update
其實可以合二為一,變成 git submodule update --init --recursive
,如此只要 1 道指令即可:
$ git submodule update --init --recursive
Submodule 'mysubmodule' (https://github.com/username/gjtsubmodule) registered for path 'mysubmodule'
Submodule path 'mysubmodule': checked out 'b1d45f5b18f760cb611347c3d2c8ede952e760d5'
最懶的 Clone 方式
Git 2.13 之後提供一同將 submodule 完整 clone 的指令, git clone --recurse-submodules <repository>
, 例如下列範例:
$ git clone --recurse-submodules https://github.com/username/gittest.git
刪除 git submodule
刪除 submodule 的方式也很直觀,只要執行 git rm <submodule>
即可,例如:
$ git rm mysubmodule
Git 會自動更新 .gitmodules
與將 submodule 刪除:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: .gitmodules
deleted: mysubmodule
最後只要 commit 即可刪掉 submodule 。
以上就是關於 Git submodule 的教學。
Happy Coding!