LV01-Git-06-子模块

本文主要Git子模块相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
点击查看本文参考资料
点击查看相关文件下载
--- ---

一、子模块

1. 出现的问题 

有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者独立开发的,用于多个父项目的库。 现在问题来了:想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。

就比如,我的笔记博客网站,它是基于hexo的,它不仅包含hexo的架构文件,还包含主题,还包含我的笔记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── ...
├── source
│   ├── 404.md
│   ├── about
│   ├── categories
│   ├── _data
│   ├── docs
│   ├── images
│   └── _posts # 笔记所在目录
│   ├── hello-world.md
│   └── hexo-theme-next
└── themes # 主题所在目录
└── next

那么这个仓库的提交记录中就会包含笔记的更新和站点的更新,但是这两者是没什么关系的,当提交记录多起来之后,我想要找站点提交记录,但是笔记更新了好几百次,这找起来就很麻烦。那么,我是不是可以换一个单独的仓库来作为笔记的仓库,在生成静态网页的时候把笔记拷贝过来?当然可以了,只需要保证hexo生成静态网页的时候,笔记存在于对应的位置即可。

2. Git的解决办法

Git 通过子模块来解决这个问题。 子模块允许我们将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让我们将另一个仓库克隆到自己的项目中,同时还保持提交的独立。

二、基本用法

1. 项目结构

我们现在要创建这样一个结构的项目:

1
2
3
projext/
├── module_a
└── readme.md

其中 module_a 需要是一个单独的版本库。我们接下来就来了解一下子模块怎么使用。

2. 创建版本库

2.1 project

我们创建project版本库,并提交readme.md文件

1
2
3
4
5
6
git init project
cd project
echo "This is project!" > readme.md

git add .
git commit -m "add readme.md"

2.2 module_a

1
2
3
4
5
6
git init module_a
cd module_a
echo "This is module_a!" > readme.md

git add .
git commit -m "add module_a readme.md"

2.3 目录结构

现在目录结构如下:

1
2
3
4
5
6
7
8
sumu@sumu-virtual-machine:~/submodule$ tree
.
├── module_a
│ └── readme.md
└── project
└── readme.md

2 directories, 2 files

现在两个仓库互不干扰。

3. 引入子模块

3.1 怎引入子模块?

我们现在在project中引入子模块module_a:

1
2
cd project
git submodule add ../module_a submodule_a

然后就报错了:

1
2
3
4
sumu@sumu-virtual-machine:~/submodule/project$ git submodule add ../module_a submodule_a
正克隆到 '/home/sumu/submodule/project/submodule_a'...
fatal: 传输 'file' 不允许
fatal: 无法克隆 '/home/sumu/submodule/module_a' 到子模组路径 '/home/sumu/submodule/project/submodule_a'

这是因为 Git 默认不允许使用本地文件传输。

1
git config --global protocol.file.allow always

然后重新添加就可以了,添加后我们可以看一下状态:

1
2
3
4
5
6
7
8
9
sumu@sumu-virtual-machine:~/submodule/project$ git submodule add ../module_a submodule_a
正克隆到 '/home/sumu/submodule/project/submodule_a'...
完成。
sumu@sumu-virtual-machine:~/submodule/project$ git status
位于分支 master
要提交的变更:
(使用 "git restore --staged <文件>..." 以取消暂存)
新文件: .gitmodules
新文件: submodule_a

3.2 有什么变化?

首先应当注意到新的 .gitmodules 文件。 该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射:

1
2
3
[submodule "submodule_a"]
path = submodule_a
url = ../module_a

如果有多个子模块,该文件中就会有多条记录。 要重点注意的是,该文件也像 .gitignore 文件一样受到(通过)版本控制。 它会和该项目的其他部分一同被拉取推送。 这就是克隆该项目的人知道去哪获得子模块的原因。

git status 输出中列出的另一个是项目文件夹记录。 如果你运行 git diff,会看到类似下面的信息:

1
2
3
4
5
6
7
8
sumu@sumu-virtual-machine:~/submodule/project$ git diff --cached submodule_a/
diff --git a/submodule_a b/submodule_a
new file mode 160000
index 0000000..6df6c91
--- /dev/null
+++ b/submodule_a
@@ -0,0 +1 @@
+Subproject commit 6df6c91c360cc2091d4b48b1b52f6f4c95dd7c1a

虽然 submodule_a 是工作目录中的一个子目录,但 Git 还是会将它视作一个子模块。当不在那个目录中时,Git 并不会跟踪它的内容, 而是将它看作子模块仓库中的某个具体的提交。

如果想看到更漂亮的差异输出,可以给 git diff 传递 --submodule 选项:

1
git diff --cached --submodule

会有如下输出:

1
2
3
4
5
6
7
8
9
10
11
sumu@sumu-virtual-machine:~/submodule/project$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..268bbe2
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "submodule_a"]
+ path = submodule_a
+ url = ../module_a
Submodule submodule_a 0000000...6df6c91 (new submodule)

3.3 提交子模块的引入

当提交时,会看到类似下面的信息:

1
2
3
4
5
6
sumu@sumu-virtual-machine:~/submodule/project$ git add .
sumu@sumu-virtual-machine:~/submodule/project$ git commit -m "引入子模块"
[master 0ab9af8] 引入子模块
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 submodule_a

4. 修改子模块

4.1 在子模块修改会影响父项目吗

4.1.1 实践一下

修改子模块之后只对子模块的版本库产生影响,对父项目的版本库不会产生任何影响,我们修改子模块的内容:

1
2
cd module_a
echo "modify module_a 1!" >> readme.md

我们可以去父项目查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
sumu@sumu-virtual-machine:~/submodule/module_a$ git status 
位于分支 master
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: readme.md

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")
sumu@sumu-virtual-machine:~/submodule/module_a$ cd ../project/
sumu@sumu-virtual-machine:~/submodule/project$ git status
位于分支 master
无文件要提交,干净的工作区

可以发现是父项目是没有做任何修改的。我们先撤销修改:

1
git checkout -- .

4.1.2 结论

子模块是独立的,怎么修改都不会影响父项目。

4.2 在父项目修改子模块?

4.2.1 实践一下

例如,我们在project中修改子模块的文件:

1
2
cd project
echo "modify module_a (project/submodule_a)!" >> submodule_a/readme.md

然后我们看一下状态:

1
2
3
4
5
6
7
8
9
10
sumu@sumu-virtual-machine:~/submodule/project$ echo "modify module_a (project/submodule_a)!" >> submodule_a/readme.md
sumu@sumu-virtual-machine:~/submodule/project$ git status
位于分支 master
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
(提交或丢弃子模组中未跟踪或修改的内容)
修改: submodule_a (修改的内容)

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")

我们尝试提交一下:

1
2
3
4
5
6
7
8
9
10
sumu@sumu-virtual-machine:~/submodule/project$ git add .
sumu@sumu-virtual-machine:~/submodule/project$ git commit -m "父项目修改子模块"
位于分支 master
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
(提交或丢弃子模组中未跟踪或修改的内容)
修改: submodule_a (修改的内容)

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")

会发现根本没有办法修改。那我们进入project/submodule_a再试一下:

1
2
3
4
5
6
7
8
9
10
11
12
sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ git status 
头指针分离于 6df6c91
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: readme.md

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")
sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ git add .
sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ git commit -m "父项目修改子模块"
[分离头指针 0765ed8] 父项目修改子模块
1 file changed, 1 insertion(+)

发现这个时候是可以的。那么这个时候我们查看一下两个目录的提交记录:

1
2
3
4
5
6
7
8
sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ git mylog 
* [sumu] 0765ed8 : 父项目修改子模块 (HEAD) (2025-06-02 13:24:44)
* [sumu] 6df6c91 : add module_a readme.md (origin/master, origin/HEAD, master) (2025-06-02 12:56:42)

sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ cd ..
sumu@sumu-virtual-machine:~/submodule/project$ git mylog
* [sumu] 0ab9af8 : 引入子模块 (HEAD -> master) (2025-06-02 13:09:43)
* [sumu] f4ac034 : add readme.md (2025-06-02 12:54:04)

会发现,我们在 project/submodule_a中的提交记录只能在子模块目录下看到,我们回到父项目目录,提交记录就没有发生变化。

4.2.2 结论

(1)在父项目中,可以修改子模块,但是无法在父项目中对子模块的修改进行提交和记录。

(2)从父项目进入子模块的目录可以正常提交子模块的修改,但是提交记录只会出现在父项目下的子模块目录,而不会出现在父项目的提交中。

5. 更新子模块

通过上面的了解,我们知道子模块修改后是独立的,那么父项目怎么获取最新的子模块?可以在父项目目录下执行以下命令:

1
git submodule update

我们上面在父项目中对子模块修改了,但是子模块自己的目录并没有做任何改变,我们来更新一下:

1
2
3
4
5
6
7
8
9
10
11
sumu@sumu-virtual-machine:~/submodule/project$ git submodule update
子模组路径 'submodule_a':检出 '6df6c91c360cc2091d4b48b1b52f6f4c95dd7c1a'

sumu@sumu-virtual-machine:~/submodule/project$ git mylog
* [sumu] 0ab9af8 : 引入子模块 (HEAD -> master) (2025-06-02 13:09:43)
* [sumu] f4ac034 : add readme.md (2025-06-02 12:54:04)


sumu@sumu-virtual-machine:~/submodule/project$ cd submodule_a/
sumu@sumu-virtual-machine:~/submodule/project/submodule_a$ git mylog
* [sumu] 6df6c91 : add module_a readme.md (HEAD, origin/master, origin/HEAD, master) (2025-06-02 12:56:42)

从这里就可以看出,这个命令强制更新子模块的源目录的内容并且覆盖了父项目中的子模块。

6. 删除子模块

删除子模块比较麻烦,需要手动删除相关的文件,否则在添加子模块时有可能出现错误。

  • (1)删除子模块目录
1
2
3
cd project
git rm --cached submodule_a
rm -rf submodule_a
  • (2)删除 .gitmodules 文件中相关子模块的信息,类似以下信息:
1
2
3
[submodule "submodule_a"]
path = submodule_a
url = ../module_a
  • (3)删除 .git/config 中相关子模块信息,类似于:
1
2
3
[submodule "submodule_a"]
url = /home/sumu/submodule/module_a
active = true
  • (4)删除 .git 文件夹中的相关子模块文件
1
rm -rf .git/modules/submodule_a

7. 查看子模块

1
git submodule

三、远程仓库

这一次就以我的博客站点为例了。两个仓库(后来都转成私有仓库了)的链接如下:

这部分我就是在windows下进行的了。

1. 添加子模块

添加远程模块的时候格式是这样的:

1
git submodule add <url> <repo_name>

在这里就是:

1
2
cd hexo-site
git submodule add git@github.com:sumumm/site-docs.git source/_posts/docs

然后就会看到如下提示:

1
2
3
4
5
6
7
D:\sumu_blog\site [master ≡]> git submodule add git@github.com:sumumm/site-docs.git source/_posts/docs
Cloning into 'D:/sumu_blog/site/source/_posts/docs'...
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 0), reused 4 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (4/4), done.

就会发现在 hexo-site/source/_posts/docs出现了我们的测试笔记:

1
2
3
4
5
D:\sumu_blog\site [master ≡ +2 ~0 -0 ~]> ls .\source\_posts\docs\01-linux开发\
目录: D:\sumu_blog\site\source\_posts\docs\01-linux开发
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2025/6/2 13:57 234 linux开发-README.md

这个时候我们就是可以正常执行下面的命令来生成静态网页:

1
2
hexo g
hexo s

2. 更新子模块

2.1 怎么更新远程最新版本?

这时候就不能单纯的用git submodule update,更新远程项目的最新版本是以下命令:

1
git submodule update --remote

然后会有如下输出:

1
2
3
4
5
6
7
8
9
D:\sumu_blog\site [master ≡ +2 ~0 -0 ~]> git submodule update --remote
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6 (delta 0), reused 6 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (6/6), 777 bytes | 48.00 KiB/s, done.
From github.com:sumumm/site-docs
e20f97c..6d34f62 master -> origin/master
Submodule path 'source/_posts/docs': checked out '6d34f6280a17c4dbe3049426dd2986baa680ce79'

这个会强制更新到和子模块远程完全相同的版本。

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
2
D:\sumu_blog\site [master ≡]> git ls-tree HEAD .\source\_posts\docs\
160000 commit ad2854a5528f6ce5841fe8972839b1f289f1d621 source/_posts/docs

当执行 git submodule update 时,读取父仓库当前提交中子模块路径对应的 提交哈希值,进入子模块目录,执行 git checkout <commit-hash>,将子模块切换到该哈希值对应的版本(分离头指针状态)。若子模块未初始化/克隆,先自动执行 git submodule init 克隆仓库。

所以其实子模块不会自动更新到最新提交,而是严格使用父仓库记录的旧版本。这样应该是为了在父仓库提交后,让其他协作者运行 git submodule update 会获得完全相同的子模块版本,确保一致性。更新的时候检出父仓库记录的提交(不联网更新)。当我们执行了git submodule update --remote,就会强制联网拉取子模块远程仓库的 最新提交(可以在 .gitmodules 中配置 branch 属性)。

但是,这个时候父仓库的记录信息并不会更新,例如,我这里有一个带子模块的仓库信息如下:

1
2
3
4
5
D:\sumu_blog\site [master ≡]> git ls-tree HEAD .\source\_posts\docs\
160000 commit ad2854a5528f6ce5841fe8972839b1f289f1d621 source/_posts/docs

D:\sumu_blog\site [master ≡]> git submodule
ad2854a5528f6ce5841fe8972839b1f289f1d621 source/_posts/docs (remotes/origin/HEAD)

可以看到,子模块的版本是ad2854a5528f6ce5841fe8972839b1f289f1d621,我们现在更新以下:

1
2
3
4
5
6
7
8
9
D:\sumu_blog\site [master ≡]> git submodule update --remote
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 5 (delta 1), reused 5 (delta 1), pack-reused 0 (from 0)
Unpacking objects: 100% (5/5), 636 bytes | 35.00 KiB/s, done.
From github.com:sumumm/site-docs
ad2854a..8ebaaf3 master -> origin/master
Submodule path 'source/_posts/docs': checked out '8ebaaf3f6dfcd4938b6d835b95a42f8e88d4d183'

然后再来看一下父仓库的记录信息:

1
2
3
4
5
D:\sumu_blog\site [master ≡ +0 ~1 -0 !]> git ls-tree HEAD .\source\_posts\docs\
160000 commit ad2854a5528f6ce5841fe8972839b1f289f1d621 source/_posts/docs

D:\sumu_blog\site [master ≡ +0 ~1 -0 !]> git submodule
+8ebaaf3f6dfcd4938b6d835b95a42f8e88d4d183 source/_posts/docs (remotes/origin/HEAD)

可以发现当前子模块的版本是8ebaaf3f6dfcd4938b6d835b95a42f8e88d4d183,但是父仓库还是原来的提交记录,当我们执行 git submodule update 时,子模块的版本就会回到之前的。例如:

1
2
3
4
5
6
7
8
D:\sumu_blog\site [master ≡ +0 ~1 -0 !]> git submodule update
Submodule path 'source/_posts/docs': checked out 'ad2854a5528f6ce5841fe8972839b1f289f1d621'

D:\sumu_blog\site [master ≡]> git ls-tree HEAD .\source\_posts\docs\
160000 commit ad2854a5528f6ce5841fe8972839b1f289f1d621 source/_posts/docs

D:\sumu_blog\site [master ≡]> git submodule
ad2854a5528f6ce5841fe8972839b1f289f1d621 source/_posts/docs (heads/master-6-gad2854a)

2.4 同步配置变更

2.4.1 提交子模块的改动

那么我们怎么更新父仓库的记录信息?我们先回到父仓库看一下状态:

1
2
3
4
5
6
7
8
9
10
D:\sumu_blog\site [master ≡ +0 ~1 -0 !]> git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: source/_posts/docs (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

可以看到子模块有了新的提交,这个时候我们可以执行:

1
2
3
#git add path/to/submodule
git add source/_posts/docs
git commit -m "更新子模块版本"

然后父仓库记录的版本就会更新了:

1
2
3
4
5
6
D:\sumu_blog\site [master ≡ +0 ~1 -0 ~]> git commit -m "更新子模块版本"
[master 4699ae0] 更新子模块版本
1 file changed, 1 insertion(+), 1 deletion(-)

D:\sumu_blog\site [master1]> git ls-tree HEAD .\source\_posts\docs\
160000 commit 8ebaaf3f6dfcd4938b6d835b95a42f8e88d4d183 source/_posts/docs

2.4.2 新 URL 配置

或者也可以用下面这个命令:

1
2
git submodule sync
git submodule sync --recursive # --recursive 参数处理嵌套子模块
  • 更新父仓库的本地配置:将父仓库中 .git/config 文件里的子模块 URL 更新为 .gitmodules 文件中的最新 URL。

  • 更新子模块的本地配置:递归更新子模块自身目录内的 .git/config 中的远程仓库 URL(通常是 origin)。

例如:

1
2
3
4
5
6
7
8
9
10
11
D:\sumu_blog\site [master ↑1]> git submodule     
e20f97cf1881ac8df94479241051930426563c9b source/_posts/docs (heads/master)

D:\sumu_blog\site [master ↑1]> git submodule update --remote
Submodule path 'source/_posts/docs': checked out 'ee6209b3d6071b5fd06905c2a79c012fe1d1f099'

D:\sumu_blog\site [master ↑1 +0 ~1 -0 !]> git submodule sync
Synchronizing submodule url for 'source/_posts/docs'

D:\sumu_blog\site [master ↑1 +0 ~1 -0 !]> git submodule
+ee6209b3d6071b5fd06905c2a79c012fe1d1f099 source/_posts/docs (remotes/origin/HEAD)

Tips:

git submodule sync子模块仓库地址变更后的关键修复命令,它可以确保:

(1)父仓库的本地配置(.git/config)与声明文件(.gitmodules)一致

(2)子模块自身的远程仓库配置同步更新

(3)该命令不会修改子模块的代码版本,仅更新配置。代码更新仍需通过 git submodule update 完成。

3. clone带子模块的仓库

clone主仓库后,子仓库是没有代码的,还要执行以下命令:

1
2
git submodule init   # 初始化子模块
git submodule update # 更新子模块代码

或者直接用下面的命令,一步到位:

1
git clone --recursive <repo_url>