# 子模塊
在項目開發時,你有可能經常性地想要去引用一些庫文件或其它資源文件。手動的方法就是直接下載那些必要的代碼文件,然后拷貝到你的項目中,最后將這些新的文件提交到你的 Git 倉庫中去。
雖然這是一種有效的方法,但是這種操作并不是最簡單有效的。如果只是任意地將這些庫文件提交到你的項目中,將帶來一系列的問題:
* 外部代碼和自己開發的代碼會被合并保存在一個項目中。其實那些庫文件自身就應該是一個項目,并且也應該獨立于我們的工作之外。在我們當前項目的版本控制系統中,它們并不需要被保存。
* 如果庫文件發生了變化(可能因為修復錯誤或是添加新的功能),更新這些庫文件的代碼對我們來說會是很繁瑣的事。我們需要再次下載它的原代碼文件,并且替換掉在倉庫中已有的文件。
由于這些都是在日常項目開發時非常普遍存在的問題,所以 Git 也提供了一個解決方案:子模塊(Submodule)。
## 倉庫包含其它的倉庫
一個 “子模塊” 其實就是一個標準的 Git 倉庫。不同的是,它被包含在另一個主項目的倉庫中。一般情況下,它包含一些庫文件和其它資源文件,你可以簡單地把這些庫文件作為一個子模塊添加到你的主項目中。
一個子模塊也是一個功能齊全的 Git 倉庫,就內部而言它和別的倉庫沒有什么區別,你可以對它進行修改、提交、抓取、推送等等操作。
讓我們來看看在實際操作中子模塊是如何工作的吧。
## 添加一個子模塊
在這個簡單的項目中,我們建立一個新的 “lib” 文件目錄用來存放一些庫文件。
```
$ mkdir lib
$ cd lib
```
使用 “git submodule add” 命令,我們會從 GitHub 中添加一個小的 Javascript 庫:
```
$ git submodule add https://github.com/djyde/ToProgress
```
來讓我們來看看現在發生了什么:
* (1) 這個命令將對一個指定的 Git 倉庫進行了一個簡單地克隆操作:
```
Cloning into 'lib/ToProgress'...
remote: Counting objects: 180, done.
remote: Compressing objects: 100% (89/89), done.
remote: Total 180 (delta 51), reused 0 (delta 0), pack-reused 91
Receiving objects: 100% (180/180), 29.99 KiB | 0 bytes/s, done.
Resolving deltas: 100% (90/90), done.
Checking connectivity... done.
```
* (2) 當然這一切也都會反映在我們當前項目的文件結構上。在項目中的 “lib” 目錄中包括了一個新的 “ToProgess” 文件目錄。通過這個文件目錄所包含的 “.git” 子文件夾我們就能確認,這就是一個標準的 Git 倉庫。
<figure></figure>
##### 概念
必須要再次闡述一下:一個子模塊的內容并**不保存**在它的父倉庫中。其實只有它的遠程 URL 會被記錄在父倉庫中,以及它在主項目中的本地路徑和簽出的版本。
當然,子模塊的工作文件都放置在你項目的指定的目錄中。最后當你要使用這些庫文件時,你會發現它們并不是主項目的版本控制的一部分。
* (3) 一個新的 “.gitmodules” 文件會被創建。這個文件就是 Git 用來跟蹤我們的子模塊并保存它的配置信息的:
```
[submodule "lib/ToProgress"]
path = lib/ToProgress
url = https://github.com/djyde/ToProgress
```
* (4) 你可能會對 Git 的內部工作原理感興趣。除了 “.gitmodules” 配置文件,Git 也會在你本地的 “.git/config” 文件中保存對子模塊的記錄。最終它也會在它的 “.git/modules” 目錄中保存每一個子模塊的 “.git” 倉庫。
##### 概念
Git 內部對子模塊的管理是非常復雜的,就像你已經看到的那些 “.gitmodules”,“.git/config”,和 “.git/modules” 等等的條目那樣。因此,這里強烈**不建議**你去手動地修改這些配置文件。為了安全起見,一定要使用適當的 Git 命令來操作子模塊。
現在讓我們來看看當前的項目狀態:
```
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: .gitmodules
new file: lib/ToProgress</file>
```
像任何其他修改一樣, Git 添加了一個子模塊,并且要求你提交這個改動到倉庫中:
```
$ git commit -m "Add 'ToProgress' Javascript library as Submodule"
```
現在,我們已經成功地添加了一個子模塊到我們主項目中來了!在了解幾個不同的案例之前,讓我們先來看看如何克隆一個已經包括了若干子模塊的項目。
## 克隆一個項目和它的子模塊
你已經知道了,一個項目倉庫并_不_包含子模塊的文件。主項目倉庫僅僅保存子模塊的_配置信息_來作為版本管理的一部分。
這就表示,當你要克隆一個帶有子模塊的項目時,在默認的情況下 “git clone” 命令僅僅接收這個項目本身。我們的 “lib” 只是一個空目錄,里面沒有任何文件。
你有兩個選擇去設置這個 “lib” 目錄(或者是任何一個你保存的其他子模塊,“lib” 在這里只是一個例子):
* (a) 你可以通過將 “--recurse-submodules” 參數加在 “git clone” 上,從而讓 Git 知道,當克隆完成的時候要去初始化所有的子模塊。
* (b) 如果你僅僅只是簡單地使用了 “git clone” 命令,并沒有附帶任何參數,你就需要在完成之后通過 “git submodule update --init --recursive” 命令來初始化那些子模塊。
## 簽出一個版本
一個 Git 倉庫可以保存無限多個提交版本,但是僅僅只有一個文件版本能保存在你當前的工作副本中。就像任何其他的 Git 倉庫一樣,你必須自己來決定在子模塊上的哪一個版本應該被簽出到你的工作副本中。
##### 概念
和一個普通的 Git 倉庫不一樣的是,子模塊永遠指向一個特定的提交,而不是分支。這是因為一個分支的內容可以在任何時間通過新的提交來改變。所以指向一個特定的提交版本就能始終保證代碼的正確。
比方說,我們希望在我們項目中使用一個舊版本的 “ToProgress” 庫。首先,我需要看一下這個庫的提交歷史記錄。我們需要切換到這個子模塊的根目錄下,然后執行 “log” 命令:
```
$ cd lib/ToProgress/
$ git log --oneline --decorate
```
在我們來檢查實際的歷史記錄之前,有一點我想強調一下:Git 命令是對上下文環境很敏感的!也就是說,通過命令行來切換到子模塊的目錄后,我們執行的所有 Git 命令僅僅只會對子模塊有效,而不是對它的父倉庫。
現在歷史記錄被打印出來了,我們會發現這個提交被標記成了 “0.1.1”:
```
83298f7 (HEAD, master) update .gitignore
a3b6186 remove page
ed693b7 update doc
3557a0e (tag: 0.1.1) change version code
2421796 update readme
```
這就是我們希望在我們的項目使用的版本。首先我們可以來簡單地看看這個提交:
```
$ git checkout 0.1.1
```
再讓我們來看看父倉庫。在主項目的目錄中執行下面的命令:
```
$ git submodule status
+3557a0e0f7280fb3aba18fb9035d204c7de6344f lib/ToProgress (0.1.1)
```
通過使用 “git submodule status”,我們可以查看子模塊的哪一個版本在當前被簽出了。在 hash 之前的 “+” 符號是非常重要的,它表明該子模塊在它父倉庫的官方記錄中存在一個**不同**的版本。這是合理的,因為我們的確修改并簽出了版本標記為 “0.1.1” 的提交。
如果在父倉庫上執行 “git status” 命令,我們就會發現像任何其他的變化一樣,Git 移動了指向這個子模塊的指針:
```
$ 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: lib/ToProgress (new commits)</file> </file>
```
為了使這個改動生效,我們現在需要提交它到倉庫中:
```
$ git commit -a -m "Moved Submodule pointer to version 1.1.0"
```
## 更新一個子模塊,當指向它的指針發生了變化之后
我們看到了如何簽出一個子模塊的特定版本。但是,如果是開發團隊的其他成員在項目中改變了對子模塊的指針呢?當他移動了指向子模塊的指針到另一個版本之后,我們就要整合他的改動,例如通過抓取,合并,或是 rebase :
```
$ git pull
Updating 43d0c47..3919c52
Fast-forward
lib/ToProgress | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
```
Git 會以一個相當含蓄的方式告訴我們,“lib/ToProgress” 發生了變化。再次使用 “git submodule” 命令來索取更多和更細節的信息:
```
$ git submodule status
+83298f72c975c29f727c846579c297938492b245 lib/ToProgress (0.1.1-8-g83298f7)
```
還記得那個小的 “+” 符號嗎?這表明子模塊發生了變化,我們當前簽出的子模塊版本不是主項目使用的中的 “官方” 版本。
使用 “update” 命令可以幫助我們修正它:
```
$ git submodule update lib/ToProgress
Submodule path 'lib/ToProgress': checked out '3557a0e0f7280fb3aba18fb9035d204c7de6344f'
```
##### 注釋
在大多數情況下,使用 “git submodule” 家族的命令是不需要指定一個特定子模塊的。但是正如上面的例子一樣,如果我們給出一個子模塊的路徑,這個操作就只會針對那個給定的子模塊。
現在我們簽出了相同版本的子模塊,這就是之前另一個團隊成員提交到項目中的那個。
值得注意的是,“update” 命令會為你下載子模塊的改動。設想一下,你的隊友在你之前已經改變了指向子模塊版本的指針。在這種情況下,Git 會為你獲取在子模塊的相應版本,并且簽出這個子模塊的版本,非常方便。
## 檢查子模塊的最新變化
正常情況下,你是不會經常改變庫文件的代碼的。如果這個子模塊被真正地測試過,并且你也知道它非常匹配你的開發項目時,你才會使用它。
無論如何,子模塊功能最大優點之一就是你可以很方便地與最新的發行版本同步(也許只是同步一個小小的優化)。
讓我們來看看子模塊是否提供了新的代碼版本:
```
$ cd lib/ToProgress
$ git fetch
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/djyde/ToProgress
83298f7..3e20bc2 master -> origin/master
```
請注意!現在我們切換到了子模塊的文件夾,之后的操作就像對待任何一個普通的項目倉庫一樣(因為它就是一個普通的 Git 倉庫)。
現在 “git fetch” 命令顯示,當前的確存在一些新的改動在子模塊的遠程上。
##### 概念
在我們準備整合這些改動之前,我想再次重申一下。當檢查這個子模塊的狀態時,我們會發現我們正處在一個**detached HEAD**上:
```
$ git status
HEAD detached at 3557a0e
nothing to commit, working directory clean
```
一般情況下,在 Git 中你總是會簽出某個分支。然而你也_可以_選擇簽出一個特定的**提交**(而**不是**一個分支)。這是一種比較罕見的情況,在 Git 中通常應該避免。
然而在子模塊上工作時,簽出某個提交的情況是非常正常的。你要確保在你的項目中,簽出一個確切的靜態的提交(不是一個分支),并轉移到一個較新的提交上。
現在讓我們通過拉取操作來整合那些新的改動到你的本地子模塊倉庫中吧。請注意!你不能使用那個簡寫的 “git pull” 命令語法,而是需要指定特定的遠程和分支。
這是因為我們正處于 “detached HEAD” 狀態。因為在這個時刻你不是在本地分支上,你需要告訴 Git,你想要把拉取出來的改動整合到哪一個分支上去。
```
$ git pull origin master
```
如果你現在已經執行過一遍 “git status” 命令了,你會發現我們的狀態仍然處于 detached HEAD,并且在同一個提交上,當前被簽出的內容并沒有發生改變。如果我們在項目中想要使用這個升級后的子模塊的代碼,我們必須明確地將 HEAD 指針移動到分支上:
```
$ git checkout master
```
我們已經完成了在子模塊上的工作,現在讓我們切換回我們主項目吧:
```
$ cd ../..
$ git submodule status
+3e20bc25457aa56bdb243c0e5c77549ea0a6a927 lib/ToProgress (0.1.1-9-g3e20bc2)
```
由于我們剛剛移動了子模塊的指針到了一個不同的版本,我們需要將這個改動提交到父倉庫中去,從而讓它成為主項目當前正式引用的 “官方” 版本。
## 在子模塊中工作
有些時候,你可能會想要在子模塊中作一些自己的改動。你已經知道了在子模塊中工作就和在一個普通的 Git 倉庫中工作一樣,你在子模塊目錄中執行的所有的 Git 命令只會對這個子模塊倉庫有效。
比方說,你想對子模塊進行一個小小的改動,你編輯了相關的文件,把它們添加到暫存區,并且提交它。
現在你可能會踩到第一塊香蕉皮。因為如果當前你正處于一個 detached HEAD 狀態,你的提交會迷失方向,它并沒有關聯到任何一個分支。一旦你簽出了其他的東西,它的內容就會丟失。所以你應該在提交之前確保,你當前已經在子模塊中簽出了一個分支。
除此之外,你已經學到的其它一切 Git 操作都仍然適用。在主項目中 “git submodule status” 會告訴你指向該子模塊的指針被移動了,你必須提交這個改動。
順便提一下,如果你的子模塊中還有_未提交_的改動,Git 也會在主項目中提醒你:
```
$ git status
...
modified: lib/ToProgress (modified content)
```
請務必始終保持子模塊有一個干凈的狀態。
## 刪除一個子模塊
盡管很少會從項目中刪除一個子模塊,但是如果你確定想要這么做,也請不要手動地刪除它,一旦所的有配置文件被打亂,將會不可避免地導致出現一系列問題。
```
$ git submodule deinit lib/ToProgress
$ git rm lib/ToPogress
$ git status
...
modified: .gitmodules
deleted: lib/ToProgress
```
使用 “git submodule deinit”,我們可以確保從配置文件中完全地刪除一個子模塊。
使用 “git rm” ,我們可以最終刪除這個子模塊的文件,包括一些其它廢棄的部分。
提交這些改動,這個子模塊就會從你的項目中被徹底地刪除了。
- Learn Version Control with Git 中文版
- 前言
- Part 1 - 基礎知識
- 什么是版本控制?
- 為什么要使用版本控制系統?
- 準備工作
- 版本控制的基本工作流程
- 從一個未被納入版本控制的項目開始
- 從一個已被納入版本控制的項目開始
- 工作在你的項目上
- Part 2 - 分支與合并
- 分支可以改變你的生命
- 在分支上工作
- 暫時保存更改
- 切換一個本地分支
- 合并改動
- 分支的工作流程
- Part 3 - 遠程倉庫
- 關于遠程倉庫
- 連接一個遠程倉庫
- 查看遠程數據
- 整合遠程的改動
- 發布一個本地分支
- 刪除分支
- Part 4 - 高級應用
- 撤銷操作
- 用 diff 來檢查改動
- 處理合并沖突
- Rebase 代替合并
- 子模塊
- git-flow 的工作流程
- 使用 SSH 公鑰驗證
- Part 5 - 工具與服務
- 桌面應用程序
- 比較和整合工具
- 代碼托管服務
- 更多學習資源
- 附錄
- 版本控制的最佳實踐
- 命令 101
- 從 Subversion 過渡到 Git
- 為什么選擇 Git