我这儿是好的 —— 子模块钉在了旧提交上
目标
把库仓库作为子模块钉进父仓库,并亲手制造一次事故:没有把更新提交到父仓库,同事就拿到了旧代码。
为什么重要
子模块上发生的事故几乎都出自同一个误解——以为父仓库指向的是分支。进入父仓库树的,是一个模式为 160000 的条目,值是提交哈希。因为是提交而不是分支,所以子模块总是以 detached HEAD 状态检出,即使库中堆叠了新提交,父仓库也不会自动跟进。本实验不是靠讲解来告诉你这个事实,而是让你亲自从历史中把它取出来。再确认 git clone 默认不会获取子模块,“只有 CI 上构建会坏”这种常见症状为什么会出现,就一下子理清了。
步骤
- 在
/root/gitx5/lib创建一个含 2 个提交的库仓库(version.txt从1.0.0变为1.1.0)。 - 创建
/root/gitx5/app,把lib作为子模块钉在vendor/lib位置。把第一次尝试被阻止的情形保存到notes/protocol.txt。 - 把父仓库记住的内容保存到
notes/pointer.txt。 - 在
lib中堆叠1.2.0提交,把子模块移到该提交,再把 detached HEAD 状态和submodule status的首字符保存到notes/detached.txt。 - 把这次更新提交到父仓库。
- 在
lib中堆叠1.3.0,只移动子模块、不提交父仓库,在这种状态下 clone 出/root/gitx5/clone,并把接收方看到的版本写入notes/accident.txt。 - 不加
--recurse-submodulesclone 出/root/gitx5/clone2,制造出首字符-,并把四种首字符整理到notes/status.txt。 - 在
notes/report.md中总结使用子模块时的规则。
参考
- 这个镜像没有全局 git 身份。每次创建仓库时,都要指定
git config user.email和user.name。 - 提交时间用
GIT_AUTHOR_DATE和GIT_COMMITTER_DATE固定。 - 常见错误:第 2 步第一次尝试被阻止时,以为实验出了问题而直接跳过。被阻止才是正常的,弄清原因正是这一步的任务。
- 常见错误:在第 6 步把父仓库提交了。必须在没有提交的状态下 clone,才能重现事故。
创建公共库仓库
在 /root/gitx5/lib 创建一个含 2 个提交的库仓库(version.txt 从 1.0.0 变为 1.1.0)。
用 git init -b main 创建,并为每个仓库设置 user.email 和 user.name。只改 version.txt 一行,堆叠两个提交即可——后面的步骤中,这个值就是用肉眼判断它被钉在哪个提交上的标记。
本地路径子模块默认被阻止
创建 /root/gitx5/app,把 lib 作为子模块钉在 vendor/lib 位置。把第一次尝试被阻止的情形保存到 notes/protocol.txt。
直接执行 git submodule add /root/gitx5/lib vendor/lib 会因 transport 'file' not allowed 被阻止。这是 CVE-2022-39253 之后的默认行为。用 git -c protocol.file.allow=always submodule add ... 重新执行,钉住之后还必须把 .gitmodules 和 vendor/lib 提交。
进入父仓库树的那一行
把父仓库记住的内容保存到 notes/pointer.txt。
git ls-tree HEAD vendor/lib 输出的那一行就是子模块的全部。请一并写下三点:模式是什么、该值对应 lib 的哪个提交、.gitmodules 里写了什么。要点是任何地方都没有分支名。
子模块永远是 detached HEAD
在 lib 中堆叠 1.2.0 提交,把子模块移到该提交,再把 detached HEAD 状态和 submodule status 的首字符保存到 notes/detached.txt。
在 lib 中提交 1.2.0 之后,在 app/vendor/lib 里执行 git fetch origin,并 git checkout 到那个提交。看看其中 git status 的第一行是什么,以及在父仓库里 git submodule status 的首字符变成了什么。此时还不要提交父仓库。
把更新提交到父仓库
把这次更新提交到父仓库。
在父仓库执行 git add vendor/lib,暂存的是一个 gitlink 值。变化的不是目录里的那些文件,而是一行提交哈希,所以 git diff --cached 只会出现两行 Subproject commit。
没有提交的更新只属于你自己
在 lib 中堆叠 1.3.0,只移动子模块、不提交父仓库,在这种状态下 clone 出 /root/gitx5/clone,并把接收方看到的版本写入 notes/accident.txt。
在 lib 中提交 1.3.0,并把 app/vendor/lib 移到那里。在父仓库查看 git status,vendor/lib 会显示为已修改,但不要提交。在这种状态下执行 git -c protocol.file.allow=always clone --recurse-submodules /root/gitx5/app /root/gitx5/clone,再对比两边的 version.txt。
首字符所表达的四种状态
不加 --recurse-submodules clone 出 /root/gitx5/clone2,制造出首字符 -,并把四种首字符整理到 notes/status.txt。
git clone /root/gitx5/app /root/gitx5/clone2 不会获取子模块。在其中执行 git submodule status,看看首字符是什么。把初始化用的命令也一并写下,下次 CI 出问题时可以直接用上。
使用子模块时的规则
在 notes/report.md 中总结使用子模块时的规则。
把这次亲手做出的三件事——父仓库记住的内容、detached HEAD、漏提交更新的事故——改写成规则的形式。同时写下 clone 时该做什么,以及评审时如何解读只有一行的 Subproject commit diff。