Git——Submodule管理


整体说明

  • submodule 允许 git 灵活地将其他项目嵌入到当前项目,同时自由地切换 sumodule 分支和内容管理,且保持各自的版本独立
  • 本质上: submodule 还是作为一个独立的项目存在的,主项目管理子模块靠 .gitmodules 文件
  • 子模块默认跟踪的是固定 commit ID ,而非分支:
    • 若子仓库更新,主仓库需手动更新子模块的 commit ID 并提交;
  • .gitmodules 是子项目的核心文件
    • 这个文件必须纳入版本控制,否则他人克隆仓库时无法识别子模块;
  • 避免嵌套过深的子模块,否则会增加版本管理和协作的复杂度

添加子模块

  • 基础语法

    1
    2
    3
    git submodule add <子仓库URL> <本地存放路径(可选)>
    # 上述命令会自动添加 submodule 相关的必要文件,立刻直接 commit 即可添加 submodule 完成,注意这里如果先执行 checkout 等可能导致 submodule 信息无法对齐,建议立即 commit
    git commit "Add: submodule xxx"
    • <子仓库URL>:子模块的 Git 仓库地址(HTTPS/SSH 均可)
    • <本地存放路径>:子模块在当前仓库中的存放目录(省略则默认用仓库名)
  • 若需让子模块跟踪分支,可添加 -b <分支名> 参数:

    1
    git submodule add -b <branch> <子仓库URL> <路径>
  • submodule 的分支可以切换绑定(或新增绑定)

    1
    2
    3
    4
    git config -f .gitmodules submodule.<本地存放路径>.branch <branch>

    # 比如将 dev 分支绑定到名为 libs/sub-repo 的 submodule 下:
    git config -f .gitmodules submodule.libs/sub-repo.branch dev

示例

  • 示例:比如将 https://github.com/example/sub-repo.git 添加到当前仓库的 libs/sub-repo 目录:

    1
    2
    3
    4
    # 进入主仓库根目录
    cd /path/to/your/main-repo
    # 添加子模块
    git submodule add https://github.com/example/sub-repo.git libs/sub-repo
  • 执行上述命令后,Git 会自动完成以下操作:

    • 1)在指定路径(如 libs/sub-repo)拉取子仓库的代码;

      • 就像是普通的 git 项目一样,还包括 .git 目录
    • 2)在主仓库根目录生成 .gitmodules 文件(记录子模块配置),内容示例:

      1
      2
      3
      [submodule "libs/sub-repo"]
      path = libs/sub-repo
      url = https://github.com/example/sub-repo.git
    • 3)主仓库的暂存区会新增 .gitmoduleslibs/sub-repo 两个条目(子模块条目是一个“链接”,记录子仓库的 commit ID)

添加后续提交子模块到主仓库

  • 添加子模块后,需要将 .gitmodules 和子模块条目提交到主仓库:
    1
    2
    3
    4
    # 提交变更
    git commit -m "添加子模块 sub-repo 到 libs 目录"
    # 推送到远程
    git push

克隆包含子模块的仓库

  • 直接 git clone 只会拉取子模块目录,但不会拉取子模块的代码,需执行:
    1
    2
    3
    4
    5
    6
    7
    8
    # 方式1:克隆时直接拉取子模块
    git clone --recurse-submodules <主仓库URL>

    # 方式2:先克隆主仓库,再初始化+更新子模块
    git clone <主仓库URL>
    cd main-repo
    git submodule init # 初始化子模块配置(读取.gitmodules)
    git submodule update # 拉取子模块的代码

git submodule update vs git submodule update --remote

  • git submodule update 命令还有个特殊参数 --remote,两者有很多容易犯错的区别
  • git submodule update
    • 用于 对齐主项目固定的子模块版本
    • 仅将子模块切换到主项目记录的哈希值(即 .gitmodules/.gitmodules 中固定的子模块版本);
    • 不会主动从子模块的远程仓库拉取新代码;
    • 若子模块本地无该哈希值的代码,会从远程克隆,但仅克隆该版本
    • 同步主项目指定的子模块版本(比如团队协作时,确保所有人用相同版本的子模块)
    • 执行 git submodule update 后:
      • 子模块处于「分离头指针(detached HEAD)」状态,且版本与主项目记录完全一致;
  • git submodule update --remote
    • 用于 更新子模块到远程最新版本
    • 先从子模块的远程仓库拉取最新代码(更新子模块的远程追踪分支);
    • 再将子模块切换到该远程分支的最新哈希值
    • 会修改主项目中记录的子模块版本(需手动提交主项目的修改)
    • 主动更新子模块到远程最新版本(比如子模块有新功能/修复,需要同步到主项目)
    • 执行 git submodule update --remote 后:
      • 子模块仍处于「分离头指针」状态(仅指向远程最新哈希),主项目的 git 状态会显示子模块版本已修改
      • 需执行 git add <子模块路径> && git commit 才能将新的子模块版本记录到主项目
    • 慎用这个命令

git submodule update vs git submodule update --remote 示例

  • 假设主项目 main-proj 包含子模块 sub-proj

  • 同步主项目指定的子模块版本:

    1
    git submodule update
  • 更新子模块到远程最新版本,并提交主项目的版本修改:

    1
    2
    3
    4
    git submodule update --remote sub-proj  # 拉取sub-proj远程最新代码,切到最新哈希
    # 提交主项目的修改
    git add sub-proj
    git commit -m "update sub-proj to latest remote version"

附录:初始化带有 submodule 的仓库详细理解

  • 正常拉取外层项目

    1
    git pull origin master:master
    • 此时除了 ./.gitmodules 文件包含关于子模块的信息外,其他的文件都不包含,包括 ./.git/
  • 初始化子模块

    1
    git submodule init
    • .gitmodules 中的所有子模块注册到外层项目中
    • 注册方式:添加子模块信息(文件夹路径和子模块项目地址)到 .git/config 文件中并指明子模块对应的 active = true
    • 注:如果子模块之前存在于 .git/config 中 且 active = false,这个初始化操作会修改为 active = true
  • 初始化指定子模块(其他子模块可以不初始化,也不会影响,未初始化的子模块会是一个空文件夹)

    1
    git submodule init <path_to_sub_module_name>
    • 仅初始化 <path_to_sub_module_name> 这个模块
      • 测试发现:注意初始化时 <path_to_sub_module_name> 是 submodule 的文件夹路径
      • 可以是相对路径或绝对路径,执行这个命令时需要在 submodule 的外面
  • 更新子模块

    1
    2
    3
    4
    # 更新所有子模块('.git/config' 和 '.gitmodules' 中的所有子模块)
    git submodule update
    # 更新单个路径下对应的模块
    git submodule update <path_to_sub_module_name>
    • 具体含义:根据主仓库中记录的 子模块 commit ID ,从子模块的远程仓库拉取对应版本的代码,并存放到主仓库指定的子模块路径中
    • 这行代码执行下面的操作:
      • 如果还没有下载,则所有子模块的链接地址项目下载到 .git/modules/
      • 将对应的 commit ID checkout 到 submodule 文件夹(工作目录)中
      • 常常用来在切换分支后同步子模块数据
    • <path_to_sub_module_name> 模块参数的使用方法同上
  • 特别注意:

    • git submodule init 后,.git/config.gitmodules 应该是一致的
    • .git/config.gitmodules 中都有,且在 .git/configactive = true 的 submodule 才能被 update 操作下载和 checkout
  • 特别说明:解耦初始化 deinit

    1
    2
    3
    4
    5
    # 将 <path_to_sub_module_name> 初始化,后续执行 git submodule update 等命令时自动更新 <path_to_sub_module_name> 这个 submodule 
    git submodule init <path_to_sub_module_name>

    # 将 <path_to_sub_module_name> 解耦初始化,后续执行 git submodule update 等命令时不会再自动更新 <path_to_sub_module_name> 这个 submodule
    git submodule deinit <path_to_sub_module_name>

更新子模块到自己的最新 commit

  • 更新逻辑:
    1
    2
    3
    4
    5
    6
    7
    # 进入子模块目录
    cd libs/sub-repo
    git pull origin master # 拉取子仓库最新代码
    cd ../.. # 回到主仓库
    git add libs/sub-repo # 提交子模块的新 commit ID
    git commit -m "update: 子模块 sub-repo 同步到最新版本"
    git push

删除子模块

  • 若需移除子模块,步骤稍多(Git 无直接 git submodule delete 命令):
    1
    2
    3
    4
    5
    6
    7
    8
    # 1. 解除子模块关联
    git submodule deinit -f libs/sub-repo
    # 2. 删除 .git 中的子模块缓存
    rm -rf .git/modules/libs/sub-repo
    # 3. 删除工作区的子模块目录,这一步后会看到 .gitmodules 中的相关 submodule 也被删除了
    git rm -f libs/sub-repo
    # 4. 提交删除操作
    git commit -m "remove: 移除子模块 sub-repo"

附录:关于 Git submodule 的理解

  • submodule 自己知道自己被当做 submodule

    • 一个项目被作为 submodule 后,他的 ./submodule_name/.git 将不再是一个文件夹,而是一个指明 .git/ 文件夹路径的配置文件

      1
      cat ./submodule_name/.git

      gitdir: ../.git/modules/submodule_name

    • .git/文件夹可以在./.git/modules/submodule_name/.git/中找到

  • submodule 相关信息都在外层项目中显示出来

  • 在 submodule 文件夹./submodule_name/下, submodule 的更新,提交等操作正常按照一般项目进行即可

    • 这里操作时虽然仓库在外层项目的./.git/modules/submodule_name/.git/中,但是在 submodule 的目录下我们可以正常访问 submodule 的仓库
    • 也就是说在 submodule 文件夹下的git操作(add, commit)实际上不修改当前文件夹下的任何文件,修改都在外层项目的./.git/modules/submodule_name/.git/仓库中
  • 外层项目只存储

    • submodule 文件夹
    • ./.gitmodules中存储 submodule 相关信息(文件夹路径与 submodule 远程地址)
    • 在GitHub中,直接用网页打开项目可以看到 submodule 会被自动解析远程地址和最近提交的ID信息,点击 submodule 对应的文件夹链接即可跳转到 submodule 远程仓库地址中

递归 submodule

  • 递归时记住项目的库都在父项目的库中即可
    • 这句话等价于所有项目的库都在根项目的 .git/

特别说明

  • 非必要不建议使用 submodule

附录:移除 submodule .git 但内容保留到主项目

  • 如果 Git 项目下面有个 submodule 也是包含 Git 的(可能是 git clone 命令下载的)的,往往不能正常的提交和管理项目,这是因为项目变成了 Git submodule 了
  • 现象:如果 submodule 是 git clone 别人的项目,我们将 submodule 提交到整个大项目中时
    • 会提示:modified:xxx(modified content, untracked content)
    • 此时如果直接提交,那么远程仓库里面 submodule 将是空的
  • 若不想再保留 submodule 的 git 仓库,则需要删除 submodule 相关的所有信息

第一步:需要先删除子模块

  • 移除子模块:
    1
    2
    3
    4
    # 1. 解除子模块关联
    git submodule deinit -f libs/sub-repo
    # 2. 删除 .git 中的子模块缓存
    rm -rf .git/modules/libs/sub-repo

第二步:重新添加文件路径(当做普通的文件)

  • 重新添加 submodule 文件夹
    1
    git add xxx

git submodule 是否跟踪分支的区别

  • 在 Git 中使用子模块(Submodule)时,“不跟踪分支(默认行为/锁定特定提交)”“跟踪分支” 的核心区别在于父项目(Superproject)如何决定子模块应该停留在哪个版本 ,以及 更新子模块时的流程

不跟踪分支(默认行为 / 锁定特定 Commit)

  • 这是 Git 子模块最原始也是最常用的工作方式
  • 父项目只关心:“子模块必须是 a1b2c3d 这个提交”,不在乎这个提交属于哪个分支,也不在乎这个提交是不是最新的
  • 当克隆父项目并运行 git submodule update 时,Git 会进入子模块目录,强制将其 checkout 到父项目记录的那个 SHA-1 哈希值
    • 注:此时,子模块处于 Detached HEAD(游离指针)状态
  • 不跟踪分支时,如果要切换到某个分支的最新 commit,需要执行如下操作:
    • 1)进入子模块目录:cd submodule_dir
    • 2)手动拉取或切换:git checkout master && git pull
    • 3)回到父项目目录:cd ..
    • 4)提交变更:git add submodule_dir -> git commit
  • 建议使用这种方式, 团队所有成员拉取代码后,得到的子模块代码完全一致,不会因为子模块远程仓库更新了代码而导致父项目构建失败

跟踪分支

  • 可通过配置 .gitmodules 文件来实现的一种更动态的模式

  • .gitmodules 中明确告诉 Git:“这个子模块应该跟随 main (或 dev) 分支”

    • 也可以通过命令行绑定,如添加 submodule 时,或之后直接修改:

      1
      2
      3
      4
      5
      6
      7
      8
      # # 新建 submodule:
      git submodule add -b submodule_master https://github.com/xxx/lib.git
      # 此时 .gitmodules 中会添加 branch = submodule_master

      # # 已有 submodule 绑定某个 分支:
      git config -f .gitmodules submodule.<本地存放路径>.branch <branch>
      # 比如将 dev 分支绑定到名为 libs/sub-repo 的 submodule 下:
      git config -f .gitmodules submodule.libs/sub-repo.branch dev
    • .gitmodules 文件中会多一行配置:

      1
      2
      3
      4
      [submodule "my-lib"]
      path = my-lib
      url = https://github.com/example/my-lib.git
      branch = main # 多出来的配置
  • 虽然父项目在数据库中依然存储的是 SHA-1 哈希值,但当你使用特定参数更新时(update 时添加 --remote),Git 会忽略本地记录的哈希值,直接去拉取远程分支的最新代码

  • 跟踪分支时,如果要切换到某个分支的最新 commit,只需要执行如下操作:

    • 不需要进入子模块目录,只需在父项目根目录运行:

      1
      git submodule update --remote
      • Git 会自动去子模块的远程仓库抓取 branch 字段指定分支的最新提交,并将子模块更新到该提交
      • 注意:如果不添加 --remote 则只是切换到当前 commit_id 而不会拉取最新分支(这与不绑定分支的 git submodule update 执行含义完全相同)
    • 注:运行完上述命令后,父项目的状态会显示子模块有变化(指向了新的 Hash),仍然需要 在父项目中执行 git addgit commit 来固化这个变更

  • 如果开发的项目依赖另一个正在快速迭代的库,且总是希望使用该库的最新版本,这种方式可以简化更新流程,但是要小心使用

一些实操及理解

  • 新增 submodule 时,默认(不跟踪):

    1
    2
    git submodule add https://github.com/xxx/lib.git
    # 此时 .gitmodules 中没有 branch 字段
  • 新增 submodule 时,跟踪分支:

    1
    2
    git submodule add -b main https://github.com/xxx/lib.git
    # 此时 .gitmodules 中会添加 branch = main
  • 更新 submodule 默认(不跟踪):

    • 如果运行 git submodule update什么都不会发生 ,因为它只会把子模块恢复到父项目当前记录的旧 Hash 值
  • 更新 submodule (若 .gitmodules 中已经跟踪分支):

    • 如果运行 git submodule update什么都不会发生 ,因为它只会把子模块恢复到父项目当前记录的旧 Hash 值,与 .gitmodules 中是否已经跟踪分支无关
    • 如果运行 git submodule update --remote,Git 会检测到配置了分支,于是去拉取远程最新代码,并更新本地子模块的指针
  • 特别说明:

    • “跟踪分支” 不意味着自动更新:即使配置了跟踪分支,当 git pull 父项目时,子模块不会自动更新到远程最新
      • 必须显式执行 git submodule update --remote
    • “跟踪分支” 后父项目仍然会记录 Hash 值,Git 的底层数据结构决定了父项目永远 只记录子模块的 Commit Hash
      • 所谓“跟踪分支”,只是提供了一个快捷命令(--remote)来帮你自动找到那个最新的 Hash 值并 checkout 过去,省去了手动进入子目录 pull 的过程
    • 如果是引用第三方稳定的开源库 ,或者要求构建环境绝对可复现,使用默认(不跟踪) 模式
    • 如果是迭代很快的开发,且父项目需要时刻集成子模块的最新开发成果,使用跟踪分支 模式