1. 程式人生 > >Git submodule - 子模組【轉】

Git submodule - 子模組【轉】

子模組

有種情況我們經常會遇到:某個工作中的專案需要包含並使用另一個專案。 也許是第三方庫,或者你獨立開發的,用於多個父專案的庫。 現在問題來了:你想要把它們當做兩個獨立的專案,同時又想在一個專案中使用另一個。

我們舉一個例子。 假設你正在開發一個網站然後建立了 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 與已經拉取的本地目錄之間的對映:

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

如果有多個子模組,該檔案中就會有多條記錄。 要重點注意的是,該檔案也像 .gitignore 檔案一樣受到(通過)版本控制。 它會和該專案的其他部分一同被拉取推送。 這就是克隆該專案的人知道去哪獲得子模組的原因。

Note

由於 .gitmodules 檔案中的 URL 是人們首先嚐試克隆/拉取的地方,因此請儘可能確保你使用的URL 大家都能訪問。 例如,若你要使用的推送 URL 與他人的拉取 URL 不同,那麼請使用他人能訪問到的 URL。 你也可以根據自己的需要,通過在本地執行 git config submodule.DbConnector.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 並不會跟蹤它的內容, 而是將它看作該倉庫中的一個特殊提交。

如果你想看到更漂亮的差異輸出,可以給 git diff 傳遞 --submodule 選項。

$ 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 'added DbConnector module'
[master fb9093c] added DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

注意 DbConnector 記錄的 160000 模式。 這是 Git 中的一種特殊模式,它本質上意味著你是將一次提交記作一項目錄記錄的,而非將它記錄成一個子目錄或者一個檔案。

克隆含有子模組的專案

接下來我們將會克隆一個含有子模組的專案。 當你在克隆這樣的專案時,預設會包含該子模組目錄,但其中還沒有任何檔案:

$ 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 子目錄是處在和之前提交時相同的狀態了。

不過還有更簡單一點的方式。 如果給 git clone 命令傳遞 --recursive 選項,它就會自動初始化並更新倉庫中的每一個子模組。

$ git clone --recursive 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'

在包含子模組的專案上工作

現在我們有一份包含子模組的專案副本,我們將會同時在主專案和子模組專案上與隊員協作。

拉取上游修改

在專案中使用子模組的最簡模型,就是隻使用子專案並不時地獲取更新,而並不在你的檢出中進行任何更改。 我們來看一個簡單的例子。

如果想要在子模組中檢視新工作,可以進入到目錄中執行 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 時都輸入 --submodle,那麼可以將 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'

此命令預設會假定你想要更新並檢出子模組倉庫的 master 分支。 不過你也可以設定為想要的其他分支。 例如,你想要 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 <[email protected]>
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 預設會嘗試更新所有子模組,所以如果有很多子模組的話,你可以傳遞想要更新的子模組的名字。

在子模組上工作

你很有可能正在使用子模組,因為你確實想在子模組中編寫程式碼的同時,還想在主專案上編寫程式碼(或者跨子模組工作)。 否則你大概只能用簡單的依賴管理系統(如 Maven 或 Rubygems)來替代了。

現在我們將通過一個例子來演示如何在子模組與主專案中同時做修改,以及如何同時提交與釋出那些修改。

到目前為止,當我們執行 git submodule update 從子模組倉庫中抓取修改時,Git 將會獲得這些改動並更新子目錄中的檔案,但是會將子倉庫留在一個稱作 “遊離的 HEAD” 的狀態。 這意味著沒有本地工作分支(例如 “master”)跟蹤改動。 所以你做的任何改動都不會被跟蹤。

為了將子模組設定得更容易進入並修改,你需要做兩件事。 首先,進入每個子模組並檢出其相應的工作分支。 接著,若你做了更改就需要告訴 Git 它該做什麼,然後執行 git submodule update --remote 來從上游拉取新工作。 你可以選擇將它們合併到你的本地工作中,也可以嘗試將你的工作變基到新的更改上。

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

$ git checkout stable
Switched to branch 'stable'

然後嘗試用 “merge” 選項。 為了手動指定它,我們只需給 update 新增 --merge 選項即可。 這時我們將會看到伺服器上的這個子模組有一個改動並且它被合併了進來。

$ 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(+)

如果我們現在更新子模組,就會看到當我們在本地做了更改時上游也有一個改動,我們需要將它併入本地。

$ 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(或任何一個你想要的遠端分支)就行了。

如果你沒有提交子模組的改動,那麼執行一個子模組更新也不會出現問題,此時 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
  > updated setup script
  > unicode support
  > remove unnecessary method
  > add new option for conn pooling

如果我們在主專案中提交併推送但並不推送子模組上的改動,其他嘗試檢出我們修改的人會遇到麻煩,因為他們無法得到依賴的子模組改動。 那些改動只存在於我們本地的拷貝中。

為了確保這不會發生,你可以讓 Git 在推送到主專案前檢查所有子模組是否已推送。 git push 命令接受可以設定為 “check” 或 “on-demand” 的 --recurse-submodules 引數。 如果任何提交的子模組改動沒有推送那麼 “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.

如你所見,它也給我們了一些有用的建議,指導接下來該如何做。 最簡單的選項是進入每一個子模組中然後手動推送到遠端倉庫,確保它們能被外部訪問到,之後再次嘗試這次推送。

另一個選項是使用 “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 會簡單地選擇之後的提交來合併,這樣沒什麼問題。

不過,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 在這裡指出了子模組歷史中的兩個分支記錄點已經分叉並且需要合併。 它將其解釋為 “merge following commits not found”(未找到接下來需要合併的提交),雖然這有點令人困惑,不過之後我們會解釋為什麼是這樣。

為了解決這個問題,你需要弄清楚子模組應該處於哪種狀態。 奇怪的是,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 建立一個分支然後手動合併。

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610
(DbConnector) $ 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 會建議你將它作為一個可行的解決方案。 它看到有人在子模組專案的某一點上合併了包含這兩次提交的分支,所以你可能想要那個。

這就是為什麼前面的錯誤資訊是 “merge following commits not found”,因為它不能 這樣 做。 它讓人困惑是因為誰能想到它會嘗試這樣做?

如果它找到了一個可以接受的合併提交,你會看到類似下面的資訊:

$ 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 add 那樣,這樣會清除衝突然後提交。不過你可能不應該這樣做。你可以輕鬆地進入子模組目錄,檢視差異是什麼,快進到這次提交,恰當地測試,然後提交它。

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

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

這些命令完成了同一件事,但是通過這種方式你至少可以驗證工作是否有效,以及當你在完成時可以確保子模組目錄中有你的程式碼。

子模組技巧

你可以做幾件事情來讓用子模組工作輕鬆一點兒。

子模組遍歷

有一個 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 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 'adding crypto library'
[add-crypto 4445836] adding 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 -fdx
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 會對你發脾氣。 假設專案內有一些檔案在子目錄中,你想要將其轉換為一個子模組。 如果刪除子目錄然後執行 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,摧毀一個子模組目錄並不會丟失任何提交或分支。

擁有了這些工具,使用子模組會成為可以在幾個相關但卻分離的專案上同時開發的相當簡單有效的方法。

prev | next