章節 ▾ 第二版

7.11 Git 工具 - 子模組

子模組

在處理一個專案時,經常會遇到需要從該專案內部使用另一個專案的情況。也許是第三方開發的庫,或者是你自己獨立開發並要在多個父專案中使用。這些場景通常會帶來一個問題:你希望將這兩個專案視為獨立但又能相互引用的關係。

舉個例子。假設你正在開發一個網站並建立 Atom 提要。與其自己編寫生成 Atom 的程式碼,不如決定使用一個庫。你很可能會需要從一個共享庫(如 CPAN 安裝或 Ruby gem)中包含此程式碼,或者將原始碼複製到你自己的專案中。包含庫的問題在於,很難對其進行任何定製,並且部署起來通常更麻煩,因為你需要確保每個客戶端都有該庫可用。將程式碼複製到專案中的問題在於,當你能獲取上游更改時,你所做的任何自定義更改都很難合併。

Git 使用子模組來解決這個問題。子模組允許你將一個 Git 倉庫作為另一個 Git 倉庫的子目錄。這使你可以在專案中克隆另一個倉庫,並保持提交的獨立性。

開始使用子模組

我們將透過開發一個已拆分為主專案和幾個子專案的簡單專案來介紹。

讓我們先將一個現有的 Git 倉庫新增為我們正在處理的倉庫的子模組。要新增新的子模組,請使用 git submodule add 命令,並附帶你想要開始跟蹤的專案(絕對或相對)URL。在本例中,我們將新增一個名為“DbConnector”的庫。

$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

預設情況下,子模組會將子專案新增到與倉庫同名的目錄中,在本例中為“DbConnector”。如果你希望它放在其他位置,可以在命令末尾新增一個不同的路徑。

如果你此時執行 git status,你會注意到幾件事情。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   .gitmodules
	new file:   DbConnector

首先,你應該注意到新的 .gitmodules 檔案。這是一個配置檔案,它儲存了專案 URL 與你將其拉入的本地子目錄之間的對映關係。

[submodule "DbConnector"]
	path = DbConnector
	url = https://github.com/chaconinc/DbConnector

如果你有多個子模組,此檔案中將有多個條目。需要注意的是,此檔案會與你的其他檔案(如 .gitignore 檔案)一樣被版本控制。它會與專案的其餘部分一起被推送和拉取。這樣,克隆此專案的其他人就知道從哪裡獲取子模組專案。

注意

由於 .gitmodules 檔案中的 URL 是其他人首先嚐試克隆/獲取的 URL,因此請務必使用他們可以訪問的 URL。例如,如果你使用不同的 URL 進行推送和拉取,請使用其他人可以訪問的 URL。你可以使用 git config submodule.DbConnector.url PRIVATE_URL 在本地覆蓋此值以供自己使用。在適用時,相對 URL 會很有幫助。

git status 輸出中的另一個條目是專案資料夾條目。如果你對此執行 git diff,你會看到一些有趣的內容。

$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

雖然 DbConnector 是工作目錄中的一個子目錄,但 Git 將其視為子模組,並且在你不在此目錄中時不會跟蹤其內容。相反,Git 將其視為來自該倉庫的特定提交。

如果你想要更友好的 diff 輸出,可以將 --submodule 選項傳遞給 git diff

$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+       path = DbConnector
+       url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

當你提交時,你會看到類似這樣的內容。

$ git commit -am 'Add DbConnector module'
[master fb9093c] Add DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

請注意 DbConnector 條目的 160000 模式。這是 Git 中的一種特殊模式,基本上意味著你正在將一個提交記錄為一個目錄條目,而不是子目錄或檔案。

最後,推送這些更改。

$ git push origin master

克隆帶有子模組的專案

在這裡,我們將克隆一個包含子模組的專案。當你克隆此類專案時,預設情況下會獲得包含子模組的目錄,但其中還沒有任何檔案。

$ git clone https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
$ cd MainProject
$ ls -la
total 16
drwxr-xr-x   9 schacon  staff  306 Sep 17 15:21 .
drwxr-xr-x   7 schacon  staff  238 Sep 17 15:21 ..
drwxr-xr-x  13 schacon  staff  442 Sep 17 15:21 .git
-rw-r--r--   1 schacon  staff   92 Sep 17 15:21 .gitmodules
drwxr-xr-x   2 schacon  staff   68 Sep 17 15:21 DbConnector
-rw-r--r--   1 schacon  staff  756 Sep 17 15:21 Makefile
drwxr-xr-x   3 schacon  staff  102 Sep 17 15:21 includes
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 scripts
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 src
$ cd DbConnector/
$ ls
$

DbConnector 目錄存在,但為空。你必須從主專案中執行兩個命令:git submodule init 來初始化你的本地配置檔案,以及 git submodule update 來從該專案獲取所有資料並檢出你的父專案中列出的相應提交。

$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

現在,你的 DbConnector 子目錄的狀態與你之前提交時的狀態完全一致。

還有一種稍微簡單的方法。如果你將 --recurse-submodules 傳遞給 git clone 命令,它將自動初始化並更新倉庫中的每個子模組,包括巢狀的子模組(如果倉庫中的任何子模組本身也有子模組)。

$ git clone --recurse-submodules https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

如果你已經克隆了專案但忘記了 --recurse-submodules,你可以透過執行 git submodule update --init 來合併 git submodule initgit submodule update 步驟。要初始化、獲取和檢出任何巢狀的子模組,可以使用萬無一失的 git submodule update --init --recursive

使用子模組處理專案

現在我們有了一個包含子模組的專案副本,並將與隊友就主專案和子模組專案進行協作。

從子模組遠端拉取上游更改

在專案中使用子模組的最簡單模型是,如果你只是在消費一個子專案,並且想不時地獲取它的更新,但實際上並沒有在你簽出的內容中進行任何修改。讓我們來做一個簡單的例子。

如果你想檢查子模組是否有新工作,可以進入該目錄並執行 git fetch,然後 git merge 上游分支來更新原生代碼。

$ git fetch
From https://github.com/chaconinc/DbConnector
   c3f01dc..d0354fc  master     -> origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
 scripts/connect.sh | 1 +
 src/db.c           | 1 +
 2 files changed, 2 insertions(+)

現在,如果你回到主專案並執行 git diff --submodule,你就能看到子模組已更新,並獲得已新增到其中的提交列表。如果你不想每次執行 git diff 時都輸入 --submodule,你可以透過將 diff.submodule 配置值設定為“log”來將其設定為預設格式。

$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

如果你此時提交,你將把子模組鎖定為包含新程式碼,供其他人更新。

還有一種更簡單的方法,如果你不想手動獲取和合並子目錄。如果你執行 git submodule update --remote,Git 會進入你的子模組併為你獲取和更新。

$ git submodule update --remote DbConnector
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'

此命令預設會假設你想將簽出更新到遠端子模組倉庫的預設分支(由遠端的 HEAD 指向)。但是,如果你願意,你可以將其設定為其他值。例如,如果你想讓 DbConnector 子模組跟蹤該倉庫的“stable”分支,你可以將其設定在你的 .gitmodules 檔案中(這樣其他人也會跟蹤它),或者只在你本地的 .git/config 檔案中。讓我們將其設定在 .gitmodules 檔案中。

$ git config -f .gitmodules submodule.DbConnector.branch stable

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'

如果你省略 -f .gitmodules,它只會為你做出更改,但跟蹤這些資訊與倉庫一起更有意義,這樣其他人也會得到這些資訊。

此時,如果我們執行 git status,Git 會顯示我們在子模組上有“新提交”。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

  modified:   .gitmodules
  modified:   DbConnector (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

如果你設定了配置項 status.submodulesummary,Git 還會顯示你子模組更改的簡要摘要。

$ git config status.submodulesummary 1

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   .gitmodules
	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c3f01dc...c87d55d (4):
  > catch non-null terminated lines

此時,如果我們執行 git diff,我們可以看到我們修改了 .gitmodules 檔案,並且我們已經拉取了大量提交,準備提交到我們的子模組專案中。

$ git diff
diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
 Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

這非常酷,因為我們可以實際看到即將提交到我們子模組中的提交日誌。一旦提交,你也可以在執行 git log -p 時事後看到這些資訊。

$ git log -p --submodule
commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Sep 17 16:37:02 2014 +0200

    updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

當你執行 git submodule update --remote 時,Git 預設會嘗試更新 **所有** 子模組。如果你有很多子模組,你可能只想傳遞你想要嘗試更新的子模組的名稱。

從專案遠端拉取上游更改

現在讓我們扮演你的協作者的角色,他擁有 MainProject 倉庫的本地克隆。僅僅執行 git pull 來獲取你新提交的更改是不夠的。

$ git pull
From https://github.com/chaconinc/MainProject
   fb9093c..0a24cfc  master     -> origin/master
Fetching submodule DbConnector
From https://github.com/chaconinc/DbConnector
   c3f01dc..c87d55d  stable     -> origin/stable
Updating fb9093c..0a24cfc
Fast-forward
 .gitmodules         | 2 +-
 DbConnector         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c87d55d...c3f01dc (4):
  < catch non-null terminated lines
  < more robust error handling
  < more efficient db routine
  < better connection routine

no changes added to commit (use "git add" and/or "git commit -a")

預設情況下,git pull 命令會遞迴地獲取子模組更改,正如我們在上面第一個命令的輸出中所看到的。但是,它 **不會更新** 子模組。這可以透過 git status 命令的輸出顯示,該命令顯示子模組“已修改”,並有“新提交”。更重要的是,顯示新提交的括號指向左側(<),表明這些提交已記錄在 MainProject 中,但不存在於本地 DbConnector 簽出中。要完成更新,你需要執行 git submodule update

$ git submodule update --init --recursive
Submodule path 'vendor/plugins/demo': checked out '48679c6302815f6c76f1fe30625d795d9e55fc56'

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

請注意,為了安全起見,你應該使用 --init 標誌執行 git submodule update,以防 MainProject 你剛剛拉取的提交添加了新的子模組,並且使用 --recursive 標誌,如果任何子模組包含巢狀的子模組。

如果你想自動化此過程,可以(從 Git 2.14 開始)將 --recurse-submodules 標誌新增到 git pull 命令中。這將在拉取後立即執行 git submodule update,使子模組處於正確狀態。此外,如果你想讓 Git 始終使用 --recurse-submodules 進行拉取,你可以將配置選項 submodule.recurse 設定為 true(這適用於 git pull,從 Git 2.15 開始)。此選項將使 Git 對所有支援 --recurse-submodules 的命令(git clone 除外)使用 --recurse-submodules 標誌。

在拉取超專案更新時可能會出現一種特殊情況:上游倉庫在你拉取的某個提交中更改了 .gitmodules 檔案中子模組的 URL。例如,當子模組專案更改其託管平臺時,可能會發生這種情況。在這種情況下,如果超專案引用了子模組遠端中本地配置的子模組提交,那麼 git pull --recurse-submodulesgit submodule update 可能會失敗。為了解決這種情況,需要使用 git submodule sync 命令。

# copy the new URL to your local config
$ git submodule sync --recursive
# update the submodule from the new URL
$ git submodule update --init --recursive

處理子模組

如果你使用子模組,很可能是因為你想在處理主專案程式碼的同時處理子模組程式碼(或多個子模組)。否則,你可能會使用更簡單的依賴管理系統(如 Maven 或 Rubygems)。

現在讓我們透過一個例子,同時修改子模組和主專案,並同時提交和釋出這些更改。

到目前為止,當我們執行 git submodule update 命令從子模組倉庫獲取更改時,Git 會獲取更改並更新子目錄中的檔案,但會使子倉庫處於所謂的“分離 HEAD”(detached HEAD)狀態。這意味著沒有本地工作分支(例如 master)來跟蹤更改。由於沒有工作分支來跟蹤更改,這意味著即使你對子模組進行了提交,這些更改很可能會在下次執行 git submodule update 時丟失。你需要執行一些額外的步驟才能使子模組中的更改被跟蹤。

為了使子模組更容易進行修改,你需要執行兩項操作。你需要進入每個子模組並檢出一個分支來工作。然後,你需要告訴 Git 如果你已進行更改並且之後 git submodule update --remote 從上游拉取了新工作,該怎麼辦。選項是你可以將它們合併到你的本地工作中,或者你可以嘗試將你的本地工作變基到新更改之上。

首先,我們進入子模組目錄並檢出一個分支。

$ cd DbConnector/
$ git checkout stable
Switched to branch 'stable'

讓我們嘗試使用“merge”選項更新我們的子模組。要手動指定它,我們只需將 --merge 選項新增到我們的 update 呼叫中。在這裡,我們將看到此子模組在伺服器上有一個更改,並且它已被合併。

$ cd ..
$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable     -> origin/stable
Updating c87d55d..92c7337
Fast-forward
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'

現在,如果我們回到主專案並執行 git diff --submodule,我們可以看到子模組已更新,並得到已新增到其中的提交列表。如果你不想每次執行 git diff 時都輸入 --submodule,你可以透過將 diff.submodule 配置值設定為“log”來將其設定為預設格式。

$ cd DbConnector/
$ vim src/db.c
$ git commit -am 'Unicode support'
[stable f906e16] Unicode support
 1 file changed, 1 insertion(+)

現在,如果我們回到主專案並執行 git diff --submodule,你就能看到子模組已更新,並獲得已新增到其中的提交列表。如果你不想每次執行 git diff 時都輸入 --submodule,你可以透過將 diff.submodule 配置值設定為“log”來將其設定為預設格式。

$ cd ..
$ git submodule update --remote --rebase
First, rewinding head to replay your work on top of it...
Applying: Unicode support
Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

如果你忘記了 --rebase--merge,Git 將只把子模組更新到伺服器上的內容,並將你的專案重置為分離 HEAD 狀態。

$ git submodule update --remote
Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

如果發生這種情況,請不要擔心,你可以簡單地回到目錄並再次檢出你的分支(它仍然包含你的工作),然後手動合併或變基 origin/stable(或任何你想要的上游分支)。

如果你還沒有提交子模組中的更改,並且運行了會導致問題的 submodule update,Git 會獲取更改但不會覆蓋子模組目錄中未儲存的工作。

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable     -> origin/stable
error: Your local changes to the following files would be overwritten by checkout:
	scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

如果你所做的更改與上游更改有衝突,當你執行更新時,Git 會告訴你。

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLICT (content): Merge conflict in scripts/setup.sh
Recorded preimage for 'scripts/setup.sh'
Automatic merge failed; fix conflicts and then commit the result.
Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

你可以進入子模組目錄並像平常一樣解決衝突。

釋出子模組更改

現在我們的子模組目錄中有了一些更改。其中一些是透過我們的更新從上游引入的,另一些是本地進行的,尚未提供給其他人,因為我們還沒有推送它們。

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > Update setup script
  > Unicode support
  > Remove unnecessary method
  > Add new option for conn pooling

如果我們提交主專案並推送它,而沒有同時推送子模組更改,那麼嘗試檢出我們更改的其他人將會遇到麻煩,因為他們將無法獲取所依賴的子模組更改。這些更改將僅存在於我們的本地副本中。

為了確保這種情況不會發生,你可以在推送主專案之前要求 Git 檢查所有子模組是否已正確推送。git push 命令接受 --recurse-submodules 引數,該引數可以設定為“check”或“on-demand”。“check”選項將使 push 在任何已提交的子模組更改未被推送時直接失敗。

$ git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
  DbConnector

Please try

	git push --recurse-submodules=on-demand

or cd to the path and use

	git push

to push them to a remote.

正如你所看到的,它還提供了一些有用的建議,關於我們接下來可能想做什麼。簡單的選擇是進入每個子模組並手動推送到遠端,以確保它們在外部可用,然後再次嘗試推送。如果你希望“check”行為對所有推送都發生,你可以透過執行 git config push.recurseSubmodules check 使此行為成為預設值。

另一個選項是使用“on-demand”值,它會嘗試為你執行此操作。

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/MainProject
   3d6d338..9a377d1  master -> master

如你所見,Git 進入 DbConnector 模組並推送它,然後再推送主專案。如果該子模組推送失敗,主專案推送也將失敗。你可以透過執行 git config push.recurseSubmodules on-demand 使此行為成為預設值。

合併子模組更改

如果你在更改子模組引用時與其他人同時更改,你可能會遇到一些問題。也就是說,如果子模組歷史已分叉並在超專案中提交到分叉的分支,那麼你可能需要花費一些精力來解決。

如果其中一個提交是另一個的直接祖先(快進合併),那麼 Git 將簡單地選擇後者進行合併,這樣就沒問題了。

然而,Git 不會嘗試進行任何簡單的合併。如果子模組提交分叉並且需要合併,你會看到類似這樣的內容。

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

基本上,這裡發生的是 Git 已經識別出兩個分支記錄的子模組歷史中的點是分叉的,並且需要合併。它解釋為“找不到後續合併提交”,這令人困惑,但我們稍後會解釋原因。

要解決這個問題,你需要弄清楚子模組應該處於什麼狀態。奇怪的是,Git 並沒有提供太多資訊來幫助你,甚至沒有兩個歷史分支的提交的 SHA-1。幸運的是,這很簡單。如果你執行 git diff,你可以獲得兩個分支中記錄的提交的 SHA-1。

$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

所以,在這種情況下,eb41d76 是我們子模組中 **我們** 擁有的提交,而 c771610 是上游擁有的提交。如果我們進入子模組目錄,它應該已經處於 eb41d76,因為合併不會觸及它。如果出於任何原因不是這樣,你可以簡單地建立一個指向它的分支並檢出它。

重要的是來自另一方的提交的 SHA-1。這將是你需要合併並解決的。你可以直接嘗試使用 SHA-1 進行合併,或者建立一個分支然後嘗試合併它。我們建議後者,即使只是為了建立更漂亮的合併提交訊息。

因此,我們將進入子模組目錄,建立一個名為“try-merge”的分支,該分支基於 git diff 中的第二個 SHA-1,然後手動合併。

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610

$ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

我們在這裡得到了一個實際的合併衝突,所以如果我們解決它並提交,那麼我們就可以簡單地用結果更新主專案。

$ vim src/main.c (1)
$ git add src/main.c
$ git commit -am 'merged our changes'
Recorded resolution for 'src/main.c'.
[master 9fd905e] merged our changes

$ cd .. (2)
$ git diff (3)
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit eb41d764bccf88be77aced643c13a7fa86714135
 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d
++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a
$ git add DbConnector (4)

$ git commit -m "Merge Tom's Changes" (5)
[master 10d2c60] Merge Tom's Changes
  1. 首先,我們解決衝突。

  2. 然後,我們回到主專案目錄。

  3. 我們可以再次檢查 SHA-1。

  4. 解決衝突的子模組條目。

  5. 提交我們的合併。

這可能有點令人困惑,但實際上並不難。

有趣的是,還有一種情況 Git 會處理。如果在子模組目錄中存在一個包含 **兩個** 提交的合併提交,Git 會將其作為可能的解決方案提供給你。它看到在子模組專案中的某個時候,有人合併了包含這兩個提交的分支,所以也許你會想要那個。

這就是為什麼之前的錯誤訊息是“找不到後續合併提交”,因為它無法做到 **這一點**。這令人困惑,因為誰會期望它 **嘗試** 這樣做呢?

如果它確實找到了一個可接受的合併提交,你將看到類似這樣的內容。

$ git merge origin/master
warning: Failed to merge submodule DbConnector (not fast-forward)
Found a possible merge resolution for the submodule:
 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes
If this is correct simply add it to the index for example
by using:

  git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"

which will accept this suggestion.
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Git 提供的建議命令將更新索引,就像你執行 git add(它會清除衝突)然後提交一樣。但你不應該這樣做。你可以輕鬆地進入子模組目錄,檢視差異,快進到該提交,對其進行適當測試,然後提交。

$ cd DbConnector/
$ git merge 9fd905e
Updating eb41d76..9fd905e
Fast-forward

$ cd ..
$ git add DbConnector
$ git commit -am 'Fast forward to a common submodule child'

這完成了同樣的事情,但至少這樣你可以驗證它是否有效,並且在你完成時你的子模組目錄中也有程式碼。

子模組技巧

你可以做一些事情讓使用子模組變得更容易。

子模組foreach

有一個 foreach 子模組命令可以在每個子模組中執行任意命令。如果你在一個專案中有很多子模組,這可能會非常有幫助。

例如,假設我們要開始一個新功能或修復一個 bug,並且我們在多個子模組中有工作。我們可以輕鬆地儲藏所有子模組中的所有工作。

$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

然後我們可以建立一個新分支並在所有子模組中切換到它。

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

你明白這個意思了。一個非常有用的事情是生成一個漂亮的統一 diff,顯示主專案和所有子專案中更改的內容。

$ git diff; git submodule foreach 'git diff'
Submodule DbConnector contains modified content
diff --git a/src/main.c b/src/main.c
index 210f1ae..1f0acdc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -245,6 +245,8 @@ static int handle_alias(int *argcp, const char ***argv)

      commit_pager_choice();

+     url = url_decode(url_orig);
+
      /* build alias_argv */
      alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1));
      alias_argv[0] = alias_string + 1;
Entering 'DbConnector'
diff --git a/src/db.c b/src/db.c
index 1aaefb6..5297645 100644
--- a/src/db.c
+++ b/src/db.c
@@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len)
        return url_decode_internal(&url, len, NULL, &out, 0);
 }

+char *url_decode(const char *url)
+{
+       return url_decode_mem(url, strlen(url));
+}
+
 char *url_decode_parameter_name(const char **query)
 {
        struct strbuf out = STRBUF_INIT;

在這裡,我們可以看到我們在子模組中定義了一個函式並在主專案中呼叫它。這顯然是一個簡化的例子,但希望它能讓你對它的用途有一個大概的瞭解。

有用的別名

你可能想為其中一些命令設定別名,因為它們可能相當長,而且你無法為大多數命令設定配置選項來使其成為預設值。我們已經在 Git 別名 中介紹了設定 Git 別名,但這裡有一個例子,如果你打算大量使用 Git 子模組,你可能會想設定這樣的別名。

$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

這樣,當你想要更新子模組時,只需執行 git supdate,或者當你想要使用子模組依賴性檢查進行推送時,只需執行 git spush

子模組問題

然而,使用子模組並非沒有障礙。

切換分支

例如,在 Git 版本早於 Git 2.13 的情況下,帶有子模組的分支切換也可能很棘手。如果你建立一個新分支,在那裡新增一個子模組,然後切換回一個沒有該子模組的分支,你仍然會有一個未跟蹤的目錄,即子模組目錄。

$ git --version
git version 2.12.2

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

刪除目錄並不難,但把它放在那裡可能會有點令人困惑。如果你刪除它,然後切換回擁有該子模組的分支,你需要執行 submodule update --init 來重新填充它。

$ git clean -ffdx
Removing CryptoLibrary/

$ git checkout add-crypto
Switched to branch 'add-crypto'

$ ls CryptoLibrary/

$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'

$ ls CryptoLibrary/
Makefile	includes	scripts		src

同樣,這並不難,但可能會有點令人困惑。

較新的 Git 版本(Git >= 2.13)透過將 --recurse-submodules 標誌新增到 git checkout 命令來簡化這一切,該命令可以根據我們正在切換到的分支將子模組置於正確狀態。

$ git --version
git version 2.13.3

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout --recurse-submodules master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working tree clean

使用 git checkout--recurse-submodules 標誌在你處理超專案的多個分支時也可能很有用,每個分支都將你的子模組指向不同的提交。實際上,如果你在記錄子模組不同提交的分支之間切換,執行 git status 後,子模組將顯示為“已修改”,並指示“新提交”。這是因為預設情況下,在切換分支時子模組狀態不會被保留。

這可能非常令人困惑,因此最好始終在你的專案中有子模組時 git checkout --recurse-submodules。對於沒有 --recurse-submodules 標誌的較舊 Git 版本,檢出後你可以使用 git submodule update --init --recursive 將子模組置於正確狀態。

幸運的是,你可以告訴 Git(>=2.14)始終使用 --recurse-submodules 標誌,透過設定配置選項 submodule.recursegit config submodule.recurse true。如上所述,這也將使 Git 為所有支援 --recurse-submodules 的命令(git clone 除外)遞迴到子模組中。

從子目錄切換到子模組

許多人遇到的另一個主要限制是關於從子目錄切換到子模組的問題。如果你一直在跟蹤專案中的檔案,並且想將它們移到子模組中,你必須小心,否則 Git 會對你生氣。假設你的專案有一個子目錄中的檔案,並且你想將其切換為子模組。如果你刪除了子目錄然後執行 submodule add,Git 會報錯。

$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index

你必須先暫存 CryptoLibrary 目錄。然後你就可以新增子模組了。

$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

現在假設你在一個分支中做了這個。如果你嘗試切換回一個其中檔案仍然在實際樹中而不是子模組中的分支——你會收到這個錯誤。

$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
  CryptoLibrary/Makefile
  CryptoLibrary/includes/crypto.h
  ...
Please move or remove them before you can switch branches.
Aborting

你可以強制使用 checkout -f 進行切換,但要小心,不要在此期間儲存未儲存的更改,因為它們可能會被該命令覆蓋。

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'

然後,當你切換回來時,你會出於某種原因得到一個空的 CryptoLibrary 目錄,並且 git submodule update 可能也無法修復它。你可能需要進入子模組目錄並執行 git checkout . 來找回所有檔案。你可以用 submodule foreach 指令碼來為多個子模組執行它。

需要注意的是,如今子模組將所有 Git 資料儲存在頂級專案的 .git 目錄中,因此與舊版本的 Git 不同,銷燬子模組目錄不會丟失你擁有的任何提交或分支。

透過這些工具,子模組可以成為一種相當簡單有效的方法,可以同時開發多個相關但仍然獨立的專案。