Git筆記(35) 子模塊


1. 多個父項目

有種情況經常會遇到:
某個工作中的項目需要包含並使用另一個項目

現在問題來了:
想要把它們當做兩個獨立的項目,同時又想在一個項目中使用另一個

假設正在開發一個網站然後創建了 Atom 訂閱

決定使用一個庫,而不是寫自己的 Atom 生成代碼
可能不得不通過 CPAN 安裝或 Ruby gem 來包含共享庫中的代碼
或者將源代碼直接拷貝到自己的項目中

如果將這個庫包含進來
那麼無論用何種方式都很難定製它,部署則更加困難
因爲必須確保每一個客戶端都包含該庫

如果將代碼複製到自己的項目中
那麼你做的任何自定義修改都會使合併上游的改動變得困難

Git 通過子模塊來解決這個問題
子模塊允許將一個 Git 倉庫作爲另一個 Git 倉庫的子目錄
它能讓你將另一個倉庫克隆到自己的項目中,同時還保持提交的獨立


2. 開始使用子模塊

將要演示如何在一個被分成一個主項目與幾個子項目的項目上開發

首先將一個已存在的 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.

默認情況下,子模塊會將子項目放到一個與倉庫同名的目錄中
如果想要放到其他地方,那麼可以在命令結尾添加一個不同的路徑

如果這時運行 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 文件一樣受到(通過)版本控制
它會和該項目的其他部分一同被拉取推送
這就是克隆該項目的人知道去哪獲得子模塊的原因

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 push origin master

3. 克隆含有子模塊的項目

接下來將會克隆一個含有子模塊的項目
當你在克隆這樣的項目時,默認會包含該子模塊目錄
但其中還沒有任何文件:

$ 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 命令傳遞 --recurse-submodules 選項
它就會自動初始化並更新倉庫中的每一個子模塊, 包括可能存在的嵌套子模塊

$ 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 update --init --recursive


4. 在包含子模塊的項目上工作

現在有一份包含子模塊的項目副本,將會同時在主項目和子模塊項目上與隊員協作


4.1. 從子模塊的遠端拉取上游修改

在項目中使用子模塊的最簡模型,就是隻使用子項目並不時地獲取更新
而並不在你的檢出中進行任何更改

如果想要在子模塊中查看新工作
可以進入到目錄中運行 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 默認會嘗試更新所有子模塊
所以如果有很多子模塊的話,可以傳遞想要更新的子模塊的名字


4.2. 從項目遠端拉取上游更改

現在,站在協作者的視角,有自己的 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

請注意,爲安全起見
如果 MainProject 提交了你剛拉取的新子模塊
那麼應該在 git submodule update 後面添加 --init 選項
如果子模塊有嵌套的子模塊,則應使用 --recursive 選項

如果想自動化此過程,那麼可以爲 git pull 命令添加 --recurse-submodules 選項(
這會讓 Git 在拉取後運行 git submodule update,將子模塊置爲正確的狀態

此外,如果想讓 Git 總是以 --recurse-submodules 拉取
可以將配置選項 submodule.recurse 設置爲 true
此選項會讓 Git 爲所有支持 --recurse-submodules 的命令使用該選項(除 clone 以外)

在爲父級項目拉取更新時,還會出現一種特殊的情況:
在拉取的提交中, 可能 .gitmodules 文件中記錄的子模塊的 URL 發生了改變
比如,若子模塊項目改變了它的託管平臺,就會發生這種情況
此時,若父級項目引用的子模塊提交不在倉庫中本地配置的子模塊遠端上
那麼執行 git pull --recurse-submodulesgit submodule update 就會失敗
爲了補救,git submodule sync 命令需要:

# 將新的 URL 複製到本地配置中
$ git submodule sync --recursive
# 從新 URL 更新子模塊
$ git submodule update --init --recursive

5. 在子模塊上工作

很有可能正在使用子模塊
因爲你確實想在子模塊中編寫代碼的同時,還想在主項目上編寫代碼(或者跨子模塊工作)
否則大概只能用簡單的依賴管理系統(如 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(或任何一個你想要的遠程分支)就行了

如果你沒有提交子模塊的改動,那麼運行一個子模塊更新也不會出現問題,此時 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'

你可以進入子模塊目錄中然後就像平時那樣修復衝突


6. 發佈子模塊改動

現在我們的子模塊目錄中有一些改動
其中有一些是我們通過更新從上游引入的
而另一些是本地生成的,由於我們還沒有推送它們
所以對任何其他人都不可用

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

如你所見,它也給我們了一些有用的建議,指導接下來該如何做

最簡單的選項是進入每一個子模塊中然後手動推送到遠程倉庫
確保它們能被外部訪問到,之後再次嘗試這次推送

如果你想要對所有推送都執行檢查
那麼可以通過設置 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 讓它成爲默認行爲


7. 合併子模塊改動

如果你其他人同時改動了一個子模塊引用,那麼可能會遇到一些問題
也就是說,如果子模塊的歷史已經分叉並且在父項目中分別提交到了分叉的分支上
那麼你需要做一些工作來修復它

如果一個提交是另一個的直接祖先(一個快進式合併)
那麼 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-1 創建一個分支然後手動合併

$ 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 建議的命令是更新索引,就像你運行了 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'

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


8. 子模的塊技巧

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


8.1. 子模塊遍歷

有一個 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;

在這裏,我們看到子模塊中定義了一個函數並在主項目中調用了它
這明顯是個簡化了的例子,但是希望它能讓你明白這種方法的用處


8.2. 有用的別名

可能想爲其中一些命令設置別名
因爲它們可能會非常長而你又不能設置選項作爲它們的默認選項
我們在 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 檢查子模塊依賴後推送


9. 子模塊的問題

然而使用子模塊還是有一些小問題


9.1. 切換分支

例如,使用 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 '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(>= 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 'adding crypto library'
[add-crypto 4445836] adding 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 config submodule.recurse true 設置 submodule.recurse 選項
告訴 Git(>=2.14)總是使用 --recurse-submodules

如上所述
這也會讓 Git 爲每個擁有 --recurse-submodules 選項的命令(除了 git clone) 總是遞歸地在子模塊中執行


9.2. 從子目錄切換到子模塊

另一個主要的告誡是許多人遇到了將子目錄轉換爲子模塊的問題
如果你在項目中已經跟蹤了一些文件,然後想要將它們移動到一個子模塊中,那麼請務必小心
假設項目內有一些文件在子目錄中,你想要將其轉換爲一個子模塊
如果刪除子目錄然後運行 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,摧毀一個子模塊目錄並不會丟失任何提交或分支

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


參考: git
以上內容,均根據git官網介紹刪減、添加和修改組成


相關推薦:

Git筆記(34) 調試
Git筆記(33) Rerere
Git筆記(32) 高級合併
Git筆記(31) 重置揭密
Git筆記(30) 重寫歷史


謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章