在项目中使用submodule时如何保持git记录干净
在主项目中引用别人的仓库时,git submodule 是一个很常见的方案。但它也很容易让人困惑:明明没有改 server 里面的代码,为什么 Android Studio 或 git status 里还是显示有改动?
核心原因是:主项目记录的不是子模块目录里的全部源码,而是子模块当前指向的某一个提交。
以一个主项目 OxyMusic 和子模块 server 为例:
OxyMusic
├── app
├── build.gradle
└── server -> 指向 KuGouMusicApi 仓库的某个 commit
这里的 server 虽然看起来像一个普通目录,但在 Git 眼里,它是一个特殊的引用。主项目只记录一件事:
server 当前应该停在哪个 commit
所以,当 server 目录里的 HEAD 从一个提交变成另一个提交时,即使你没有修改任何源码,主项目也会显示:
M server
这表示的不是“你改了 server 代码”,而是“主项目记录的子模块指针和本地 server 当前指向的提交不一致”。
子模块源码改动和子模块指针改动
使用 submodule 时,要分清两类变化。
第一类是子模块内部的源码改动。例如:
cd server
git status
如果你看到:
modified: xxx.js
untracked: package-lock.json
这说明 server 仓库内部有代码或未跟踪文件变化。这些变化属于 server 仓库,不属于主项目仓库。
第二类是主项目中的子模块指针改动。例如在主项目根目录执行:
git status
看到:
modified: server
这说明主项目认为 server 当前指向的 commit 变了。
这两件事是不同的:
server 内部源码变化:发生在子模块仓库里
主项目中的 M server:发生在主项目对子模块 commit 的引用上
日常保持主项目干净的方式
如果你只想在主项目中使用 server,不打算修改 server 仓库,那么日常流程应该是:
git pull
git submodule update --init --recursive
第一行拉取主项目更新,第二行让子模块回到主项目记录的版本。
如果这两个命令执行后,主项目仍然显示干净:
git status
结果类似:
nothing to commit, working tree clean
就说明状态正常。
当子模块远端更新时,如何让主项目引用新版
如果你希望 server 跟随原作者仓库更新,不要直接在 server 里随意切换后就不管。推荐在主项目根目录执行:
git submodule update --remote server
然后查看状态:
git status
git diff -- server
你通常会看到类似:
Subproject commit old_commit
Subproject commit new_commit
这表示主项目的 server 指针从旧提交更新到了新提交。
确认这个新版可以使用后,在主项目中提交这个指针变化:
git add server
git commit -m "Update server submodule"
这次提交只会更新主项目对 server 的引用,不会把 server 的源码推送到原作者仓库。
只有在子模块目录中执行下面这样的命令,才是在操作子模块自己的远端仓库:
cd server
git push origin main
如果你没有原作者仓库的写权限,这个 push 也会被 GitHub 拒绝。
遇到 rollback 后仍然显示改动怎么办
Android Studio 的 rollback 对普通文件通常有效,但对子模块不一定能把它切回主项目记录的 commit。
应该先在主项目根目录检查:
git status --short
git diff -- server
如果看到的是子模块提交变化,例如:
-Subproject commit eeca806...
+Subproject commit 5a58694...
说明问题是子模块指针不同步。
正常情况下可以执行:
git submodule update --init server
让 server 回到主项目记录的提交。
如果主项目记录的子模块提交已经不存在
有一种比较麻烦的情况:主项目记录了一个子模块 commit,但这个 commit 在子模块远端已经取不到了。
执行:
git submodule update --init server
可能会报错:
fatal: remote error: upload-pack: not our ref <commit>
fatal: Fetched in submodule path 'server', but it did not contain <commit>.
这说明主项目记录的子模块提交在远端仓库中已经不存在。常见原因包括:
- 子模块远端发生过 force push
- 子模块仓库换过历史
- 主项目曾经记录了一个只存在于别人本地或旧仓库中的提交
这时想让项目真正保持干净,最合理的做法通常是把主项目中的子模块指针更新到一个当前远端仍然存在的提交:
git submodule update --remote server
git add server
git commit -m "Update server submodule reference"
这不是在修改别人的仓库,而是在修复主项目对子模块的引用。
如果你必须回到那个已经不存在的旧提交,就需要找到包含该提交的旧远端、fork、备份仓库,或者让拥有该提交的人重新推送它。否则本地无法凭空 checkout 到这个提交。
不建议长期隐藏子模块变化
有些情况下,可以用下面的命令让 Git 暂时忽略 server 的变化:
git update-index --assume-unchanged server
恢复检查:
git update-index --no-assume-unchanged server
但这只是本地隐藏改动,不是真正解决问题。长期使用容易让你错过真实的子模块更新,也会让团队协作时状态不一致。
真正干净的做法是:主项目记录一个当前可以从子模块远端获取到的 commit,并且所有人都通过 git submodule update --init --recursive 同步到这个版本。
推荐工作流总结
平时拉取项目:
git pull
git submodule update --init --recursive
只改主项目代码,不改 server:
git status
确认改动只出现在主项目自己的文件中,不要提交 server 内部源码变化。
需要更新 server 到原作者最新版:
git submodule update --remote server
git diff -- server
git add server
git commit -m "Update server submodule"
发现 server 内部有未跟踪文件:
cd server
git status
如果这些文件不是你需要的,可以删除或按子模块仓库自己的规则处理。
一句话总结:
主项目可以提交 server 的 commit 指针,但不要提交或推送 server 仓库里的源码改动。只要分清这两层 Git 状态,submodule 就能既保持引用清晰,又保持主项目记录干净。