Git 工具 - 子模塊


子模塊

常常有這樣的事情,當你在一個項目上工做時,你須要在其中使用另一個項目。也許它是一個第三方開發的庫或者是你獨立開發和並在多個父項目中使用的。這個場景下一個常見的問題產生了:你想將兩個項目單獨處理可是又須要在其中一箇中使用另一個。git

這裏有一個例子。假設你在開發一個網站,爲之建立Atom源。你不想編寫一個本身的Atom生成代碼,而是決定使用一個庫。你可能不得不像CPAN install或者Ruby gem同樣包含來自共享庫的代碼,或者將代碼拷貝到你的項目樹中。若是採用包含庫的辦法,那麼無論用什麼辦法都很難去定製這個庫,部署它就更加困難了,因 爲你必須確保每一個客戶都擁有那個庫。把代碼包含到你本身的項目中帶來的問題是,當上遊被修改時,任何你進行的定製化的修改都很難歸併。github

Git 經過子模塊處理這個問題。子模塊容許你將一個 Git 倉庫看成另一個Git倉庫的子目錄。這容許你克隆另一個倉庫到你的項目中而且保持你的提交相對獨立。web

子模塊初步

假設你想把 Rack 庫(一個 Ruby 的 web 服務器網關接口)加入到你的項目中,可能既要保持你本身的變動,又要延續上游的變動。首先你要把外部的倉庫克隆到你的子目錄中。你經過git submodule add將外部項目加爲子模塊:服務器

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

如今你就在項目裏的rack子目錄下有了一個 Rack 項目。你能夠進入那個子目錄,進行變動,加入你本身的遠程可寫倉庫來推送你的變動,從原始倉庫拉取和歸併等等。若是你在加入子模塊後馬上運行git status,你會看到下面兩項:網站

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

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

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

若是你有多個子模塊,這個文件裏會有多個條目。很重要的一點是這個文件跟其餘文件同樣也是處於版本控制之下的,就像你的.gitignore文件同樣。它跟項目裏的其餘文件同樣能夠被推送和拉取。這是其餘克隆此項目的人獲知子模塊項目來源的途徑。spa

git status的輸出裏所列的另外一項目是 rack 。若是你運行在那上面運行git diff,會發現一些有趣的東西:版本控制

$ 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 把它視做一個子模塊,當你不在那個目錄裏時並不記錄它的內容。取而代之的是,Git 將它記錄成來自那個倉庫的一個特殊的提交。當你在那個子目錄裏修改並提交時,子項目會通知那裏的 HEAD 已經發生變動並記錄你當前正在工做的那個提交;經過那樣的方法,當其餘人克隆此項目,他們能夠從新建立一致的環境。指針

這是關於子模塊的重要一點:你記錄他們當前確切所處的提交。你不能記錄一個子模塊的master或者其餘的符號引用。code

當你提交時,會看到相似下面的:

$ 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中是一個特殊模式,基本意思是你將一個提交記錄爲一個目錄項而不是子目錄或者文件。

你能夠將rack目錄看成一個獨立的項目,保持一個指向子目錄的最新提交的指針而後反覆地更新上層項目。全部的Git命令都在兩個子目錄裏獨立工做:

$ 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

克隆一個帶子模塊的項目

這裏你將克隆一個帶子模塊的項目。當你接收到這樣一個項目,你將獲得了包含子項目的目錄,但裏面沒有文件:

$ 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目錄存在了,可是是空的。你必須運行兩個命令:git submodule init來初始化你的本地配置文件,git submodule update來從那個項目拉取全部數據並檢出你上層項目裏所列的合適的提交:

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

如今你的rack子目錄就處於你先前提交的確切狀態了。若是另一個開發者變動了 rack 的代碼並提交,你拉取那個引用而後歸併之,將獲得稍有點怪異的東西:

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

你歸併來的僅僅上是一個指向你的子模塊的指針;可是它並不更新你子模塊目錄裏的代碼,因此看起來你的工做目錄處於一個臨時狀態:

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

事情就是這樣,由於你所擁有的指向子模塊的指針和子模塊目錄的真實狀態並不匹配。爲了修復這一點,你必須再次運行git 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'

每次你從主項目中拉取一個子模塊的變動都必須這樣作。看起來很怪可是管用。

一個常見問題是當開發者對子模塊作了一個本地的變動可是並無推送到公共服務器。而後他們提交了一個指向那個非公開狀態的指針而後推送上層項目。當其餘開發者試圖運行git submodule update,那個子模塊系統會找不到所引用的提交,由於它只存在於第一個開發者的系統中。若是發生那種狀況,你會看到相似這樣的錯誤:

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

你不得不去查看誰最後變動了子模塊

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

而後,你給那個傢伙發電子郵件說他一通。

上層項目

有時候,開發者想按照他們的分組獲取一個大項目的子目錄的子集。若是你是從 CVS 或者 Subversion 遷移過來的話這個很常見,在那些系統中你已經定義了一個模塊或者子目錄的集合,而你想延續這種類型的工做流程。

在 Git 中實現這個的一個好辦法是你將每個子目錄都作成獨立的 Git 倉庫,而後建立一個上層項目的 Git 倉庫包含多個子模塊。這個辦法的一個優點是你能夠在上層項目中經過標籤和分支更爲明確地定義項目之間的關係。

子模塊的問題

使用子模塊並不是沒有任何缺點。首先,你在子模塊目錄中工做時必須相對當心。當你運行git submodule update,它會檢出項目的指定版本,可是不在分支內。這叫作得到一個分離的頭——這意味着 HEAD 文件直接指向一次提交,而不是一個符號引用。問題在於你一般並不想在一個分離的頭的環境下工做,由於太容易丟失變動了。若是你先執行了一次submodule update,而後在那個子模塊目錄裏不建立分支就進行提交,而後再次從上層項目裏運行git submodule update同時不進行提交,Git會毫無提示地覆蓋你的變動。技術上講你不會丟失工做,可是你將失去指向它的分支,所以會很難取到。

爲了不這個問題,當你在子模塊目錄裏工做時應使用git checkout -b work建立一個分支。當你再次在子模塊裏更新的時候,它仍然會覆蓋你的工做,可是至少你擁有一個能夠回溯的指針。

切換帶有子模塊的分支一樣也頗有技巧。若是你建立一個新的分支,增長了一個子模塊,而後切換回不帶該子模塊的分支,你仍然會擁有一個未被追蹤的子模塊的目錄

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

你將不得不將它移走或者刪除,這樣的話當你切換回去的時候必須從新克隆它——你可能會丟失你未推送的本地的變動或分支。

最後一個須要引發注意的是關於從子目錄切換到子模塊的。若是你已經跟蹤了你項目中的一些文件可是想把它們移到子模塊去,你必須很是當心,不然Git會生你的氣。假設你的項目中有一個子目錄裏放了 rack 的文件,而後你想將它轉換爲子模塊。若是你刪除子目錄而後運行submodule add,Git會向你大吼:

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

如今假設你在一個分支裏那樣作了。若是你嘗試切換回一個仍然在目錄裏保留那些文件而不是子模塊的分支時——你會獲得下面的錯誤:

$ 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

而後,當你切換回來,你會獲得一個空的rack目錄。你能夠運行git submodule update從新克隆,也能夠將/tmp/rack目錄從新移回空目錄。

相關文章
相關標籤/搜索