git submodule 把另一个仓库“嵌”在当前仓库里,它在 .git 里到底留下什么、超级项目的提交里记了什么、为什么 clone 之后子模块是空的?这篇用两个本机仓库把整个生命周期走一遍。
$ 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 才被记录。
$ 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)让它对齐。
$ 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 才同步到后者。