Git submodule 的精准用法

From 清冽之泉
Jump to navigation Jump to search

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 自己的完整开发历史

这种设计避免了两个问题:

  1. 不把别人写的几百个插件源码文件伪装成自己的文件
  2. 又能让配置仓库精确声明自己依赖哪个插件版本

submodule 最重要的价值

  1. 固定插件版本,它记录的不是模糊的“使用最新版”,而是,这套配置经过测试时,各插件分别停留在哪个准确版本。这相当于给插件环境加了一把版本锁。
  2. 整体回滚。主仓库回滚到 abc1234,子仓库也会恢复到当时的状态。普通嵌套 git clone 做不到这一点。即使主配置回到一个月前,插件仍可能停留在今天的新版本。
  3. 异地完整复现。git clone --recurse-submodules URL PATH,恢复出来的插件环境与原电脑一致。
  4. 明确记录插件升级。使用 submodule 后,升级插件会被主仓库识别为一次正式变化,这让插件升级变成一种可查看、可测试、可提交、可撤销的操作。

工作流

配置

  1. git config diff.submodule log
  2. git config status.submodulesummary 1

添加

  1. cd ~/.emacs.d
  2. git init -b main
  3. git submodule add https://github.com/minad/vertico.git travel/packages/vertico
  4. git status
  5. git commit -m "Add Vertico submodule"
  6. git submodule status

不要修改插件原始仓库。不应把自己的配置写进子仓库。

= 查看

  1. git -C travel/packages/vertico log --oneline --decorate -10

升级

git submodule update --remote -- travel/packages/vertico git diff --submodule=log

保留

  1. git add travel/packages/vertico
  2. 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 最大的价值不是方便追逐最新版,而是方便长期固定已验证版本。

删除

  1. git rm travel/packages/vertico
  2. git rm travel/40-test/init-vertico.el
  3. git commit -m "Remove Vertico" </syntaxhighlight>

git rm 会同时更新主仓库索引和 .gitmodules 中相应的 submodule 记录。

暂停 =

  1. git submodule deinit -- travel/packages/vertico

不暂停

  1. 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

</syntaxhighlight>

第二条命令让本机插件真正切换到主仓库指定的版本。

单纯运行第一条命令不太够。

边界

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 插件管理最大的意义是:

对插件版本状态进行精确控制,使整套配置可以稳定锁定、明确升级、快速回滚,并在其他电脑上一键复现。