Git submodule 固定版本:子目录已经更新,父仓库为什么仍记录旧提交

10-01 3阅读

把一个仓库作为子模块放进项目后,进入子目录切换到新版,应用测试也通过了,但同事拿到父项目仍然使用旧版。这通常不是 fetch 没生效,而是两层仓库记录的状态不同:子模块有自己的提交历史,父仓库还需要明确记录要使用其中哪一个提交。

父仓库通过 gitlink 保存子模块提交的对象标识,而不是把子目录所有文件当成自己的普通文件逐个记录。.gitmodules 提供路径和来源等配置,它不能替代这个版本指针。只更换子模块工作树所在提交,不会自动改写父仓库已经产生的提交。

在本地做一次完整的版本升级

下面代码保存为 demo.sh 后用 bash demo.sh 运行,已在 Git 2.52.0、Bash 5.2.37 验证。所有仓库、身份和配置都在一次性目录里,不访问网络。protocol.file.allow 只对指定命令生效,用于读取脚本刚创建的可信本地源,不修改全局配置。

Git submodule 固定版本:子目录已经更新,父仓库为什么仍记录旧提交

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,以免覆盖本来需要保留的子模块修改。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。