正确做法是克隆时使用git clone --recursive <url>,或分两步执行git submodule init和git submodule update --init;子模块目录默认为空是Git设计使然,并非bug。

克隆含子模块的仓库后,不运行 git submodule update --init,子模块目录永远是空的——这不是 bug,是 Git 的设计逻辑。
克隆时就拉取全部子模块内容
新人最容易卡在这一步:用 git clone 拉下主仓库,进目录一看,子模块文件夹存在但里面什么都没有。这是因为默认克隆只下载主仓库的 .gitmodules 文件和引用,不下载子模块本身。
正确做法是一次性完成:
- 用
git clone --recursive <url>(推荐)——适用于首次克隆 - 或分两步:
git clone <url>后进入目录,再执行git submodule update --init
注意:--recursive 会递归初始化所有嵌套子模块(比如子模块里还有子模块),而 --init 只初始化一级;如果项目结构简单,--init 就够用。
更新子模块到远程最新提交
日常开发中,子模块仓库自己也在更新,你得主动把它的新提交同步进主项目。但直接 git submodule update 不会拉新代码——它只检出 .gitmodules 或主仓库记录的「那个旧提交」。
要拉远程最新,必须加 --remote:
-
git submodule update --remote:检出子模块远程HEAD(通常是 default branch,如main)的最新提交 -
git submodule update --remote --rebase:用 rebase 方式更新,避免产生无意义 merge 提交 - 如果子模块用了非默认分支,先在子模块目录里
git checkout feat/x,再回到主项目git add <path>提交新引用
⚠️ 注意:--remote 不会自动 git push 主项目。更新完子模块后,主项目工作区会显示子模块路径为“modified”,需手动 git add <path> + git commit 固化这个新引用。
子模块状态混乱?先看 git submodule status
子模块出问题,第一反应不该是删重来,而是先确认当前状态。运行 git submodule status 会输出三列:
- 首字符
-:子模块未初始化(还没 runinit) - 首字符
+:子模块检出的提交与主项目记录的不一致(常见于本地改过子模块但没提交) - 首字符
(空格):状态正常,检出的就是主项目记录的提交 - 第二列是实际检出的 commit hash
- 第三列是路径
例如输出 +a1b2c3d src/libfoo,说明你本地改了 src/libfoo 但没提交,此时 git diff 也会显示它被修改。想丢弃这些改动?进子模块目录 git reset --hard 即可。
删除子模块不是 rm -rf 就完事
直接 rm -rf <path> 会留下脏状态:主项目仍认为该路径是个子模块,git status 一直报 “deleted”;.gitmodules 和 .git/config 里的配置也还在。
干净删除四步走:
- 执行
git submodule deinit <path>:清除.git/config中的子模块条目,并清空子模块工作区(但保留.git目录) - 执行
git rm <path>:从暂存区移除子模块引用,同时删掉.gitmodules中对应段落 - 手动删残留:
rm -rf .git/modules/<path>(否则下次 init 可能出错) git commit -m "remove submodule <path>"
这四步缺一不可。漏掉 deinit,下次 submodule update 还会试图拉取;漏掉 rm -rf .git/modules/...,可能触发 “submodule path 'x' already exists in the index” 报错。
子模块真正的复杂点不在命令多,而在「主项目只存引用,子模块内容完全自治」——这意味着每次更新、切换、调试,都要在两个 git 上下文之间跳转。最容易被忽略的是:你改了子模块代码,却忘了在子模块里 git commit 和 git push,导致别人 update --remote 拉不到你的改动。


















