bigywater.tech
Git · 68

submodule:gitlink 与 .gitmodules

git submodule 把另一个仓库“嵌”在当前仓库里,它在 .git 里到底留下什么、超级项目的提交里记了什么、为什么 clone 之后子模块是空的?这篇用两个本机仓库把整个生命周期走一遍。

160000
超级项目的 tree 里子模块的条目模式,值是子仓库的一个提交 hash,只此而已
- + 空格
git submodule status 的前缀:- 未初始化,+ 与记录的不同,空格 一致
.git/modules/
子仓库真正的 git 目录放在超级项目的 .git 里,子模块目录里只有一个 .git 文件

add 做了什么

实测 · 本机执行输出
  $ git -c protocol.file.allow=always submodule add -q /work/l64lib.git vendor/lib 2>&1 | tail -1; git status --short
  A  .gitmodules
  A  vendor/lib
  $ cat .gitmodules | sed -E "s#url = .*/#url = …/#"; echo "--- .git/config 里:"; git config --get-regexp "^submodule" | sed -E "s#.*/#…/#"
  [submodule "vendor/lib"]
  	path = vendor/lib
  	url = …/l64lib.git
  --- .git/config 里:
  …/l64lib.git
  …/lib.active true
  $ echo "--- 子模块目录里的 .git 是个文件:"; cat vendor/lib/.git; echo "--- 真正的 git 目录在超级项目的 .git/modules 下:"; ls .git/modules; ls .git/modules/vendor/lib | tr "\n" " "; echo
  --- 子模块目录里的 .git 是个文件:
  gitdir: ../../.git/modules/vendor/lib
  --- 真正的 git 目录在超级项目的 .git/modules 下:
  vendor
  HEAD config description hooks index info logs objects packed-refs refs
  $ git ls-files -s vendor/lib .gitmodules | sed -E "s/[0-9a-f]{40}/<sha40>/"
  100644 <sha40> 0	.gitmodules
  160000 <sha40> 0	vendor/lib

创建 .gitmodules(路径与 URL,提交进仓库),在 .git/config 写下 submodule.<名>.url 与 active,把子仓库克隆到 .git/modules/<路径>,并在子模块目录放一个 .git 文件(gitdir: …)指回去。暂存区里子模块是 160000 条目。

超级项目记什么

实测 · 本机执行输出
  $ git ls-tree -r HEAD | sed -E "s/[0-9a-f]{40}/<sha40>/"
  100644 blob <sha40>	.gitmodules
  100644 blob <sha40>	app.txt
  160000 commit <sha40>	vendor/lib
  记录的 hash == 子仓库当前 HEAD: True

超级项目的 tree 里子模块只是一个 160000 的 commit 条目,值是子仓库的某个提交 hash。子仓库的任何内容都不在超级项目里。

新提交

实测 · 本机执行输出
  $ git status --short; git diff --submodule=log | sed -E "s/[0-9a-f]{7}\.\.[0-9a-f]{7}/<旧>..<新>/"; git submodule status | sed -E "s/[0-9a-f]{40}/<sha40>/"
   M vendor/lib
  Submodule vendor/lib <旧>..<新>:
    > lib v2
  +<sha40> vendor/lib (remotes/origin/HEAD)

子仓库有新提交且子模块检出到它时,超级项目显示 M(git diff --submodule=log 展开);只有在超级项目里 git add 并提交,新 hash 才被记录。

clone 之后

实测 · 本机执行输出
  $ ls vendor/lib | wc -l | sed "s/^ */vendor\/lib 里的文件数: /"; git submodule status | sed -E "s/[0-9a-f]{40}/<sha40>/"; echo "(开头的 - 表示还没初始化)"; git config --get-regexp "^submodule" | wc -l | sed "s/^ */.git\/config 里 submodule 配置项数: /"
  vendor/lib 里的文件数: 0
  -<sha40> vendor/lib
  (开头的 - 表示还没初始化)
  .git/config 里 submodule 配置项数: 0
  $ git -c protocol.file.allow=always submodule update --init 2>&1 | sed -E "s#/[^ ]*/l64lib.git#<url>#; s/[0-9a-f]{40}/<sha40>/" | tail -2; ls vendor/lib; git submodule status | sed -E "s/[0-9a-f]{40}/<sha40>/"; cd vendor/lib && git status | head -1
  done.
  Submodule path 'vendor/lib': checked out '<sha40>'
  lib.txt
   <sha40> vendor/lib (heads/main)
  HEAD detached at decf4a0

普通 clone 得到空目录,git submodule status 以 - 开头。git submodule update --init 才克隆并把子模块检出到记录的提交(分离 HEAD)。clone --recurse-submodules 一步完成。

切换不跟随

实测 · 本机执行输出
  $ git checkout -q HEAD~1; git submodule status | sed -E "s/[0-9a-f]{40}/<sha40>/"; echo "(+ 表示子模块当前检出的提交与超级项目记录的不同)"; git status --short
  +<sha40> vendor/lib (heads/main)
  (+ 表示子模块当前检出的提交与超级项目记录的不同)
   M vendor/lib
  $ git -c protocol.file.allow=always submodule update 2>&1 | tail -1 | sed -E "s/[0-9a-f]{40}/<sha40>/"; git submodule status | sed -E "s/[0-9a-f]{40}/<sha40>/"; cat vendor/lib/lib.txt
  Submodule path 'vendor/lib': checked out '<sha40>'
   <sha40> vendor/lib (477f05d)
  v1

切换超级项目的分支或提交不会自动更新子模块,status 里以 + 提醒不一致;git submodule update(或 submodule.recurse=true)让它对齐。

URL

实测 · 本机执行输出
  $ ls vendor/lib; git submodule status | sed -E "s/[0-9a-f]{40}/<sha40>/"
  lib.txt
   <sha40> vendor/lib (heads/main)
  $ echo "改了 .gitmodules 之后,.git/config 里的 URL 还是旧的:"; git config --get submodule.vendor/lib.url | sed -E "s#.*/#…/#"; git submodule sync -q; echo "sync 之后:"; git config --get submodule.vendor/lib.url
  改了 .gitmodules 之后,.git/config 里的 URL 还是旧的:
  …/l64lib.git
  sync 之后:
  https://example.invalid/lib.git

.gitmodules 是随仓库提交的,.git/config 是本机实际使用的;改了前者要 git submodule sync 才同步到后者。

分步演示

子模块
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。子模块的“远程”是本机的裸仓库(file 路径),因此每次 submodule add / update 都带 -c protocol.file.allow=always(新版 git 默认禁止子模块用 file 协议);输出里 hash、URL 做了替换
    • 没有验证:嵌套子模块(子模块里的子模块);git submodule update --remote 追踪分支;submodule 的 branch 配置;删除子模块的完整步骤(git rm + .git/modules 清理);子模块的 shallow 克隆;git worktree 与子模块;submodule.recurse 的完整效果
    京ICP备2026067502号-1京公网安备11010502063487号