Git submodule 固定版本:子目录已经更新,父仓库为什么仍记录旧提交
把一个仓库作为子模块放进项目后,进入子目录切换到新版,应用测试也通过了,但同事拿到父项目仍然使用旧版。这通常不是 fetch 没生效,而是两层仓库记录的状态不同:子模块有自己的提交历史,父仓库还需要明确记录要使用其中哪一个提交。
父仓库通过 gitlink 保存子模块提交的对象标识,而不是把子目录所有文件当成自己的普通文件逐个记录。.gitmodules 提供路径和来源等配置,它不能替代这个版本指针。只更换子模块工作树所在提交,不会自动改写父仓库已经产生的提交。
在本地做一次完整的版本升级
下面代码保存为 demo.sh 后用 bash demo.sh 运行,已在 Git 2.52.0、Bash 5.2.37 验证。所有仓库、身份和配置都在一次性目录里,不访问网络。protocol.file.allow 只对指定命令生效,用于读取脚本刚创建的可信本地源,不修改全局配置。
AI概念示意图:用抽象图形说明本文主题,不代表真实界面或运行结果。
#!/usr/bin/env bash set -euo pipefail export LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null lab=$(mktemp -d) trap 'rm -rf -- "$lab"' EXIT for repo in library app; do git -c init.templateDir= init -q -b main "$lab/$repo" git -C "$lab/$repo" config user.name Demo git -C "$lab/$repo" config user.email demo@example.invalid git -C "$lab/$repo" config commit.gpgSign false done printf 'v1\n' > "$lab/library/release.txt" git -C "$lab/library" add . git -C "$lab/library" commit -qm v1 v1=$(git -C "$lab/library" rev-parse HEAD) cd "$lab/app" git -c protocol.file.allow=always submodule add -q "$lab/library" vendor/lib git commit -qm 'Pin library v1' printf 'v2\n' > "$lab/library/release.txt" git -C "$lab/library" commit -qam v2 v2=$(git -C "$lab/library" rev-parse HEAD) git -C vendor/lib fetch -q origin git -C vendor/lib switch -q --detach "$v2" [[ $(git rev-parse HEAD:vendor/lib) == "$v1" ]] [[ $(git -C vendor/lib rev-parse HEAD) == "$v2" ]] printf 'before: parent=v1 child=v2\n' git diff --submodule=short -- vendor/lib git add vendor/lib git commit -qm 'Pin library v2' [[ $(git rev-parse HEAD:vendor/lib) == "$v2" ]] git ls-tree HEAD vendor/lib git clone -q "$lab/app" "$lab/reader" git -C "$lab/reader" -c protocol.file.allow=always submodule update --init -q [[ $(git -C "$lab/reader/vendor/lib" rev-parse HEAD) == "$v2" ]] [[ $(cat "$lab/reader/vendor/lib/release.txt") == v2 ]] if git -C "$lab/reader/vendor/lib" symbolic-ref -q HEAD; then exit 1; fi printf 'fresh clone: v2; detached HEAD\n'
三个位置要分别读取
第一组断言确认父仓库 HEAD 中的 gitlink 仍是 v1,子模块 HEAD 已经是 v2。git diff --submodule=short 展示两个提交标识之间的变化;这份差异说明父项目有一个尚未提交的依赖版本调整,不能仅凭子目录里的 release.txt 已显示 v2 就认定升级完成。
执行 git add vendor/lib 后,父仓库暂存的是新的子模块提交指针。再提交父仓库,HEAD:vendor/lib 才变成 v2。git ls-tree 展示的条目模式为 160000,类型为 commit,这与普通文件的 blob 条目不同。脚本打印的具体哈希会随提交时间变化,断言比较的是实际保存的标识。
让另一个克隆证明指针可重现
脚本接着克隆父仓库,再运行 submodule update --init。新克隆应检出父项目所记录的 v2,内容断言也检查 release.txt 确实是 v2。默认 checkout 更新方式通常让子模块处于分离 HEAD,本例通过 symbolic-ref 的失败状态验证这一点。
分离 HEAD 在这里表示固定依赖版本,不等于仓库损坏。若要在子模块里开发并提交,先有意识地建立自己的分支,再把新提交送到协作者可访问的子模块远程;仅在本机产生一个提交并把它的哈希写入父仓库,别人未必能够获取那个对象。
真实项目的交付顺序
升级流程应包含选定子模块提交、验证兼容性、确认该提交已能从约定来源获取,以及提交父仓库指针。发布父项目之前先保证子模块提交可获取,能避免出现“父项目可克隆,依赖版本不存在”的交接故障。
普通 update 依据父仓库记录的提交恢复工作树;带 --remote 的更新则涉及子模块远程跟踪分支,是另一种选择新版本的流程。不要把两者混用后,再以为父仓库始终固定到远程最新版本。团队应明确谁负责提出和验收指针更新。
本例没有未提交文件,也没有嵌套子模块。实际操作前要分别查看父仓库和子模块状态;存在本地改动时,先处理这些工作,再更新版本。不要为了让指针看起来整齐就加 --force,以免覆盖本来需要保留的子模块修改。


