章節 ▾ 第二版

7.11 Git 工具 - 子模組

子模組

在進行一個專案的同時,往往需要使用另一個專案。這可能是一個第三方開發的庫,或者是一個你獨立開發並在多個父專案中使用的庫。在這種情況下,一個常見的問題是:你希望能夠將這兩個專案視為獨立的,但又能在其中一個專案中使用另一個。

舉個例子。假設你正在開發一個網站並建立 Atom feed。你決定使用一個庫來代替編寫自己的 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 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

請注意,為了安全起見,你應該執行 git submodule update 時帶上 --init 標誌,以防你剛拉取的主專案提交添加了新的子模組;如果任何子模組有巢狀子模組,則帶上 --recursive 標誌。

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

在拉取超級專案更新時,可能會發生一種特殊情況:上游倉庫可能在你拉取的某個提交中更改了 .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”選項更新子模組。要手動指定,我們只需在 update 呼叫中新增 --merge 選項。這裡我們將看到此子模組在伺服器上有一個更改,並且它被合併進來。

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

這達到了相同的目的,但至少透過這種方式,你可以驗證它是否有效,並且在完成後你的子模組目錄中包含程式碼。

子模組技巧

你可以做一些事情,讓子模組的使用變得更輕鬆。

子模組 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'

你明白了。一個非常有用的功能是,你可以生成一個統一的差異,顯示你的主專案和所有子專案中的更改。

$ 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 --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)透過向 git checkout 命令新增 --recurse-submodules 標誌來簡化這一切,該標誌負責將子模組置於我們要切換到的分支的正確狀態。

$ 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 後子模組將顯示為“modified”,並指示“new commits”。這是因為子模組狀態在切換分支時預設不會被帶過去。

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

幸運的是,你可以透過設定配置選項 submodule.recurse 來告訴 Git(>=2.14)始終使用 --recurse-submodules 標誌:git 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 不同,銷燬子模組目錄不會丟失你擁有的任何提交或分支。

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

scroll-to-top