GIT 支持子模塊,所謂 GIT 子模塊,即某個項目需要管理的模塊數目太多,而各個模塊需要不同的人或團隊維護,此時就需要在GIT中引入子模塊。GIT 引入子模塊后,其本身的上游代碼提交歷史依然可以保存下來,並且避免了在上游代碼發生變更時本地的定制代碼歸並(Merge)困難。
新建帶子模塊的項目 PyDemo
我們舉一個簡單的例子說明上述問題:假設你開發了一個項目 PyDemo,PyDemo 項目中使用了Leveldb 的 Python 綁定 cpy-leveldb(https://github.com/forhappy/cpy-leveldb),但是需要在定制 cpy-leveldb 的功能,此時你就需要在PyDemo 項目中新建一個子模塊 cpy-leveldb,然后修改本地 cpy-leveldb的實現,此時 PyDemo 把它視作一個子模塊,當你不在 cpy-leveldb 目錄里時並不記錄它的內容,取而代之的是,Git 將它記錄成來自那個倉庫的一個特殊的提交。當你在那個子目錄里修改並提交時,子項目會通知那里的 HEAD 已經發生變更並記錄你當前正在工作的那個提交
代碼如下:
forhappy@forhappy-lenovo:/tmp$ mkdir PyDemo forhappy@forhappy-lenovo:/tmp$ cd PyDemo/ forhappy@forhappy-lenovo:/tmp/PyDemo$ ls forhappy@forhappy-lenovo:/tmp/PyDemo$ git init Initialized empty Git repository in /tmp/PyDemo/.git/ forhappy@forhappy-lenovo:/tmp/PyDemo$ touch README forhappy@forhappy-lenovo:/tmp/PyDemo$ vim README forhappy@forhappy-lenovo:/tmp/PyDemo$ git add . forhappy@forhappy-lenovo:/tmp/PyDemo$ git ci -m "Initial commit." [master (root-commit) face532] Initial commit. 1 file changed, 1 insertion(+) create mode 100644 README forhappy@forhappy-lenovo:/tmp/PyDemo$ git st # On branch master nothing to commit (working directory clean) forhappy@forhappy-lenovo:/tmp/PyDemo$ git submodule add git@github.com:forhappy/cpy-leveldb.git Cloning into 'cpy-leveldb'... remote: Counting objects: 888, done. remote: Compressing objects: 100% (475/475), done.
建立了 PyDemo 項目的子模塊后我們查看一下:
forhappy@forhappy-lenovo:/tmp/PyDemo$ ls cpy-leveldb README forhappy@forhappy-lenovo:/tmp/PyDemo$ git st # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: .gitmodules # new file: cpy-leveldb # forhappy@forhappy-lenovo:/tmp/PyDemo$ git add . forhappy@forhappy-lenovo:/tmp/PyDemo$ git ci -m "add submodule cpy-leveldb" [master c02fba3] add submodule cpy-leveldb 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 cpy-leveldb
首先你注意到有一個.gitmodules
文件。這是一個配置文件,保存了子項目 URL 和你拉取到的本地子目錄
forhappy@forhappy-lenovo:/tmp/PyDemo$ cat .gitmodules [submodule "cpy-leveldb"] path = cpy-leveldb url = git@github.com:forhappy/cpy-leveldb.git
盡管 cpy-leveldb 是你工作目錄里的子目錄,但 GIT 把它視作一個子模塊,當你不在該目錄里時並不記錄它的內容。注意 cpy-leveldb 條目的 160000 模式,這在 GIT 中是一個特殊模式,基本意思是你將一個提交記錄為一個目錄項而不是子目錄或者文件。
好了,至此一個帶子模塊的 GIT 倉庫就構建完了,你可以在主項目中添加,刪除或修改源碼,或者在 cpy-leveldb 中定制你自己的功能,兩者不會相互影響,各自保持自己的提交記錄。
克隆帶子模塊的項目
當你克隆一個帶子項目的 GIT 項目時,你將得到了包含子項目的目錄,但里面沒有文件:
forhappy@forhappy-lenovo:/tmp/PyDemo-work$ git clone /tmp/PyDemo Cloning into 'PyDemo'... done. forhappy@forhappy-lenovo:/tmp/PyDemo-work$ ls PyDemo forhappy@forhappy-lenovo:/tmp/PyDemo-work$ cd PyDemo/ forhappy@forhappy-lenovo:/tmp/PyDemo-work/PyDemo$ ls cpy-leveldb README forhappy@forhappy-lenovo:/tmp/PyDemo-work/PyDemo$ cd cpy-leveldb/ forhappy@forhappy-lenovo:/tmp/PyDemo-work/PyDemo/cpy-leveldb$ ls forhappy@forhappy-lenovo:/tmp/PyDemo-work/PyDemo/cpy-leveldb$ cd ..
此時你需要執行:git submodule init
來初始化你的本地配置文件,另外你還需要執行:git submodule update
來從 cpy-leveldb 項目拉取所有數據並檢出你上層項目里所列的合適的提交。
forhappy@forhappy-lenovo:/tmp/PyDemo-work/PyDemo$ git submodule init Submodule 'cpy-leveldb' (git@github.com:forhappy/cpy-leveldb.git) registered for path 'cpy-leveldb' forhappy@forhappy-lenovo:/tmp/PyDemo-work/PyDemo$ git submodule update Cloning into 'cpy-leveldb'... remote: Counting objects: 888, done. remote: Compressing objects: 100% (475/475), done.
一個常見問題是當開發者對子模塊做了一個本地的變更但是並沒有推送到公共服務器。然后他們提交了一個指向那個非公開狀態的指針然后推送上層項目。當其他開發者試圖運行git submodule update
,那個子模塊系統會找不到所引用的提交,因為它只存在於第一個開發者的系統中。如果發生那種情況,你會看到類似這樣的錯誤:
$ git submodule update fatal: reference isn’t a tree: 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 Unable to checkout '6c5e70b984a60b3cecd395edd5ba7575bf58e0' in submodule path 'cpy-leveldb'
子模塊問題
使用子模塊並非沒有任何缺點。首先,你在子模塊目錄中工作時必須相對小心。當你運行git submodule update
,它會檢出項目的指定版本,但是不在分支內。這叫做獲得一個分離的頭——這意味着 HEAD 文件直接指向一次提交,而不是一個符號引用。問題在於你通常並不想在一個分離的頭的環境下工作,因為太容易丟失變更了。如果你先執行了一次submodule update
,然后在那個子模塊目錄里不創建分支就進行提交,然后再次從上層項目里運行git submodule update
同時不進行提交,Git會毫無提示地覆蓋你的變更。技術上講你不會丟失工作,但是你將失去指向它的分支,因此會很難取到。
為了避免這個問題,當你在子模塊目錄里工作時應使用 git checkout -b
創建一個分支。當你再次在子模塊里更新的時候,它仍然會覆蓋你的工作,但是至少你擁有一個可以回溯的指針。
切換帶有子模塊的分支同樣也很有技巧。如果你創建一個新的分支,增加了一個子模塊,然后切換回不帶該子模塊的分支,你仍然會擁有一個未被追蹤的子模塊的目錄。
本文部分段落參考來源
Pro Git:http://github.danmarner.com/section/ch6-6/