Git使用submodule(子模块)解决Git仓库的嵌套问题,允许一个 Git 仓库作为另一个 Git 仓库的子目录。 能够将一个仓库同步到自己的项目中的同时,保持Git仓库提交的独立性。详细参见《pro git》子模块

目录

  1. submodule的原理
  2. 使用submodule
    1. 1、新建submodule
    2. 2、克隆包含submodule的项目
    3. 3、更新submodule
      1. 关于 git fetch 与 git pull
    4. 4、修改并提交submodule
      1. 提交前的fetch
      2. 提交
    5. 5、解决submodule冲突
  3. 写在最后

submodule的原理

submodule的实现很巧妙也很简单。其实submodule只是在原Git仓库中多添加了一条或者几条子模块的记录(保存在 .gitmodule 中),它记录了仓库中包含的子模块的路径和URL。

1
2
3
[submodule "project/submodule"]
path = project/submodule # 在项目中的路径,即子模块所在的文件夹是project/submodule
url = https://github.com/submodule.git # 远程仓库的URL

当需要同步子模块的文件时,通过指定的URL使用HTTP协议下载到本地,同样如果对子模块里的内容做出修改之后,通过该URL可以提交到子模块的远程仓库(而不是提交到依赖该子模块项目的仓库)。

为了保证父仓库当前子模块的内容不会随着子模块仓库内容更新而改变,父仓库中还记录了子模块当前指向的提交记录。

例如:project仓库中包含了一个submodule子模块,当前子模块指向的远程仓库中的索引是 ef34 。Git会对每一次提交建立一个索引,该索引与提交记录一一对应。

1
2
3
4
---project
|---submodule@ef34
|---others
|---.git

如果子模块仓库发生了不兼容更新,最新的提交记录是 ty23 。由于父仓库中子模块指向的索引是 ef34 ,即使子模块远程仓库中的内容发生了变化,也不会影响依赖于旧代码的项目。project在clone或者pull的时候,拉取的仍然是 ef34 的内容。如果此时project需要使用最新的submodule代码,通过更新子模块即可将子模块的索引修改为 ty23

1
2
3
4
5
commit ty23 
commit ef34
commit jk93
...
...

使用submodule

1、新建submodule

使用git submodule add添加新的子模块

1
$ git submodule add "URL"

下图演示了添加名为Game的子模块

之后会发现目录下多了一个Game目录和.gitmodules的文件。其中Game目录下是你所希望包含项目的分支代码,而gitmodules的内容如下。也就是之前提到的submodule使用子模块的名称和URL记录本仓库包含的子模块。

1
2
3
[submodule "Game"]
path = Game
url = https://github.com/yx1302317313/Game.git

提交之后查看远程仓库会发现添加的子模块的文件并没有传到远程仓库,子模块的文件夹实际是一个URL,指向了包含子模块本身的远程仓库。

2、克隆包含submodule的项目

当使用git clone 克隆一个包含子模块的项目时,子模块的文件并不会一同并拉取到本地,而只是一个空目录。

这时需要使用git submodule init用来初始化本地配置文件,而 git submodule update则从该项目中抓取所有数据并检出父项目中列出的合适的提交。


一个更简单的方法是给git clone加上递归选项 --recursive

1
$ git clone --recursive "URL" # 自动初始化并更新仓库中的每一个子模块

[注意]: 由于git的子模块跟踪的是提交记录(commit id),所以拉取下来的是一个游离分支,其实是将一个提交记录作为了一个游离的分支。这时候还需要切换到master分支。

1
2
$ cd Game
$ git checkout master

所以整个克隆的步骤如下:

  1. 克隆远程仓库:git clone “URL”
  2. 初始化子模块:git submodule init
  3. 拉取子模块的内容:git submodule update
  4. 进入子模块切换至主分支: git checkout master

3、更新submodule

  1. 进入submodule的目录。
  2. 使用git fetchgit merge更新submodule,与远程仓库的最新代码合并(当然也可以直接使用git pull)。

关于 git fetch 与 git pull

git pull = git fetch + git merge, 可以参考stack overflow

__更简单的方式__:运行git submodule update --remote, 使用该命令Git将会进入子模块然后进行抓取并更新。

注意:该命令默认fetch和merge主分支(master),如果是其他分支,需要进行配置(见《pro git》)。

4、修改并提交submodule

提交前的fetch

当修改了submodule的文件时,这里有一个需要注意的地方。一般我们在提交之前都需要查看远程分支是否有更新,会不会有冲突。我们的方法是使用git fetch抓取远程分支查看是否更新然后进行git merge。(使用git pull也没什么太大的问题)。

需要注意的地方是git submodule update,之前提到这个命令也可以完成上面的功能,还更方便。

但是《pro git》中提到

当我们运行 git submodule update 从子模块仓库中抓取修改时,Git 将会获得这些改动并更新子目录中的文件,但是会将子仓库留在一个称作“游离的 HEAD”的状态。 这意味着没有本地工作分支(例如“master” )跟踪改动。 所以你做的任何改动都不会被跟踪。

这段话感觉很难理解,英文版说的更直接一点。

So far, when we’ve run the git submodule update command to fetch changes from the submodule repositories, Git would get the changes and update the files in the subdirectory but will leave the sub-repository in what’s called a “detached HEAD” state. This means that there is no local working branch (like “master”, for example) tracking changes. With no working branch tracking changes, that means even if you commit changes to the submodule, those changes will quite possibly be lost the next time you run git submodule update. You have to do some extra steps if you want changes in a submodule to be tracked.

关键是这句话

With no working branch tracking changes, that means even if you commit changes to the submodule, those changes will quite possibly be lost the next time you run git submodule update.

意思就是如果远程仓库发生了改动,并且你本地有未推送到远程仓库的提交(commit),执行git submodule update后你的提交并不会与远程仓库进行合并(merge)。而是成为了一个游离的分支。

像这样,在update之后,分支变成了a58a3ae,而不再是master(master变成了游离状态)。


如果发生了这种情况,处理很简单,使用git checkout master

正确的做法是使用git submodule update --merge,这个命令会告诉Git进行合并(前提是不发生冲突,如果发生了冲突请看下一节)。

提交

注意:使用git push并不会提交子模块。

为了防止这种错误发生,git推荐提交使用git push --recurse-submodules=check,如果没有提交子模块,会提示你使用两种方法完成子模块的提交。

  1. 进入各个子模块目录使用git push提交各个子模块,然后提交你的项目。
  2. 使用git push --recurse-submodules=on-demand命令提交, 该命令会自动提交子模块,然后再提交你的项目。如果子模块提交失败,整个提交也会失败。

到这里,子模块的使用就快基本完成了。最后一部分是最令人麻烦的conflict。

5、解决submodule冲突

解决冲突的步骤如下:

  1. 进入子模块目录,解决冲突。(方法同解决正常的git冲突)
  2. 在子模块中提交解决冲突的记录。
  3. 切换到你的项目目录。
  4. 提交解决子模块冲突的记录。
  5. 推送到远程(如果需要)。

git pull 发生冲突


找到冲突的文件


解决冲突,并在子模块目录提交记录


在父目录中提交子模块冲突的提交记录


成功推送到远程仓库

写在最后

当然子模块还有些技巧,比如子模块遍历,命令别名等,见《pro git》。