Git submodule 的精准用法
Git submodule 适合这种情况:
- 主项目需要别的子项目
- 主项目管理别的子项目的版本
概念
在 Emacs 配置中,可以把 ~/.emacs.d/ 视为主仓库。把 Vertico Consult Magit 等视为子仓库。
单凭 git clone 子仓库,Emacs 也可以用。但主仓库对子仓库的完整细节缺乏了解。
而 git submodule add 可以解决
- Vertico 从哪个 URL 下载
- 应该保存在哪个目录
- 应该使用哪个 commit
- 换电脑后应该怎样恢复
- 主配置回滚时,Vertico 应该回到哪个版本
.gitmodules 内容大致如下:
[submodule "travel/packages/vertico"]
path = travel/packages/vertico
url = https://github.com/minad/vertico.git
submodule 还会记录一条指向某个 commit 的 gitlink。
| 仓库 | 负责记录什么 |
|---|---|
~/.emacs.d 主仓库
|
自己的配置文件,以及每个插件应使用哪个 commit |
| Vertico 子仓库 | Vertico 源码和 Vertico 自己的完整开发历史 |
| Consult 子仓库 | Consult 源码和 Consult 自己的完整开发历史 |
| Magit 子仓库 | Magit 源码和 Magit 自己的完整开发历史 |
这种设计避免了两个问题:
- 不把别人写的几百个插件源码文件伪装成自己的文件
- 又能让配置仓库精确声明自己依赖哪个插件版本
submodule 最重要的价值
- 固定插件版本,它记录的不是模糊的“使用最新版”,而是,这套配置经过测试时,各插件分别停留在哪个准确版本。这相当于给插件环境加了一把版本锁。
- 整体回滚。主仓库回滚到 abc1234,子仓库也会恢复到当时的状态。普通嵌套
git clone做不到这一点。即使主配置回到一个月前,插件仍可能停留在今天的新版本。 - 异地完整复现。
git clone --recurse-submodules URL PATH,恢复出来的插件环境与原电脑一致。 - 明确记录插件升级。使用 submodule 后,升级插件会被主仓库识别为一次正式变化,这让插件升级变成一种可查看、可测试、可提交、可撤销的操作。
工作流
配置
- git config diff.submodule log
- git config status.submodulesummary 1
添加
- cd ~/.emacs.d
- git init -b main
- git submodule add https://github.com/minad/vertico.git travel/packages/vertico
- git status
- git commit -m "Add Vertico submodule"
- git submodule status
不要修改插件原始仓库。不应把自己的配置写进子仓库。
查看
- git -C travel/packages/vertico log --oneline --decorate -10
升级
- git submodule update --remote -- travel/packages/vertico
- git diff --submodule=log
保留
- git add travel/packages/vertico
- git commit -m "Update Vertico" ;; commit 了才正式获得了主仓库的承认
一次升级一个插件,一个插件形成一个独立提交。
未提交+放弃
如果执行升级后发现新版本有问题,而且还没有提交:
git submodule update \
--checkout \
--force \
-- travel/packages/vertico
这就是最简单的“升级失败,退回旧版”。
已提交+放弃
假设升级 Vertico 的提交是:7ac39fe Update Vertico
可以在主仓库运行:
git revert 7ac39fe
git submodule update --init --recursive
git revert 会新增一个提交,撤销那次插件升级;随后 git submodule update 让磁盘上的插件切回旧 commit。
这种做法比随意修改子仓库历史更清楚,因为决定插件正式版本的是主仓库中的 gitlink,而不是子仓库当前碰巧停留在哪里。
detached
git status 时发现 submodule 经常处于 detached HEAD?
这在 submodule 中通常是正常现象。
主仓库需要插件准确停在某个 commit,而不是随时变化。对只使用插件、不直接参与开发插件的人而言,这不是故障,而是版本锁定的正常状态。
submodule 应该少升级、慢升级、一次一个、稳定后再提交。submodule 最大的价值不是方便追逐最新版,而是方便长期固定已验证版本。
删除
- git rm travel/packages/vertico
- git rm travel/40-test/init-vertico.el
- git commit -m "Remove Vertico" </syntaxhighlight>
git rm 会同时更新主仓库索引和 .gitmodules 中相应的 submodule 记录。
暂停
- git submodule deinit -- travel/packages/vertico
不暂停
- git submodule update --init -- travel/packages/vertico
移机
;; 一次性移机主仓库和子仓库
git clone --recurse-submodules \
主仓库URL \
~/.emacs.d
或者,
;; 先移机主仓库
;; 再移机子仓库
git clone 主仓库URL ~/.emacs.d
git submodule update --init --recursive
日常更新
git pull
git submodule update --init --recursive
第二条命令让本机插件真正切换到主仓库指定的版本。
单纯运行第一条命令不太够。
边界
Git submodule 只负责源码仓库和版本关系,不是完整的 Emacs 包管理器。
它不会自动完成:
- 把插件加入
load-path - 执行
require - 安装插件依赖
- 生成 autoload
- 字节编译
- native compilation
- 编写
setq - 添加 hook
- 配置快捷键
例如 Consult 依赖其他插件时,Git 不会自动读取 Emacs 的 Package-Requires 并安装依赖。
需要自己继续添加依赖:
git submodule add 依赖仓库URL travel/packages/依赖名
然后自己配置加载关系。
Git submodule 的本质是,放弃包管理器的一部分自动化,换取透明、固定、可控和可复现。对于大量、频繁更新的插件,手工管理会比较繁琐;对于少量精选、长期固定、谨慎升级的插件,非常合适。
对比
| 能力 | 普通嵌套 git clone | Git submodule |
|---|---|---|
| 插件能否运行 | 能 | 能 |
| 插件保持独立仓库 | 能 | 能 |
| 主仓库记录插件 URL | 否 | 是 |
| 主仓库记录插件路径 | 否 | 是 |
| 主仓库锁定插件 commit | 否 | 是 |
| 换电脑后一条命令恢复 | 否 | 是 |
| 主配置和插件整体回滚 | 否 | 是 |
| 正式记录插件升级 | 否 | 是 |
| 保留插件自己的完整历史 | 是 | 是 |
最核心的区别是,普通 clone 插件只是碰巧存在于这台电脑。Git submodule,插件是主项目正式声明的一项依赖。
工作流
| 目的 | 命令 |
|---|---|
| 添加插件 | git submodule add URL PATH
|
| 查看所有插件版本 | git submodule status
|
| 升级一个插件 | git submodule update --remote -- PATH
|
| 查看插件升级内容 | git diff --submodule=log
|
| 接受插件升级 | git add PATH && git commit
|
| 放弃未提交的升级 | git submodule update --checkout --force -- PATH
|
| 初始化全部插件 | git submodule update --init --recursive
|
| 克隆主仓库和全部插件 | git clone --recurse-submodules URL
|
| 暂时停用本机插件 | git submodule deinit -- PATH
|
| 永久删除插件 | git rm PATH && git commit
|
| 查看插件自己的日志 | git -C PATH log --oneline
|
git submodule update \
--remote \
-- travel/packages/插件名
查看升级内容
git diff --submodule=log
测试失败,恢复旧版
git submodule update \
--checkout \
--force \
-- travel/packages/插件名
测试成功,接受新版
git add travel/packages/插件名
git commit -m "Update 插件名"
删除插件
git rm travel/packages/插件名
git rm travel/对应状态目录/init-插件名.el
git commit -m "Remove 插件名"
换电脑完整恢复
git clone --recurse-submodules \
主仓库URL \
~/.emacs.d
总结
普通 git clone 只是:
- 把插件下载到了当前电脑。
git submodule add 则是:
- 让主仓库正式记录插件的 URL、保存路径和准确版本。
Git submodule 对 Emacs 插件管理最大的意义是:
- 对插件版本状态进行精确控制,使整套配置可以稳定锁定、明确升级、快速回滚,并在其他电脑上一键复现。