Git知识点:子模块完全指南

1,124 阅读7分钟

子模块允许将一个Git仓库作为另一个Git仓库的子目录,同时还保持提交的独立。子模块对应的目录不会直接添加到主仓库中,而是以gitlink形式保存在主仓库中,gitlink是从一个树对象(tree object)到一个提交对象(commit object)的链接,文件模式是160000,而不是040000。

image.png

目录(A subdirectory):040000

普通文件(A normal (not-executable) file):100644

可执行文件(A normal, but executable, file):100755

符号链接(A symlink, the content of the file will be the link target):120000

子模块(A gitlink, SHA-1 of the object refers to a commit in another repository):160000

说明1:主仓库不会跟踪子仓库的内容,而是通过树对象保存子仓库的某个具体的提交(无法通过配置文件修改),所以当子仓库发生修改后,必须在父仓库中更新子模块的引用,将其指向子模块的新提交。

说明2:如果将包含子仓库的主仓库推送到GitHub,子仓库对应的目录会多一个类似@5befc84的后缀,即git submodule status输出的SHA-1哈希值,点击目录会跳转到子仓库。

image.png

相关命令

查看子模块

$ git submodule                    # 默认子命令为status
$ git submodule status [<path>]    # 查看子模块状态
$ git submodule summary [<path>]   # 查看子模块摘要

添加子模块

在根目录下创建.gitmodules文件保存子模块URL地址和目录路径,如果有多个子模块,.gitmodules文件会有多条记录。注意:.gitmodules文件也像.gitignore文件一样需要版本控制。

$ git submodule add <repository> [<path>]                # 默认子模块名、子目录名与仓库同名
$ git submodule add --name <name> <repository> [<path>]  # 指定子模块名

说明:执行git submodule add后不需要再执行git submodule init和git submodule update。

初始化子模块

根据.gitmodules文件和命令行参数更新.git/configure文件,即将.gitmodules文件中子模块设置复制到.git/configure文件中。

$ git submodule init [<path>]

取消子模块

$ git submodule deinit <path>

更新子模块

根据.git/configure文件中地址抓取子仓库的内容。执行git submodule update更新出来的子仓库都以某个提交而不是以某个分支进行检出,此时子仓库处于分离头指针状态,必须切换到某个工作分支才能进行修改和提交。

$ git submodule update [<path>]                       #
$ git submodule update --init [<path>]                # 将git submodule init和git submodule update合并成一步
$ git submodule update --init --recursive [<path>]    # 初始化、抓取并检出任何嵌套的子模块

设置子模块

$ git submodule set-branch <path>            # 设置子模块的远程跟踪分支
$ git submodule set-url <path> <newurl>      # 设置子模块的URL地址

遍历子模块

$ git submodule foreach <command> # 在每个子模块中运行命令

说明:如果项目中包含了大量子模块,foreach子模块命令非常有用。

$ git submodule foreach 'git stash'              # 保存所有子模块的工作进度
$ git submodule foreach 'git checkout master'    # 切换所有子模块到指定分支
$ git submodule foreach 'git diff'               # 查看所有子模块的改动差异

同步子模块

$ git submodule sync [<path>] # 将新的 URL 复制到本地配置中
$ git submodule sync --recursive

克隆子模块

克隆一个含有子模块的项目时,默认仅包含子模块的目录,不会自动克隆子模块的仓库。这一行为是可选择性子模块的基本特性之一,对于只关心项目本身数据,对项目引用的外部项目数据并不关心的用户,这个功能非常好,数据也没有冗余而且克隆的速度也更块。

方式1:
$ git clone <repository> [<directory>]
$ git submodule init [<path>]
$ git submodule update [<path>]
方式2:
$ git clone --recurse-submodules <repository> [<directory>] # 自动初始化并更新每一个子模块,包括可能存在的嵌套子模块

获取子模块

方式1:
$ cd <path>                # 进入子模块目录
$ git fetch                # 抓取
$ git merge origin/master  # 合并上游分支来更新本地代码
# git pull
方式2:
$ git submodule update --remote <path>    # 抓取并更新

相关文件

  • .git/config文件:保存主仓库和子仓库的本地配置,执行git config submodule.<name>.<option>命令编辑该文件。不需要同步。
  • .gitmodules文件:保存所有子仓库的全局配置,例如name、url和path,执行git config -f .gitmodules submodule.<name>.<option>命令编辑该文件。需要同步。
  • .git/modules/<name>目录:保存子仓库的版本信息和元数据。
  • <path>/.git文件:保存子仓库实际仓库路径(.git/modules/<name>),文件内容为gitdir: <path>

说明:Git子模块本质上是在主仓库中保存子仓库的相关信息,包括子仓库的版本号、路径和URL等。

代码示例

准备工作

$ vim prepare.sh
# 创建目录---logs保存日志文件,repos保存远程仓库,workspace保存本地仓库
PROJECTDIR=/c/home/github/project/git
rm -rf $PROJECTDIR/{logs,repos,workspaces}
mkdir -p $PROJECTDIR/{logs,repos,workspaces}
# 允许使用本地文件系统
git config --global protocol.file.allow always
# 创建一个子仓库(libA.git)和一个主仓库(main.git)---远程仓库
git --bare --git-dir=$PROJECTDIR/repos/libA.git init
git --bare --git-dir=$PROJECTDIR/repos/main.git init
# 克隆子仓库(libA.git),添加数据,然后提交并推送---本地仓库
git clone $PROJECTDIR/repos/libA.git $PROJECTDIR/workspaces/libA
cd $PROJECTDIR/workspaces/libA
echo "echo libA version1" > lib.sh
git add --all
git commit -m "add lib.sh"
git push origin master
# 克隆主仓库(main.git),添加数据,然后提交并推送---本地仓库
git clone $PROJECTDIR/repos/main.git $PROJECTDIR/workspaces/main
cd $PROJECTDIR/workspaces/main
echo "echo main" > main.sh
git add --all
git commit -m "add main.sh"
git push origin master

创建主仓库

# 执行准备脚本
$ source prepare.sh
# 主仓库中添加子模块
$ cd $PROJECTDIR/workspaces/main
$ git submodule add --name subA $PROJECTDIR/repos/libA.git libA
# 查看工作区状态,新增.gitmodules和libA两个文件
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   .gitmodules
        new file:   libA
# 查看.gitmodules文件
$ cat .gitmodules
[submodule "subA"]
        path = libA
        url = C:/home/github/project/git/repos/libA.git
# git diff
# 提交并推送
$ echo "source libA/lib.sh" >> main.sh
$ mkdir resource && touch resource/.keep
$ git add --all
$ git commit -m "add libA"
[master d907023] add libA
 4 files changed, 5 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 libA
 create mode 100644 resource/.keep
$ git push origin master

修改子仓库

$ cd $PROJECTDIR/workspaces/main/libA
$ git branch
* master
$ echo "echo libA version2" > lib.sh
$ git add --all
$ git commit -m "update lib.sh"
$ git push origin master

提交主仓库

# 修改子模块,除了子仓库需要提交推送,主仓库也需要提交推送(主仓库引用子仓库的提交ID发生变化)
# 备注:为了避免其他人克隆主仓库时找不到子仓库最新提交而导致出错,首先对子仓库进行提交推送,然后再对主仓库进行提交推送!!!
$ cd $PROJECTDIR/workspaces/main
$ git status -s
 M libA
$ git diff
diff --git a/libA b/libA
index 2ab4ead..7e2b40c 160000
--- a/libA
+++ b/libA
@@ -1 +1 @@
-Subproject commit 2ab4ead3f457fa663bf7aebedc3c3d5dc47113a9
+Subproject commit 7e2b40c000c201824f2d2d43f3247d7938bd20b5 
$ git add --all
$ git commit -m "upgrade libA"
$ git push origin master

克隆主仓库

# 克隆含有子模块的仓库(默认仅包含子模块的目录,不会自动克隆子模块的仓库)
$ git clone $PROJECTDIR/repos/main.git $PROJECTDIR/workspaces/main-clone
$ cd $PROJECTDIR/workspaces/main-clone
# 查看子模块状态,提交ID前面的减号表示子模块未检出,加号表示子模块版本有差异,空格表示子模块正常
$ git submodule status
-7e2b40c000c201824f2d2d43f3247d7938bd20b5 libA
# 初始化子模块,初始化本地配置文件
$ git submodule init
# 更新子模块,抓取子仓库并检出主仓库中引用的提交
$ git submodule update
Cloning into 'libA'...
done.
Submodule path 'libA': checked out '7e2b40c000c201824f2d2d43f3247d7938bd20b5'
$ git submodule status
 7e2b40c000c201824f2d2d43f3247d7938bd20b5 libA (heads/master)
# 执行git submodule update更新子模块都以某个提交进行检出,此时子仓库处于分离头指针状态,必须切换到某个工作分支才能进行修改和提交
$ cd $PROJECTDIR/workspaces/main-clone/libA
$ git branch
* (HEAD detached at 7e2b40c)
  master
$ git checkout master

主仓库的对象关系图

image.png 注意:resource和libA在树对象(07dec0c)中文件模式不一样,resource是040000(目录),libA是160000(gitlink)。

子仓库的对象关系图

image.png

命令实现

脚本文件

Git的git submodule命令实际上是通过git-submodule.sh脚本文件实现的,该脚本提供用于创建、删除、更新、初始化和同步子模块等命令。子模块在v1.5.3版本中引入,通过git-submodule.sh脚本执行许多命令来实现子模块功能。v2.7.0版本中引入submodule--helper底层命令来完成子模块相关操作,git-submodule.sh最终调用submodule--helper命令来实现模块功能(submodule--helper是内置命令,而submodule不是内置命令)。

image.png

image.png

// https://github.com/git/git/blob/master/git.c
static struct cmd_struct commands[] = {
    { "add", cmd_add, RUN_SETUP | NEED_WORK_TREE },
    { "commit", cmd_commit, RUN_SETUP | NEED_WORK_TREE },
    { "submodule--helper", cmd_submodule__helper, RUN_SETUP },
};
// https://github.com/git/git/blob/master/builtin/submodule--helper.c
int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
{
    parse_opt_subcommand_fn *fn = NULL;
    const char *const usage[] = {
        N_("git submodule--helper <command>"),
        NULL
    };
    struct option options[] = {
        OPT_SUBCOMMAND("clone", &fn, module_clone),
        OPT_SUBCOMMAND("add", &fn, module_add),
        OPT_SUBCOMMAND("update", &fn, module_update),
        OPT_SUBCOMMAND("foreach", &fn, module_foreach),
        OPT_SUBCOMMAND("init", &fn, module_init),
        OPT_SUBCOMMAND("status", &fn, module_status),
        OPT_SUBCOMMAND("sync", &fn, module_sync),
        OPT_SUBCOMMAND("deinit", &fn, module_deinit),
        OPT_SUBCOMMAND("summary", &fn, module_summary),
        OPT_SUBCOMMAND("push-check", &fn, push_check),
        OPT_SUBCOMMAND("absorbgitdirs", &fn, absorb_git_dirs),
        OPT_SUBCOMMAND("set-url", &fn, module_set_url),
        OPT_SUBCOMMAND("set-branch", &fn, module_set_branch),
        OPT_SUBCOMMAND("create-branch", &fn, module_create_branch),
        OPT_END()
    };
    argc = parse_options(argc, argv, prefix, options, usage, 0);

    return fn(argc, argv, prefix);
}

执行脚本

(1)Linux下直接运行git-submodule.sh脚本操作子模块

$ export PATH=$PATH:$(git --exec-path)
$ file /usr/lib/git-core/*submodule*
/usr/lib/git-core/git-submodule:         POSIX shell script, ASCII text executable
/usr/lib/git-core/git-submodule--helper: symbolic link to git
$ bash -x git-submodule status

(2)Windows下直接运行git-submodule.sh脚本操作子模块

  • 获取脚本安装位置
$ git --exec-path
C:\Program Files\Git\mingw64/libexec/git-core
  • 配置脚本安装到环境变量

image.png

  • 启动脚本调试模式
$ bash -x git-submodule status

常见问题

判断一个文件夹是否是一个子模块?

方式1:
git submodule status <directory>
方式2:
git config -f .gitmodules --list | grep '<directory>'
方式3:
git ls-files --stage | grep "^16" | grep '<directory>'

内容总结

Git子模块是管理外部仓库依赖关系的最佳工具之一,熟悉常用的子模块命令,以及它的内部实现方式,才能更好地帮助我们管理并跟踪外部仓库的版本变化,使得项目开发更加灵活和高效。

参考资料

git-submodule

gitsubmodules

gitmodules

git-config

《Pro Git》7.11 Git 工具 - 子模块

《Git权威指南》4.4. 子模组协同模型

git-submodule实现

《Git版本控制管理(第2版)》16.4 原生解决方案:gitlink和git submodule

《Git高手之路》9.1.4 子模块解决方案——版本库嵌套