Cao Yi

第十二章 子模块(Submodule)

⇦上一章 - 首页🏠 - 下一章⇨



按:子模块并不是必须的,如果项目比较简单,完全没必要使用子模块。本章内容可以在需要时再看。

一般一个项目的代码用一个仓库管理比较方便,而不同的项目之间有时会有一些代码依赖关系,此时我们可以用子模块的方式来建立这种关系——类似 Java 项目中的 maven 依赖。

子模块是一个独立的仓库,它的版本管理由它自己。一个仓库 A 将另外一个仓库 B 用作子模块,则 A 持有 B 的 repo URL 以及用作子模块的 B 的版本号(commit hash)。

1. 实验准备

1.1 Remote Server

IP: 192.168.0.102

Create two git repositories:

$ pwd
/home/caoyi/sandbox/test
$ git init --bare project-main.git
$ git init --bare project-sub.git

1.2 Local Host

远程的仓库创建好后,再将它们 clone 到本地。

1.2.1 Clone Repo

$ pwd
/d/sandbox/temp
$ git clone ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-main.git
$ git clone ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-sub.git

1.2.2 Prepare Data

main repo

$ pwd
/d/sandbox/temp/project-main
$ echo "hello from main repo" > hello.txt
$ git add .
$ git commit -m "first commit in main repo"
$ git push origin main

sub repo:

$ pwd
/d/sandbox/temp/project-sub
$ echo "hello from the sub repo" > hello-sub.txt
$ git add .
$ git commit -m "first commit in the sub repo"
$ git push origin main

2. 添加子模块

$ pwd
/d/sandbox/temp/project-main
$ git submodule add ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-sub.git

(本例只添加了一个子模块,实际开发中,可以根据需要添加若干。添加的模块默认以仓库的名创建一个新的目录,如果要使用其他名称,可以将自定义名称跟在上面的指令最后。)

通过 git status 发现多了两个文件

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   .gitmodules
        new file:   project-sub

.gitmodules 里记录了子模块的信息

$ cat .gitmodules
[submodule "project-sub"]
        path = project-sub
        url = ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-sub.git

project-sub 就是引入的子模块的目录,主库只会把这个目录名当作一个文件加入版本管理的信息中,而不会去管理目录内部的内容,那是子模块自己去管理的,因此,即使使用 git add 也不会把子模块目录里的内容添加到主库的 Staging Area 中,这样的设计很合理,否则就不需要 submodule 了。主库和子模块的版本管理是相对独立的。

子模块入库:

$ git commit -m "add submodule"
[main 99d7ec7] add submodule
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 project-sub
$ git push origin main

查看子模块的状态

$ git submodule status
-bf676211dc70759919951a9b8974e57b816d3568 project-sub

3. Clone 带有子模块的仓库

如果直接 clone,不带参数,得到的仓库,是不会有子模块的,需要继续执行 submodule init + update 才行,即:

$ git clone ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-main.git
$ git submodule init
$ git submodule update

initupdate 两句还可以合写为 git submodule update --init, 如果考虑模块嵌套,还可以写作 git submodule update --init --recursive.

也可以使用 --recurse-submodules or --recursive 一步到位:

$ git clone --recurse-submodules ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-main.git

or

$ git clone --recursive ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-main.git

4. 更新子模块

子模块是独立的仓库,主库不能管理它的版本,它也不知它是哪些库的子模块。这也符合「最小知识原则」,主仓库仅仅持有子模块的「引用」——模块名、仓库地址、版本号(commit hash)。

当我们通过主库进入到子模块中并做了修改时,这些修改只能通过子模块的仓库去处理,然后主库更新引用。

我们现在修改子模块的一点内容:

$ pwd
/d/sandbox/temp/project-main/project-sub
$ echo "hello from sub in main" >> hello-sub.txt
$ git status
On branch main
Your branch is up to date with 'origin/main'.

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:   hello-sub.txt

然后退到主库用 git status 查看状态

$ pwd
/d/sandbox/temp/project-main
$ git status
On branch main
Your branch is up to date with 'origin/main'.

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)
  (commit or discard the untracked or modified content in submodules)
        modified:   project-sub (modified content)

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

屏幕提示:主库无任何变化,但子模块有变化,不过需要进入子模块去处理。(这里本应该用 git diff 查看下具体改动内容,但做实验时忘了,不过没关系,继续往下走,后边的 git show 也会看到改了哪些内容。) OK,我们现在去子模块里处理一下:

$ pwd
/d/sandbox/temp/project-main/project-sub
$ git add .
$ git commit -m "hello from sub module in main/sub"
$ git push origin main

退到主库用 git status 查看状态:

$ pwd
/d/sandbox/temp/project-main
$ git status
On branch main
Your branch is up to date with 'origin/main'.

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:   project-sub (new commits)

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

屏幕提示:主库无任何变化,但子模块有新的 commit. 假若我们的主模块需要使用这个更新后的子模块,我们按如下操作:

$ pwd
/d/sandbox/temp/project-main
$ git add .
$ git commit -m "update submodule"
$ git push origin main

可用 git show 看看刚才的到底修改了什么

$ git show
commit 93e78e5971682ec1765cde2a24732ab565538892 (HEAD -> main, origin/main)
Author: Cao Yi <iridiumcao@gmail.com>
Date:   Thu Dec 21 21:03:23 2023 +0800

    update submodule

diff --git a/project-sub b/project-sub
index bf67621..950e20e 160000
--- a/project-sub
+++ b/project-sub
@@ -1 +1 @@
-Subproject commit bf676211dc70759919951a9b8974e57b816d3568
+Subproject commit 950e20e33528800ca4aa1255446c6f51b1019f97

可见主库里那个和 submodule 文件夹同名的文件里记录的是子模块的某个 commit hash,即当前主库使用的子模块的版本号。这些内容都在主库的 .git/modules/ 中存放。

如果子模块的远程有更新,可以先到子模块中更新,然后主库考虑是否使用这个最新的版本。

可以在主库使用 --recurse-submodules 一步到位,把子模块也更新了:

git pull origin  --recurse-submodules

这里更新子模块,不一定是把子模块更新到它本身的最新版本,而是更新到主库持有的 commit hash 对应的版本。

5. 删除子模块

删除子模块需要先从主库中移除相关引用信息,然后再物理删除子模块的目录。

$ pwd
/d/sandbox/temp/project-main
$ git submodule deinit project-sub
$ git rm project-sub

查看一下状态

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   .gitmodules
        deleted:    project-sub

提交并推送

$ git commit -m "remove submodule"
$ git push origin main

6. 「删除后再加」问题处理

删除一个子模块,提交,再添加,会出现下面的报错:

$ git submodule add ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-sub.git
fatal: A git directory for 'project-sub' is found locally with remote(s):
  origin        ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-sub.git
If you want to reuse this local git directory instead of cloning again from
  ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-sub.git
use the '--force' option. If the local git directory is not the correct repo
or you are unsure what this means choose another name with the '--name' option.

按屏幕提示加 --force 改成这样就好了:

$ git submodule add --force ssh://caoyi@192.168.0.102/home/caoyi/sandbox/test/project-sub.git
Reactivating local git directory for submodule 'project-sub'

注意 --force 的位置,不能放在指令最后。


参考:


⇦上一章 - 首页🏠 - 下一章⇨