Git 子模塊

2018-09-27 15:50 更新

經(jīng)常有這樣的事情,當(dāng)你在一個(gè)項(xiàng)目上工作時(shí),你需要在其中使用另外一個(gè)項(xiàng)目。也許它是一個(gè)第三方開發(fā)的庫(kù)或者是你獨(dú)立開發(fā)和并在多個(gè)父項(xiàng)目中使用的。這個(gè)場(chǎng)景下一個(gè)常見的問(wèn)題產(chǎn)生了:你想將兩個(gè)項(xiàng)目單獨(dú)處理但是又需要在其中一個(gè)中使用另外一個(gè)。

這里有一個(gè)例子。假設(shè)你在開發(fā)一個(gè)網(wǎng)站,為之創(chuàng)建Atom源。你不想編寫一個(gè)自己的Atom生成代碼,而是決定使用一個(gè)庫(kù)。你可能不得不像CPAN install或者Ruby gem一樣包含來(lái)自共享庫(kù)的代碼,或者將代碼拷貝到你的項(xiàng)目樹中。如果采用包含庫(kù)的辦法,那么不管用什么辦法都很難去定制這個(gè)庫(kù),部署它就更加困難了,因?yàn)槟惚仨毚_保每個(gè)客戶都擁有那個(gè)庫(kù)。把代碼包含到你自己的項(xiàng)目中帶來(lái)的問(wèn)題是,當(dāng)上游被修改時(shí),任何你進(jìn)行的定制化的修改都很難歸并。

Git 通過(guò)子模塊處理這個(gè)問(wèn)題。子模塊允許你將一個(gè) Git 倉(cāng)庫(kù)當(dāng)作另外一個(gè)Git倉(cāng)庫(kù)的子目錄。這允許你克隆另外一個(gè)倉(cāng)庫(kù)到你的項(xiàng)目中并且保持你的提交相對(duì)獨(dú)立。

子模塊初步

假設(shè)你想把 Rack 庫(kù)(一個(gè) Ruby 的 web 服務(wù)器網(wǎng)關(guān)接口)加入到你的項(xiàng)目中,可能既要保持你自己的變更,又要延續(xù)上游的變更。首先你要把外部的倉(cāng)庫(kù)克隆到你的子目錄中。你通過(guò)git submodule add將外部項(xiàng)目加為子模塊:

$ git submodule add git://github.com/chneukirchen/rack.git rack
Initialized empty Git repository in /opt/subtest/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 422 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.

現(xiàn)在你就在項(xiàng)目里的rack子目錄下有了一個(gè) Rack 項(xiàng)目。你可以進(jìn)入那個(gè)子目錄,進(jìn)行變更,加入你自己的遠(yuǎn)程可寫倉(cāng)庫(kù)來(lái)推送你的變更,從原始倉(cāng)庫(kù)拉取和歸并等等。如果你在加入子模塊后立刻運(yùn)行g(shù)it status,你會(huì)看到下面兩項(xiàng):

$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      new file:   .gitmodules
#      new file:   rack
#

首先你注意到有一個(gè).gitmodules文件。這是一個(gè)配置文件,保存了項(xiàng)目 URL 和你拉取到的本地子目錄

$ cat .gitmodules
[submodule "rack"]
      path = rack
      url = git://github.com/chneukirchen/rack.git

如果你有多個(gè)子模塊,這個(gè)文件里會(huì)有多個(gè)條目。很重要的一點(diǎn)是這個(gè)文件跟其他文件一樣也是處于版本控制之下的,就像你的.gitignore文件一樣。它跟項(xiàng)目里的其他文件一樣可以被推送和拉取。這是其他克隆此項(xiàng)目的人獲知子模塊項(xiàng)目來(lái)源的途徑。

git status的輸出里所列的另一項(xiàng)目是 rack 。如果你運(yùn)行在那上面運(yùn)行git diff,會(huì)發(fā)現(xiàn)一些有趣的東西:

$ git diff --cached rack
diff --git a/rack b/rack
new file mode 160000
index 0000000..08d709f
--- /dev/null
+++ b/rack
@@ -0,0 +1 @@
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433

盡管rack是你工作目錄里的子目錄,但 Git 把它視作一個(gè)子模塊,當(dāng)你不在那個(gè)目錄里時(shí)并不記錄它的內(nèi)容。取而代之的是,Git 將它記錄成來(lái)自那個(gè)倉(cāng)庫(kù)的一個(gè)特殊的提交。當(dāng)你在那個(gè)子目錄里修改并提交時(shí),子項(xiàng)目會(huì)通知那里的 HEAD 已經(jīng)發(fā)生變更并記錄你當(dāng)前正在工作的那個(gè)提交;通過(guò)那樣的方法,當(dāng)其他人克隆此項(xiàng)目,他們可以重新創(chuàng)建一致的環(huán)境。

這是關(guān)于子模塊的重要一點(diǎn):你記錄他們當(dāng)前確切所處的提交。你不能記錄一個(gè)子模塊的master或者其他的符號(hào)引用。

當(dāng)你提交時(shí),會(huì)看到類似下面的:

$ git commit -m 'first commit with submodule rack'
[master 0550271] first commit with submodule rack
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 rack

注意 rack 條目的 160000 模式。這在Git中是一個(gè)特殊模式,基本意思是你將一個(gè)提交記錄為一個(gè)目錄項(xiàng)而不是子目錄或者文件。

你可以將rack目錄當(dāng)作一個(gè)獨(dú)立的項(xiàng)目,保持一個(gè)指向子目錄的最新提交的指針然后反復(fù)地更新上層項(xiàng)目。所有的Git命令都在兩個(gè)子目錄里獨(dú)立工作:

$ git log -1
commit 0550271328a0038865aad6331e620cd7238601bb
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Apr 9 09:03:56 2009 -0700

    first commit with submodule rack
$ cd rack/
$ git log -1
commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433
Author: Christian Neukirchen <chneukirchen@gmail.com>
Date:   Wed Mar 25 14:49:04 2009 +0100

    Document version change

克隆一個(gè)帶子模塊的項(xiàng)目

這里你將克隆一個(gè)帶子模塊的項(xiàng)目。當(dāng)你接收到這樣一個(gè)項(xiàng)目,你將得到了包含子項(xiàng)目的目錄,但里面沒有文件:

$ git clone git://github.com/schacon/myproject.git
Initialized empty Git repository in /opt/myproject/.git/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
$ cd myproject
$ ls -l
total 8
-rw-r--r--  1 schacon  admin   3 Apr  9 09:11 README
drwxr-xr-x  2 schacon  admin  68 Apr  9 09:11 rack
$ ls rack/
$

rack目錄存在了,但是是空的。你必須運(yùn)行兩個(gè)命令:git submodule init來(lái)初始化你的本地配置文件,git submodule update來(lái)從那個(gè)項(xiàng)目拉取所有數(shù)據(jù)并檢出你上層項(xiàng)目里所列的合適的提交:

$ git submodule init
Submodule 'rack' (git://github.com/chneukirchen/rack.git) registered for path 'rack'
$ git submodule update
Initialized empty Git repository in /opt/myproject/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 173 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.
Submodule path 'rack': checked out '08d709f78b8c5b0fbeb7821e37fa53e69afcf433'

現(xiàn)在你的rack子目錄就處于你先前提交的確切狀態(tài)了。如果另外一個(gè)開發(fā)者變更了 rack 的代碼并提交,你拉取那個(gè)引用然后歸并之,將得到稍有點(diǎn)怪異的東西:

$ git merge origin/master
Updating 0550271..85a3eee
Fast forward
 rack |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
[master*]$ git status
# On branch 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:   rack
#

你歸并來(lái)的僅僅上是一個(gè)指向你的子模塊的指針;但是它并不更新你子模塊目錄里的代碼,所以看起來(lái)你的工作目錄處于一個(gè)臨時(shí)狀態(tài):

$ git diff
diff --git a/rack b/rack
index 6c5e70b..08d709f 160000
--- a/rack
+++ b/rack
@@ -1 +1 @@
-Subproject commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433

事情就是這樣,因?yàn)槟闼鶕碛械闹赶蜃幽K的指針和子模塊目錄的真實(shí)狀態(tài)并不匹配。為了修復(fù)這一點(diǎn),你必須再次運(yùn)行g(shù)it submodule update:

$ git submodule update
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 2 (delta 0)
Unpacking objects: 100% (3/3), done.
From git@github.com:schacon/rack
   08d709f..6c5e70b  master     -> origin/master
Submodule path 'rack': checked out '6c5e70b984a60b3cecd395edd5b48a7575bf58e0'

每次你從主項(xiàng)目中拉取一個(gè)子模塊的變更都必須這樣做??雌饋?lái)很怪但是管用。

一個(gè)常見問(wèn)題是當(dāng)開發(fā)者對(duì)子模塊做了一個(gè)本地的變更但是并沒有推送到公共服務(wù)器。然后他們提交了一個(gè)指向那個(gè)非公開狀態(tài)的指針然后推送上層項(xiàng)目。當(dāng)其他開發(fā)者試圖運(yùn)行g(shù)it submodule update,那個(gè)子模塊系統(tǒng)會(huì)找不到所引用的提交,因?yàn)樗淮嬖谟诘谝粋€(gè)開發(fā)者的系統(tǒng)中。如果發(fā)生那種情況,你會(huì)看到類似這樣的錯(cuò)誤:

$ git submodule update
fatal: reference isn’t a tree: 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Unable to checkout '6c5e70b984a60b3cecd395edd5ba7575bf58e0' in submodule path 'rack'

你不得不去查看誰(shuí)最后變更了子模塊

$ git log -1 rack
commit 85a3eee996800fcfa91e2119372dd4172bf76678
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Apr 9 09:19:14 2009 -0700

    added a submodule reference I will never make public. hahahahaha!

然后,你給那個(gè)家伙發(fā)電子郵件說(shuō)他一通。

上層項(xiàng)目

有時(shí)候,開發(fā)者想按照他們的分組獲取一個(gè)大項(xiàng)目的子目錄的子集。如果你是從 CVS 或者 Subversion 遷移過(guò)來(lái)的話這個(gè)很常見,在那些系統(tǒng)中你已經(jīng)定義了一個(gè)模塊或者子目錄的集合,而你想延續(xù)這種類型的工作流程。

在 Git 中實(shí)現(xiàn)這個(gè)的一個(gè)好辦法是你將每一個(gè)子目錄都做成獨(dú)立的 Git 倉(cāng)庫(kù),然后創(chuàng)建一個(gè)上層項(xiàng)目的 Git 倉(cāng)庫(kù)包含多個(gè)子模塊。這個(gè)辦法的一個(gè)優(yōu)勢(shì)是你可以在上層項(xiàng)目中通過(guò)標(biāo)簽和分支更為明確地定義項(xiàng)目之間的關(guān)系。

子模塊的問(wèn)題

使用子模塊并非沒有任何缺點(diǎn)。首先,你在子模塊目錄中工作時(shí)必須相對(duì)小心。當(dāng)你運(yùn)行g(shù)it submodule update,它會(huì)檢出項(xiàng)目的指定版本,但是不在分支內(nèi)。這叫做獲得一個(gè)分離的頭——這意味著 HEAD 文件直接指向一次提交,而不是一個(gè)符號(hào)引用。問(wèn)題在于你通常并不想在一個(gè)分離的頭的環(huán)境下工作,因?yàn)樘菀讈G失變更了。如果你先執(zhí)行了一次submodule update,然后在那個(gè)子模塊目錄里不創(chuàng)建分支就進(jìn)行提交,然后再次從上層項(xiàng)目里運(yùn)行g(shù)it submodule update同時(shí)不進(jìn)行提交,Git會(huì)毫無(wú)提示地覆蓋你的變更。技術(shù)上講你不會(huì)丟失工作,但是你將失去指向它的分支,因此會(huì)很難取到。

為了避免這個(gè)問(wèn)題,當(dāng)你在子模塊目錄里工作時(shí)應(yīng)使用git checkout -b work創(chuàng)建一個(gè)分支。當(dāng)你再次在子模塊里更新的時(shí)候,它仍然會(huì)覆蓋你的工作,但是至少你擁有一個(gè)可以回溯的指針。

切換帶有子模塊的分支同樣也很有技巧。如果你創(chuàng)建一個(gè)新的分支,增加了一個(gè)子模塊,然后切換回不帶該子模塊的分支,你仍然會(huì)擁有一個(gè)未被追蹤的子模塊的目錄

$ git checkout -b rack
Switched to a new branch "rack"
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/myproj/rack/.git/
...
Receiving objects: 100% (3184/3184), 677.42 KiB | 34 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
$ git commit -am 'added rack submodule'
[rack cc49a69] added rack submodule
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 rack
$ git checkout master
Switched to branch "master"
$ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#      rack/

你將不得不將它移走或者刪除,這樣的話當(dāng)你切換回去的時(shí)候必須重新克隆它——你可能會(huì)丟失你未推送的本地的變更或分支。

最后一個(gè)需要引起注意的是關(guān)于從子目錄切換到子模塊的。如果你已經(jīng)跟蹤了你項(xiàng)目中的一些文件但是想把它們移到子模塊去,你必須非常小心,否則Git會(huì)生你的氣。假設(shè)你的項(xiàng)目中有一個(gè)子目錄里放了 rack 的文件,然后你想將它轉(zhuǎn)換為子模塊。如果你刪除子目錄然后運(yùn)行submodule add,Git會(huì)向你大吼:

$ rm -Rf rack/
$ git submodule add git@github.com:schacon/rack.git rack
'rack' already exists in the index

你必須先將rack目錄撤回。然后你才能加入子模塊:

$ git rm -r rack
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/testsub/rack/.git/
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 88 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.

現(xiàn)在假設(shè)你在一個(gè)分支里那樣做了。如果你嘗試切換回一個(gè)仍然在目錄里保留那些文件而不是子模塊的分支時(shí)——你會(huì)得到下面的錯(cuò)誤:

$ git checkout master
error: Untracked working tree file 'rack/AUTHORS' would be overwritten by merge.

你必須先移除rack子模塊的目錄才能切換到不包含它的分支:

$ mv rack /tmp/
$ git checkout master
Switched to branch "master"
$ ls
README  rack

然后,當(dāng)你切換回來(lái),你會(huì)得到一個(gè)空的rack目錄。你可以運(yùn)行g(shù)it submodule update重新克隆,也可以將/tmp/rack目錄重新移回空目錄。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)