天天看點

git子子產品

經常有這樣的事情,當你在一個項目上工作時,你需要在其中使用另外一個項目。也許它是一個第三方開發的庫或者是你獨立開發和并在多個父項目中使用的。這個場景下一個常見的問題産生了:你想将兩個項目單獨處理但是又需要在其中一個中使用另外一個。

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

Git 通過子子產品處理這個問題。子子產品允許你将一個 Git 倉庫當作另外一個Git倉庫的子目錄。這允許你克隆另外一個倉庫到你的項目中并且保持你的送出相對獨立。

子子產品初步

假設你想把 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 和你拉取到的本地子目錄

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

如果你有多個子子產品,這個檔案裡會有多個條目。很重要的一點是這個檔案跟其他檔案一樣也是處于版本控制之下的,就像你的

.gitignore

檔案一樣。它跟項目裡的其他檔案一樣可以被推送和拉取。這是其他克隆此項目的人獲知子子產品項目來源的途徑。

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

或者其他的符号引用。

當你送出時,會看到類似下面的:

$ 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 <[email protected]>
Date:   Thu Apr 9 09:03:56 2009 -0700

    first commit with submodule rack
$ cd rack/
$ git log -1
commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433
Author: Christian Neukirchen <[email protected]>
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
# Changed but not updated:
#   (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 [email protected]: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 <[email protected]>
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 [email protected]: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 [email protected]:schacon/rack.git rack
'rack' already exists in the index
           

你必須先将

rack

目錄撤回。然後你才能加入子子產品:

$ git rm -r rack
$ git submodule add [email protected]: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

目錄重新移回空目錄。

繼續閱讀