LV01-Git-06-子模块
本文主要Git子模块相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
点击查看本文参考资料
参考方向 | 参考原文 |
Git 官网 | https://git-scm.com/ |
Git 官方文档 | https://git-scm.com/doc |
Pro Git Book | https://git-scm.com/book/zh/v2 |
Git 快速使用指南 | https://training.github.com/downloads/zh_CN/github-git-cheat-sheet/ |
Visual Git Cheat Sheet | https://ndpsoftware.com/git-cheatsheet.html#loc=index |
一、子模块
1. 出现的问题
有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者独立开发的,用于多个父项目的库。 现在问题来了:想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。
就比如,我的笔记博客网站,它是基于hexo的,它不仅包含hexo的架构文件,还包含主题,还包含我的笔记:
1 | . |
那么这个仓库的提交记录中就会包含笔记的更新和站点的更新,但是这两者是没什么关系的,当提交记录多起来之后,我想要找站点提交记录,但是笔记更新了好几百次,这找起来就很麻烦。那么,我是不是可以换一个单独的仓库来作为笔记的仓库,在生成静态网页的时候把笔记拷贝过来?当然可以了,只需要保证hexo生成静态网页的时候,笔记存在于对应的位置即可。
2. Git的解决办法
Git 通过子模块来解决这个问题。 子模块允许我们将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让我们将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
二、基本用法
1. 项目结构
我们现在要创建这样一个结构的项目:
1 | projext/ |
其中 module_a 需要是一个单独的版本库。我们接下来就来了解一下子模块怎么使用。
2. 创建版本库
2.1 project
我们创建project版本库,并提交readme.md文件
1 | git init project |
2.2 module_a
1 | git init module_a |
2.3 目录结构
现在目录结构如下:
1 | sumu@sumu-virtual-machine:~/submodule$ tree |
现在两个仓库互不干扰。
3. 引入子模块
3.1 怎引入子模块?
我们现在在project中引入子模块module_a:
1 | cd project |
然后就报错了:
1 | sumu@sumu-virtual-machine:~/submodule/project$ git submodule add ../module_a submodule_a |
这是因为 Git 默认不允许使用本地文件传输。
1 | git config --global protocol.file.allow always |
然后重新添加就可以了,添加后我们可以看一下状态:
1 | sumu@sumu-virtual-machine:~/submodule/project$ git submodule add ../module_a submodule_a |
3.2 有什么变化?
首先应当注意到新的 .gitmodules
文件。 该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射:
1 | [submodule "submodule_a"] |
如果有多个子模块,该文件中就会有多条记录。 要重点注意的是,该文件也像 .gitignore
文件一样受到(通过)版本控制。 它会和该项目的其他部分一同被拉取推送。 这就是克隆该项目的人知道去哪获得子模块的原因。
在 git status
输出中列出的另一个是项目文件夹记录。 如果你运行 git diff
,会看到类似下面的信息:
1 | sumu@sumu-virtual-machine:~/submodule/project$ git diff --cached submodule_a/ |
虽然 submodule_a
是工作目录中的一个子目录,但 Git 还是会将它视作一个子模块。当不在那个目录中时,Git 并不会跟踪它的内容, 而是将它看作子模块仓库中的某个具体的提交。
如果想看到更漂亮的差异输出,可以给 git diff
传递 --submodule
选项:
1 | git diff --cached --submodule |
会有如下输出:
1 | sumu@sumu-virtual-machine:~/submodule/project$ git diff --cached --submodule |
3.3 提交子模块的引入
当提交时,会看到类似下面的信息:
1 | sumu@sumu-virtual-machine:~/submodule/project$ git add . |
4. 修改子模块
4.1 在子模块修改会影响父项目吗
4.1.1 实践一下
修改子模块之后只对子模块的版本库产生影响,对父项目的版本库不会产生任何影响,我们修改子模块的内容:
1 | cd module_a |
我们可以去父项目查看一下:
1 | sumu@sumu-virtual-machine:~/submodule/module_a$ git status |
可以发现是父项目是没有做任何修改的。我们先撤销修改:
1 | git checkout -- . |
4.1.2 结论
子模块是独立的,怎么修改都不会影响父项目。
4.2 在父项目修改子模块?
4.2.1 实践一下
例如,我们在project中修改子模块的文件:
1 | cd project |
然后我们看一下状态:
1 | sumu@sumu-virtual-machine:~/submodule/project$ echo "modify module_a (project/submodule_a)!" >> submodule_a/readme.md |
我们尝试提交一下:
1 | sumu@sumu-virtual-machine:~/submodule/project$ git add . |
会发现根本没有办法修改。那我们进入project/submodule_a
再试一下:
1 | sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ git status |
发现这个时候是可以的。那么这个时候我们查看一下两个目录的提交记录:
1 | sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ git mylog |
会发现,我们在 project/submodule_a
中的提交记录只能在子模块目录下看到,我们回到父项目目录,提交记录就没有发生变化。
4.2.2 结论
(1)在父项目中,可以修改子模块,但是无法在父项目中对子模块的修改进行提交和记录。
(2)从父项目进入子模块的目录可以正常提交子模块的修改,但是提交记录只会出现在父项目下的子模块目录,而不会出现在父项目的提交中。
5. 更新子模块
通过上面的了解,我们知道子模块修改后是独立的,那么父项目怎么获取最新的子模块?可以在父项目目录下执行以下命令:
1 | git submodule update |
我们上面在父项目中对子模块修改了,但是子模块自己的目录并没有做任何改变,我们来更新一下:
1 | sumu@sumu-virtual-machine:~/submodule/project$ git submodule update |
从这里就可以看出,这个命令强制更新子模块的源目录的内容并且覆盖了父项目中的子模块。
6. 删除子模块
删除子模块比较麻烦,需要手动删除相关的文件,否则在添加子模块时有可能出现错误。
- (1)删除子模块目录
1 | cd project |
- (2)删除
.gitmodules
文件中相关子模块的信息,类似以下信息:
1 | [submodule "submodule_a"] |
- (3)删除
.git/config
中相关子模块信息,类似于:
1 | [submodule "submodule_a"] |
- (4)删除
.git
文件夹中的相关子模块文件
1 | rm -rf .git/modules/submodule_a |
7. 查看子模块
1 | git submodule |
三、远程仓库
这一次就以我的博客站点为例了。两个仓库(后来都转成私有仓库了)的链接如下:
这部分我就是在windows下进行的了。
1. 添加子模块
添加远程模块的时候格式是这样的:
1 | git submodule add <url> <repo_name> |
在这里就是:
1 | cd hexo-site |
然后就会看到如下提示:
1 | D:\sumu_blog\site [master ≡]> git submodule add git@github.com:sumumm/site-docs.git source/_posts/docs |
就会发现在 hexo-site/source/_posts/docs
出现了我们的测试笔记:
1 | D:\sumu_blog\site [master ≡ +2 ~0 -0 ~]> ls .\source\_posts\docs\01-linux开发\ |
这个时候我们就是可以正常执行下面的命令来生成静态网页:
1 | hexo g |
2. 更新子模块
2.1 怎么更新远程最新版本?
这时候就不能单纯的用git submodule update
,更新远程项目的最新版本是以下命令:
1 | git submodule update --remote |
然后会有如下输出:
1 | D:\sumu_blog\site [master ≡ +2 ~0 -0 ~]> git submodule update --remote |
这个会强制更新到和子模块远程完全相同的版本。
2.2 不加–remote为什么不行?
2.2.1 无 --remote
参数
1 | git submodule update |
数据来源:父仓库中记录的 本地提交哈希值
更新逻辑:
(1)读取父仓库的 .gitmodules
配置和当前提交记录的 子模块哈希值
(2)将子模块检出(checkout)到该哈希值对应的提交
(3)不连接远程仓库,仅使用本地已有的子模块仓库数据
- 适用场景:切换分支/回退版本后,将子模块同步到父仓库记录的版本
2.2.2 使用 --remote
参数
1 | git submodule update --remote |
- 数据来源:子模块配置的 远程仓库(origin)
- 更新逻辑:
(1)进入子模块目录
(2)执行 git fetch origin
获取远程最新数据
(3)检出 .gitmodules
中配置分支的最新提交(如未配置则用默认分支)
(4)将新提交哈希记录到父仓库
2.3 怎么确定子模块版本?
git submodule update
命令是怎么确认子模块的版本的?父仓库的 当前提交 中存储了子模块的 精确 Git 提交哈希值(例如 160000
模式的树对象)。我们可以通过以下命令查看:
1 | git ls-tree HEAD <submodule-path> |
会看到如下打印信息:
1 | D:\sumu_blog\site [master ≡]> git ls-tree HEAD .\source\_posts\docs\ |
当执行 git submodule update
时,读取父仓库当前提交中子模块路径对应的 提交哈希值,进入子模块目录,执行 git checkout <commit-hash>
,将子模块切换到该哈希值对应的版本(分离头指针状态)。若子模块未初始化/克隆,先自动执行 git submodule init
克隆仓库。
所以其实子模块不会自动更新到最新提交,而是严格使用父仓库记录的旧版本。这样应该是为了在父仓库提交后,让其他协作者运行 git submodule update
会获得完全相同的子模块版本,确保一致性。更新的时候检出父仓库记录的提交(不联网更新)。当我们执行了git submodule update --remote
,就会强制联网拉取子模块远程仓库的 最新提交(可以在 .gitmodules
中配置 branch
属性)。
但是,这个时候父仓库的记录信息并不会更新,例如,我这里有一个带子模块的仓库信息如下:
1 | D:\sumu_blog\site [master ≡]> git ls-tree HEAD .\source\_posts\docs\ |
可以看到,子模块的版本是ad2854a5528f6ce5841fe8972839b1f289f1d621,我们现在更新以下:
1 | D:\sumu_blog\site [master ≡]> git submodule update --remote |
然后再来看一下父仓库的记录信息:
1 | D:\sumu_blog\site [master ≡ +0 ~1 -0 !]> git ls-tree HEAD .\source\_posts\docs\ |
可以发现当前子模块的版本是8ebaaf3f6dfcd4938b6d835b95a42f8e88d4d183,但是父仓库还是原来的提交记录,当我们执行 git submodule update
时,子模块的版本就会回到之前的。例如:
1 | D:\sumu_blog\site [master ≡ +0 ~1 -0 !]> git submodule update |
2.4 同步配置变更
2.4.1 提交子模块的改动
那么我们怎么更新父仓库的记录信息?我们先回到父仓库看一下状态:
1 | D:\sumu_blog\site [master ≡ +0 ~1 -0 !]> git status |
可以看到子模块有了新的提交,这个时候我们可以执行:
1 | #git add path/to/submodule |
然后父仓库记录的版本就会更新了:
1 | D:\sumu_blog\site [master ≡ +0 ~1 -0 ~]> git commit -m "更新子模块版本" |
2.4.2 新 URL 配置
或者也可以用下面这个命令:
1 | git submodule sync |
更新父仓库的本地配置:将父仓库中
.git/config
文件里的子模块 URL 更新为.gitmodules
文件中的最新 URL。更新子模块的本地配置:递归更新子模块自身目录内的
.git/config
中的远程仓库 URL(通常是origin
)。
例如:
1 | D:\sumu_blog\site [master ↑1]> git submodule |
Tips:
git submodule sync
是 子模块仓库地址变更后的关键修复命令,它可以确保:(1)父仓库的本地配置(
.git/config
)与声明文件(.gitmodules
)一致(2)子模块自身的远程仓库配置同步更新
(3)该命令不会修改子模块的代码版本,仅更新配置。代码更新仍需通过
git submodule update
完成。
3. clone带子模块的仓库
clone主仓库后,子仓库是没有代码的,还要执行以下命令:
1 | git submodule init # 初始化子模块 |
或者直接用下面的命令,一步到位:
1 | git clone --recursive <repo_url> |