Git 中树与模块的使用指南
理解子模块的工作原理
要理解子模块的工作方式,可以从两个角度来看:
1.
创建新集合
:将子模块添加到超级项目并将集合推送到远程仓库的用户视角。
2.
克隆现有集合
:从远程仓库克隆带有子模块的超级项目副本的用户视角。
添加子模块
要将一组新的子模块关联到现有项目,可以使用
submodule add
命令。以下示例将两个子模块添加到现有项目(仓库)中,该项目将作为超级项目。假设已经创建并推送了名为
mod1
和
mod2
的项目,可以使用以下命令将子模块添加到超级项目中:
$ git submodule add <url to mod1> mod1
$ git submodule add <url to mod2> mod2
submodule
命令的
add
操作会执行以下几个步骤:
1.
克隆子模块仓库
:Git 将子模块的仓库克隆到当前目录。
$ git submodule add <remote path for mod1> mod1
Cloning into 'mod1'...
done.
$ git submodule add <remote path for mod2> mod2
Cloning into 'mod2'...
done.
-
检出默认分支
:默认情况下,Git 会检出
master分支。
$ cd mod1
$ git branch
* master
$ cd ../mod2
$ git branch
* master
-
记录克隆路径
:Git 将子模块的克隆路径添加到
.gitmodules文件中。
$ cat .gitmodules
[submodule "mod1"]
path = mod1
url = <remote path for mod1>
[submodule "mod2"]
path = mod2
url = <remote path for mod2>
-
添加文件到索引
:Git 将
.gitmodules文件添加到索引,准备提交。 - 添加提交 ID 到索引 :Git 将子模块的当前提交 ID 添加到索引,准备提交。
$ git status
On branch master
...
new file: .gitmodules
new file: mod1
new file: mod2
完成上述步骤后,子模块的路径会记录在
.gitmodules
文件中,以便在未来克隆主项目时包含这些子模块。要完成
mod1
和
mod2
的添加过程,只需在超级项目目录中完成 Git 工作流,提交并推送
add
命令暂存的子模块相关更改:
$ git commit -m "Add submodules mod1 and mod2"
[master 2745a27] Add submodules mod1 and mod2
3 files changed, 8 insertions(+)
create mode 100644 .gitmodules
create mode 160000 mod1
create mode 160000 mod2
$ git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 400 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To C:/Users/bcl/submod/remote/main.git
941450a..d116ad1 master -> master
这里,我们告知 Git 将另外两个仓库与当前仓库关联起来。Git 部分通过创建
.gitmodules
文件来管理,该文件映射子模块内容应放置的子目录,并存储每个模块的名称和当前提交的 SHA1 值。这些信息随后会推送到远程仓库,以便在未来克隆项目时存储连接信息。需要注意的是,这里不会推送子模块仓库本身的任何更改。
确定子模块状态
git submodule status
命令用于查看与项目关联的各个子模块的状态。该命令会显示子模块当前检出的提交的 SHA1 值和路径。输出还包含一个简单的前缀字符,定义如下:
| 前缀字符 | 含义 |
| ---- | ---- |
|
-
| 子模块未初始化 |
|
+
| 子模块当前检出的版本与包含仓库中的 SHA1 值不同 |
|
U
| 子模块存在合并冲突 |
查看刚刚添加的子模块的当前状态:
$ git submodule status
8add7dab652c856b65770bca867db2bbb39c0d00 mod1 (heads/master)
7c2584f768973e61e8a725877dc317f7d2f74f37 mod2 (heads/master)
第一列包含每个子模块当前检出提交的 SHA1 值,后面是添加子模块时分配的本地名称。为进一步演示这种映射关系,可以进入子模块目录并查看日志:
$ cd mod1
$ git log --oneline
8add7da Add initial content for module mod1.
可以看到,当前提交的 SHA1 值与
status
命令输出中的值匹配。
内部而言,Git 将子模块的信息存储在
.git/modules
目录中。在这个目录下,每个附加到项目的模块都有一个单独的子目录。例如:
$ ls .git/modules/*
.git/modules/mod1:
config HEAD index logs/ packed-refs
description hooks/ info/ objects/ refs/
.git/modules/mod2:
config HEAD index logs/ packed-refs
description hooks/ info/ objects/ refs/
克隆带有子模块的项目
现在,切换到另一个用户的视角,该用户想要克隆带有子模块的项目并进行开发。首先,创建一个单独的目录,然后将带有子模块的项目克隆到该目录中:
$ git clone <remote path>/main.git
Cloning into 'main'...
done.
$ cd main
$ ls -a
./ ../ .git/ .gitmodules file1.txt mod1/ mod2/
看起来已经克隆了超级项目和子模块,但查看子模块目录会发现里面没有内容:
$ ls mod1
$ ls mod2
这是为什么呢?使用
submodule status
命令查看子模块的状态:
$ git submodule status
-8add7dab652c856b65770bca867db2bbb39c0d00 mod1
-7c2584f768973e61e8a725877dc317f7d2f74f37 mod2
第一列的破折号
-
表示子模块未初始化。在这种情况下,未初始化意味着超级项目不知道这些模块。子模块目录存在但未填充内容,更重要的是,子模块位置的信息(来自
.gitmodules
文件)还未添加到超级项目的配置文件中。
submodule init
命令可以解决这个问题:
$ git submodule init
Submodule 'mod1' (<remote path>/mod1.git) registered for path 'mod1'
Submodule 'mod2' (<remote path>/mod2.git) registered for path 'mod2'
运行该命令后,仓库的配置文件中就有了远程信息:
$ git config -l | grep submodule
submodule.mod1.url=<remote path>/mod1.git
submodule.mod2.url=<remote path>/mod2.git
这完成了初始化步骤。但此时查看仓库目录,会发现里面仍然没有内容。实际上,拉取带有子模块的现有项目的子模块是一个两步过程。
init
子命令将子模块注册到超级项目的配置中,以便直接引用它们。接下来,运行
submodule update
子命令将这些仓库克隆到子目录中,并检出包含项目的指定提交:
$ git submodule update
Cloning into 'mod1'...
done.
Submodule path 'mod1': checked out '8add7dab652c856b65770bca867db2bbb39c0d00'
Cloning into 'mod2'...
done.
Submodule path 'mod2': checked out '7c2584f768973e61e8a725877dc317f7d2f74f37'
为什么需要两步呢?将
init
和
update
子命令分开,为用户提供了在克隆子模块之前(即在
update
命令之前)更新
.gitmodules
文件中 URL(路径)的机会。如果不需要这样做,并且想通过一个命令执行这两个操作,可以使用以下快捷方式:
$ git submodule update --init
更方便的是,Git 为
clone
命令提供了
--recursive
选项,该选项包含了
submodule update --init
的功能,进一步简化了操作:
$ git clone --recursive <URL of container project>
Cloning into 'main'...
done.
Submodule 'mod1' (<url to remote mod1>/mod1.git) registered for
path 'mod1'
Submodule 'mod2' ((<url to remote mod2>/mod2.git) registered for
path 'mod2'
Cloning into 'mod1'...
done.
Submodule path 'mod1': checked out
'8add7dab652c856b65770bca867db2bbb39c0d00'
Cloning into 'mod2'...
done.
Submodule path 'mod2': checked out
'7c2584f768973e61e8a725877dc317f7d2f74f37'
(注意,也可以使用
--recurse-submodules
,它与
--recursive
相同。)
这个操作克隆了子模块的仓库,并检出了添加子模块时的当前提交。如果回到
mod1
和
mod2
的原始独立仓库并查看日志,会发现自添加这些仓库作为子模块以来有了一些更新:
$ cd <original separate mod1 path>/mod1; git log --oneline
a76a3fd update info file
8add7da Add initial content for module mod1.
$ cd <original separate mod2 path>/mod2; git log --oneline
cfa214d update 2 to info file
7c2584f update of info file
07f58e0 Add initial content for module mod2.
现在,查看最近克隆的仓库中子模块更新的结果,会发现一些差异:
$ cd mod1
mod1 ((8add7da...))
$ git log --oneline
8add7da Add initial content for module mod1.
$ cd ../mod2
mod2 ((7c2584f...))
$ git log --oneline
7c2584f update of info file
07f58e0 Add initial content for module mod2.
可以看到,这里没有最新的提交,只有添加子模块到刚刚克隆的超级项目时的提交。这是使用子模块时一个独特而重要的区别。包含子模块的项目会保留将仓库作为子模块添加到项目时活动或使用的提交记录。
此外,查看子模块上的活动分支会发现没有活动分支。添加子模块时的当前检出提交是当前活动的分离 HEAD。这并不像听起来那么糟糕,它只是意味着 Git 指向的是一个特定的版本,而不是特定的分支引用:
$ cd mod1; git branch
* (HEAD detached at 8add7da)
master
$ cd ../mod2; git branch
* (HEAD detached at 7c2584f)
master
mod2
的命令提示符可能如下所示:
<local path to mod>/mod2 ((7c2584f...))
这是关于子模块的一个重要点:它们最初与添加到容器项目时选择的提交相关联。然而,每个子模块的仓库仍然是一个独立的 Git 仓库,可以在添加为子模块后进行更新。
由于知道组成子模块的 Git 项目已经有了更新,这就引出了如何更新子模块以获取最新内容的问题。而且,一旦子模块更新到新的提交,还需要考虑如何更新容器项目以确保记录其子模块现在指向的提交。如果有多个子模块,还会面临如何轻松跨多个子模块执行这些操作的问题。接下来先看看如何处理多个子模块。
处理多个子模块
如前所述,使用子模块并非易事。而且,当尝试管理多个子模块时,复杂程度可能会增加。幸运的是,Git 的
submodule
命令包含一个
foreach
子命令,可以简化跨多个子模块执行相同操作的过程。该命令的语法很简单:
git submodule [--quiet] foreach [--recursive] <command>
这里的
<command>
可以是想要对每个子模块运行的任何命令,后面还可以跟该命令特定的额外参数或选项。以
git
命令为例,可以使用此功能查看每个子模块的日志:
$ git submodule foreach git log --oneline
Entering 'mod1'
8add7da Add initial content for module mod1.
Entering 'mod2'
7c2584f update of info file
07f58e0 Add initial content for module mod2.
如果添加
--quiet
选项,则输出中将省略 “Entering ‘
’” 行。
--recursive
选项仅在有嵌套子模块(即子模块下还有子模块)时需要。
Git 还提供了几个填充了信息的变量,可以在构建命令时使用,这些变量如下:
-
$name
:子模块的名称
-
$path
:子模块相对于超级项目的路径
-
$sha1
:超级项目中记录的子模块的当前 SHA1 值
-
$toplevel
:超级项目的绝对路径
例如,可以构建一个简单的命令来显示模块的名称和超级项目知道的当前 SHA1 值:
$ git submodule --quiet foreach 'echo $path $sha1'
mod1 8add7dab652c856b65770bca867db2bbb39c0d00
mod2 7c2584f768973e61e8a725877dc317f7d2f74f37
从远程更新子模块
如果子模块所基于的远程仓库已更新,可以采用以下几种方法更新子模块(这里指的是子模块克隆自的原始项目,而不是超级项目,即进入子模块目录并运行
git remote -v
时显示的远程仓库):
1.
手动切换并更新
:切换到每个子模块,检出一个分支(如果需要),然后进行拉取或获取并合并操作。
$ cd mod1
$ git checkout <branch>
$ git pull
Updating 8add7da..a76a3fd
Fast-forward
mod1.info | 1 +
1 file changed, 1 insertion(+)
-
使用
recurse-submodules选项 :使用git pull的recurse-submodules选项更新子模块的内容。这将更新子模块中的默认远程跟踪分支(通常是origin/master),然后可以进入每个子模块并将远程跟踪分支合并到本地分支(同样,这假设已经检出了一个分支)。
# 在超级项目中
$ git pull --recurse-submodules
Fetching submodule mod1
From <remote path>/mod1
8add7da..a76a3fd master -> origin/master
Fetching submodule mod2
From <remote path>/mod2
7c2584f..cfa214d master -> origin/master
Already up-to-date.
# 在子模块中
$ git merge origin/master
Updating 8add7da..d05eb00
Fast-forward
mod1.info | 2 ++
1 file changed, 2 insertions(+)
-
使用
--remote选项 :使用submodule命令的update子命令并加上--remote选项。
# 在超级项目中
$ git submodule update --remote
Submodule path 'mod1': checked out 'a76a3fd2470d21dcdca8a9671f39be383aae1ea1'
Submodule path 'mod2': checked out 'cfa214db650ef5bcc7287323943d98b46d0a5354'
如果只想更新特定的子模块,只需在命令末尾添加子模块名称:
$ git submodule update --remote mod1
-
使用
foreach子命令 :使用foreach子命令遍历每个子模块并执行更新操作。
# 在超级项目中
$ git submodule foreach git pull origin master
Entering 'mod1'
From <remote path>/mod1
* branch master -> FETCH_HEAD
Already up-to-date.
Entering 'mod2'
From <remote path>/mod2
* branch master -> FETCH_HEAD
Updating 7c2584f..e9b2d79
Fast-forward
mod2.info | 2 ++
1 file changed, 2 insertions(+)
查看子模块差异
将子模块更新到最新推送的内容后,子模块中的内容与超级项目引用的子模块内容之间会存在差异。可以使用
submodule status
命令轻松查看这些差异:
$ git submodule status
+d05eb000ecb6cc1f00bc1b45d3e1cb6fb48e108d mod1 (heads/master)
+e9b2d790cf97ee43dc745d9996e07426e5570242 mod2 (heads/master)
前面的加号
+
表示 “子模块当前检出的版本与包含仓库中的 SHA1 值不同”。也可以使用
diff
命令查看这种差异:
$ git diff
diff --git a/mod1 b/mod1
index 8add7da..d05eb00 160000
--- a/mod1
+++ b/mod1
@@ -1 +1 @@
通过以上方法,可以有效地管理和使用 Git 中的子模块,提高项目开发的效率和可维护性。
Git 中树与模块的使用指南
子模块默认分支设置
子模块的默认分支通常假定为
master
,不过若有需求,能够借助
git config
命令来加以更改。示例如下:
$ git config -f .gitmodules submodule.mod2.branch testbranch
这里的
-f
选项是用来指定文件,也就是
.gitmodules
文件,并且设定键值对
submodule->mod2->branch
。操作完成之后,
.gitmodules
文件的内容如下:
$ cat .gitmodules
[submodule "mod1"]
path = mod1
url = C:/Users/bcl/submod/remote/mod1.git
[submodule "mod2"]
path = mod2
url = C:/Users/bcl/submod/remote/mod2.git
branch = testbranch
子模块操作流程总结
下面通过 mermaid 流程图来总结子模块的主要操作流程:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(创建超级项目):::process
B --> C{是否添加子模块}:::decision
C -->|是| D(使用 git submodule add 添加子模块):::process
D --> E(执行 git commit 和 git push):::process
C -->|否| F(跳过子模块添加):::process
E --> G{是否克隆项目}:::decision
F --> G
G -->|是| H(使用 git clone 克隆项目):::process
H --> I{子模块是否初始化}:::decision
I -->|否| J(使用 git submodule init 初始化子模块):::process
J --> K(使用 git submodule update 更新子模块):::process
I -->|是| K
K --> L{是否更新子模块}:::decision
L -->|是| M(选择更新方法更新子模块):::process
L -->|否| N(结束操作):::process
M --> N
N --> O([结束]):::startend
不同子模块操作场景对比
为了更清晰地了解不同子模块操作的适用场景,下面通过表格进行对比:
| 操作场景 | 操作命令 | 适用情况 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- | ---- |
| 添加子模块 |
git submodule add <url> <name>
| 向超级项目中添加新的子模块 | 操作简单,能快速关联子模块 | 需手动提交和推送更改 |
| 克隆项目及子模块 |
git clone --recursive <URL>
| 一次性克隆超级项目和所有子模块 | 简化操作,自动完成初始化和更新 | 若子模块较多,克隆时间可能较长 |
| 初始化子模块 |
git submodule init
| 克隆项目后子模块未初始化 | 为后续更新子模块做准备 | 需额外执行更新操作 |
| 更新子模块 |
git submodule update --remote
| 远程子模块有更新时 | 自动获取最新提交 | 可能引入不兼容的更改 |
| 批量处理子模块 |
git submodule foreach <command>
| 对多个子模块执行相同操作 | 提高效率,减少重复操作 | 命令复杂时可能出错 |
子模块使用注意事项
在使用子模块的过程中,有一些要点需要特别留意:
1.
提交记录保留
:超级项目会记录子模块添加时的提交 ID,这就意味着子模块后续的更新不会自动反映在超级项目里。所以,在子模块更新之后,要记得在超级项目中提交并推送更改,从而保证记录的准确性。
2.
分支管理
:子模块默认处于分离 HEAD 状态,也就是不指向特定的分支。要是需要在子模块中进行开发,就需要手动检出分支。
3.
嵌套子模块
:如果项目存在嵌套子模块,在克隆和更新的时候要使用
--recursive
或者
--recurse-submodules
选项,以此确保所有子模块都能被正确处理。
4.
远程仓库更新
:当远程子模块仓库有更新时,要及时更新本地子模块,防止使用到过时的代码。
总结
Git 子模块为项目管理提供了强大的功能,能够让我们把多个独立的仓库组合成一个超级项目,从而提升项目的可维护性和开发效率。不过,子模块的操作相对复杂,需要我们深入理解其工作原理和操作流程。
通过本文,我们已经了解了子模块的添加、克隆、状态查看、更新等操作,并且掌握了一些实用的技巧和注意事项。在实际的项目开发中,要依据具体的需求选择合适的操作方法,同时注意子模块的管理和维护,这样才能充分发挥 Git 子模块的优势。
希望本文能帮助你更好地使用 Git 子模块,让你的项目管理更加得心应手。如果你在使用过程中遇到任何问题,欢迎留言讨论。
超级会员免费看

被折叠的 条评论
为什么被折叠?



