Git
章節 ▾ 第二版

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,請使用其他人可以存取的 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 fetchgit 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 旗標,如果任何子模組有巢狀子模組。

如果您想自動執行此程序,您可以將 --recurse-submodules 旗標新增至 git pull 命令 (自 Git 2.14 起)。這將使 Git 在提取後立即執行 git submodule update,從而將子模組置於正確的狀態。此外,如果您想讓 Git 始終使用 --recurse-submodules 進行提取,您可以將配置選項 submodule.recurse 設定為 true (這適用於自 Git 2.15 起的 git pull)。此選項將使 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」狀態。這表示沒有本機工作分支 (例如 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'

如果我們進入 DbConnector 目錄,我們會發現新的變更已合併到我們的本機 stable 分支中。現在,讓我們看看當我們對程式庫進行自己的本機變更,而其他人同時將其他變更推送到上游時會發生什麼。

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

現在,如果我們更新我們的子模組,我們可以看到當我們進行本機變更,而上游也有需要合併的變更時會發生什麼。

$ 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 嘗試合併,也可以為它建立一個分支,然後嘗試將其合併。我們建議後者,即使只是為了建立一個更簡潔的合併提交訊息。

因此,我們將進入我們的子模組目錄,根據 git diff 中的第二個 SHA-1 建立一個名為「try-merge」的分支,並手動合併。

$ 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'

這可以完成相同的操作,但至少這樣您可以在完成後驗證它是否有效,並且您的子模組目錄中有程式碼。

子模組技巧

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

Submodule Foreach

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

例如,假設我們想開始一個新功能或進行錯誤修復,並且我們在多個子模組中進行工作。我們可以輕鬆地將所有工作儲存在我們所有的子模組中。

$ 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'

您明白了。您可以做的一件非常有用的事情是產生一個漂亮的統一差異,顯示您的主專案和所有子專案中發生的變更。

$ 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 2.13 之前的 Git 版本中,切換包含子模組的分支也可能很棘手。如果您建立一個新分支,在那裡新增一個子模組,然後切換回沒有該子模組的分支,您仍然會將子模組目錄作為未追蹤的目錄

$ 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 將子模組置於正確的狀態。

幸運的是,您可以透過設定組態選項 submodule.recursegit config submodule.recurse true,告訴 Git (>=2.14) 始終使用 --recurse-submodules 標記。如上所述,這也會使 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 不同,銷毀子模組目錄不會遺失您擁有的任何提交或分支。

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

scroll-to-top