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
创建一个分支。当你再次在子模块里更新的时候,它仍然会覆盖你的工作,但是至少你拥有一个可以回溯的指针。
切换带有子模块的分支同样也很有技巧。如果你创建一个新的分支,增加了一个子模块,然后切换回不带该子模块的分支,你仍然会拥有一个未被追踪的子模块的目录。