Git 子模組

2023-06-28 12:00:35

1 概覽

使用 Git 管理原始碼,進行子模組操作時,此文可作為參考(Lookup Cheat-Sheet)

2 何時使用 submodules

合適的場景:

  1. 子模組程式碼應獨立於其他應用者專案(container/container project)。不依賴,高內聚,同一程式碼庫共用於多個應用者專案。
  2. 程式碼規模大,若其中某個同樣大規模的程式碼模組長時間不需要更新,器皿專案無需每次拉取時都涉及該模組時,應將該模組獨立為子模組。方便合作程式碼貢獻者可以選擇只初始化一次,之後都不再提取(fetch)該子模組。
  3. 子模組與器皿專案應用不同的框架或語言。

與其使用 submodules,不如使用 subtrees 的場景:

  1. 需要讓某個模組具有子程式碼的概念,作為一個器件,依附於器皿專案當中,對其做客製化化修改,而不用去動器皿專案,只操作器件。同時具備管理一套程式碼庫的簡便性。
  2. 模組簡單,或模組有依賴器皿專案的程式碼,則不必拆分為單獨的專案倉儲,避免冗雜的本地/遠端參照(ref)管理。所有程式碼都是一套原生程式碼。
Submodules Subtrees
更復雜 (尤其對初學者) 更簡單
另一個倉儲的一個提交參照連結 程式碼提交會合併到器皿專案的提交歷史中
能夠單獨被存取(存在於中心伺服器中的獨立倉儲) 去中心化(直接通過器皿專案存取)
需要更多的步驟來操作 克隆,拉取,推播都與之前相同(當然也有針對子樹的命令)
不佔用器皿專案倉儲大小 直接佔用器皿專案倉儲容量

3 使用技巧及規範

3.1 為倉儲新增和設定子模組

# 新增子模組並跟蹤名稱為 branch_name 的分支,不加 -b 將會對後續子模組的跟蹤產生影響
git submodule add -b branch_name URL_to_Git_repo optional_directory_path

3.1.1 一般原則

  1. 為子模組設定分支。除了 git submodule add 時使用 -b 引數設定以外,還可以通過修改 .gitmodules 檔案更改。

  2. 設計好倉儲內子模組的存放路徑。除了 git submodule add 時通過 optional_directory_name 設定以外,還可以通過修改 .gitmodules 檔案更改。

注:除了直接用文字編輯器修改 .gitmodules 檔案以外,還可以通過命令進行修改(參考如下範例)。

# 檢視現有的 submodule 屬性
git config --file=.gitmodules -l

# 修改 submodule 屬性
git config --file=.gitmodules submodule.modulename.branch branch_name
git config --file=.gitmodules submodule.modulename.path path/to/submodule
git config --file=.gitmodules submodule.modulename.url URL_to_Git_repo

3.2 從一個已經設定好子模組的倉儲獲取子模組

# 克隆倉儲時,同時克隆倉儲的子模組,相當於進行了子模組的初始化和更新
git clone --recursive URL_to_Git_repo
# 克隆倉儲後,初始化並更新子模組,子模組就會參照子模組倉儲預設分支下最新一次提交
git submodule update --init
# 子模組設定變更、子模組變更(新增、更名、刪除等)
git pull
git submodule sync
git submodule update --init

3.2.1 對子模組倉儲直接進行程式碼編寫

# 簽出分支,使子模組具備拉取最新修改並直接連通父倉儲推播的能力
git submodule foreach "git checkout $(git config -f $toplevel/.gitmodules submodule.$name.branch || echo main)"
# 子模組遠端有更新,需要獲取。以下命令等同於進入子模組目錄執行 `git pull`。如果本地有未提交的修改,均需要合併操作,有衝突無法合併則獲取會失敗。
git submodule update --remote --merge
# 推播程式碼到遠端分支
git push --recurse-submodules=on-demand

3.2.2 不更改子模組程式碼

使用 detached HEAD,子模組連結的是提交 commit 的雜湊值 hash。無法拉取或提交程式碼,只能通過更新倉儲的子模組版本,再通過 git submodule update 命令更新子模組。

3.3 刪除已有子模組

# 從 .git/config 中刪除子模組
git submodule deinit -f path/to/submodule

# 從器皿專案(父專案)的 .git/modules 目錄中刪除子模組資料夾
rm -rf .git/modules/path/to/submodule

# 從 .gitmodules 中去掉子模組入口並刪除子模組所在物理路徑 path/to/submodule 下的所有檔案
git rm -f path/to/submodule

3.4 常用命令解析

# 為所有子模組執行 git 命令,-q 引數靜默模式,--recursive 迴圈操作所有子模組(包括子模組的子模組參照)
git submodule foreach "git command"
# 更新子模組
# --remote 從子模組遠端分支更新(最新),不加此引數則從父倉儲的子模組版本連結更新(對應提交雜湊值)
# --recursive 迴圈操作所有子模組(包括子模組的子模組參照)
# --merge,合併遠端分支的修改,有衝突需要解決衝突。不會 detach HEAD。
# --rebase,復原本地提交,臨時儲存並保留本地修改,應用遠端修改,再將本地修改合併,合併時同樣需要解決衝突。不會 detach HEAD。
git submodule update

注:對於 --remote 而言

  1. remote 使用 submodule.<name>.branch=branch_name 來確認分支名稱,若未設定則使用遠端倉儲設定的預設分支。再通過讀取子模組 branch.branch_name.remote=origin 來確認從遠端分支獲取的地址和 HEAD,預設為 detached HEAD,對應 remotes/origin/HEAD,獲取該分支最新提交。
  2. 如果本地子模組簽出了某個分支,則 HEAD attached 到對應分支,即 branch.branch_name.merge=refs/heads/branch_name
  3. 如果本地子模組當前的 HEAD commit hash 和 submodule.<name>.branch 中的值不一致的話,會導致子模組被 detach HEAD,不再追蹤某個分支,而是對映到 submodule.<name>.branch 最新的提交上。以上通常存在兩種情況,第一種是 HEAD 和 submodule.<name>.branch 分支本身就不同。第二種是本地子模組的提交落後於 submodule.<name>.branch 最新的提交。通過新增 --merge 引數可以避免 detachment。
  4. 如果子模組有本地提交併與遠端提交產生了衝突,需要用 --rebase 引數來解決。
  5. 子模組內使用 git pull 直接使用的是對應分支的 HEAD,為 branch.branch_name.merge=refs/heads/branch_name。對應上述第2點,和一個簽出 branch_name 分支後 HEAD attach 到 heads/branch_name 的子模組的 HEAD 是一致的。

3.5 常用 git 設定(推薦進行設定)

  1. 除了直接改 .gitconfig 檔案,還可通過命令檢視和更改
# 檢視現有 git 設定
git config --global -l

# 修改現有 git 設定
git config --global status.submoduleSummary true
git config --global diff.submodule log
git config --global alias.spush "push --recurse-submodules=on-demand"
  1. 檢視 submodule 提交變更
[status]
	submoduleSummary = true
  1. diff 時可以看到 submodule 的變更項
[diff]
	submodule = log
  1. 命令別名
[alias]
	sdiff = "!git diff && git submodule foreach 'git diff'"
	spull = "!git pull && git submodule sync && git submodule update --init"
	smerge = "submodule update --remote --merge"
	srebase = "submodule update --remote --rebase"
	scheckout = "submodule foreach 'git checkout $(git config -f $toplevel/.gitmodules submodule.$name.branch || echo main) && git pull'"
	spush = "push --recurse-submodules=on-demand"

4 參考連結