Git submodule 的精准用法: Difference between revisions

From 清冽之泉
Jump to navigation Jump to search
No edit summary
Line 1: Line 1:
Git submodule 适合这种情况:
Git submodule 适合这种情况:
* 主项目需要别的子项目
* 主项目管理别的子项目的版本


:一个主项目依赖若干外部 Git 仓库,但又希望这些外部仓库继续保持独立,并且由主项目精确指定它们的版本。
在 Emacs 配置中,可以把 ~/.emacs.d/ 视为主仓库。把 Vertico Consult Magit 等视为子仓库。


在 Emacs 配置中,可以把:
单凭 git clone 子仓库,Emacs 也可以用。但主仓库对子仓库的完整细节缺乏了解。
 
<syntaxhighlight lang="text">
~/.emacs.d/
</syntaxhighlight>
 
视为主仓库,把 Vertico、Consult、Magit 等第三方插件视为独立仓库。
 
使用 submodule 后,主仓库可以记录每个插件的:
 
* 仓库 URL
* 保存路径
* 当前锁定的准确 commit
 
这样就能对整套插件环境进行精确控制、整体回滚和异地复现。
 
== 核心原理 ==
 
=== 普通 git clone 为什么不够 ===
 
假设已经在 <code>~/.emacs.d/</code> 建立了 Git 仓库,然后直接执行:
 
<syntaxhighlight lang="bash">
cd ~/.emacs.d/travel/packages
git clone https://github.com/minad/vertico.git
</syntaxhighlight>
 
目录会变成:
 
<syntaxhighlight lang="text">
~/.emacs.d/                    主仓库
└── travel/packages/
    └── vertico/              另一个独立 Git 仓库
        └── .git/
</syntaxhighlight>
 
Vertico 当然可以正常运行。Emacs 并不关心插件是通过哪种方式下载的,只要源码位于 <code>load-path</code> 中即可。
 
但对外层的 <code>~/.emacs.d</code> 主仓库而言,这个嵌套仓库只是一个未经正式登记的外部目录。主仓库没有完整记录:


而 git submodule add 可以解决
* Vertico 从哪个 URL 下载
* Vertico 从哪个 URL 下载
* 应该保存在哪个目录
* 应该保存在哪个目录
Line 49: Line 14:
* 主配置回滚时,Vertico 应该回到哪个版本
* 主配置回滚时,Vertico 应该回到哪个版本


普通 <code>git clone</code> 解决的是:
.gitmodules 内容大致如下:
 
:把插件下载到当前电脑。
 
Git submodule 解决的是:
 
:让主仓库正式声明:本项目依赖这个外部仓库,它应放在这个路径,并固定使用这个准确版本。
 
=== submodule 到底记录了什么 ===
 
执行:
 
<syntaxhighlight lang="bash">
git submodule add \
  https://github.com/minad/vertico.git \
  travel/packages/vertico
</syntaxhighlight>
 
Git 会记录两类信息。
 
==== .gitmodules:记录 URL 和路径 ====
 
主仓库根目录会出现:
 
<syntaxhighlight lang="text">
.gitmodules
</syntaxhighlight>
 
内容大致如下:


<syntaxhighlight lang="ini">
<syntaxhighlight lang="ini">
Line 85: Line 22:
</syntaxhighlight>
</syntaxhighlight>


它记录:
submodule 还会记录一条指向某个 commit 的 gitlink。
 
<syntaxhighlight lang="text">
插件 URL: https://github.com/minad/vertico.git
保存位置: travel/packages/vertico
</syntaxhighlight>
 
<code>.gitmodules</code> 是主仓库里的普通文本文件,应当提交进 Git。
 
==== gitlink:记录准确 commit ====
 
主仓库不会把 Vertico 的所有源码文件当成自己的文件追踪,而是在:
 
<syntaxhighlight lang="text">
travel/packages/vertico
</syntaxhighlight>
 
这个路径上记录一个特殊条目,指向 Vertico 的某个 commit。
 
例如:
 
<syntaxhighlight lang="text">
Vertico → 9f84c31
</syntaxhighlight>
 
这类特殊引用通常称为 <code>gitlink</code>。
 
因此,主仓库记录的是:
 
:当前这套 Emacs 配置应当使用 Vertico <code>9f84c31</code> 版本。
 
插件的完整开发历史仍然保存在 Vertico 自己的仓库里,并不会被复制进主仓库。
 
=== 主仓库和插件仓库的职责分工 ===


{|class="wikitable"
{|class="wikitable"
Line 142: Line 46:
# 又能让配置仓库精确声明自己依赖哪个插件版本
# 又能让配置仓库精确声明自己依赖哪个插件版本


=== submodule 最重要的价值 ===
submodule 最重要的价值
# 固定插件版本,它记录的不是模糊的“使用最新版”,而是,这套配置经过测试时,各插件分别停留在哪个准确版本。这相当于给插件环境加了一把版本锁。
# 整体回滚。主仓库回滚到 abc1234,子仓库也会恢复到当时的状态。普通嵌套 <code>git clone</code> 做不到这一点。即使主配置回到一个月前,插件仍可能停留在今天的新版本。
# 异地完整复现。<code>git clone --recurse-submodules URL PATH</code>,恢复出来的插件环境与原电脑一致。
# 明确记录插件升级。使用 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


<syntaxhighlight lang="text">
不要修改插件原始仓库。不应把自己的配置写进子仓库。
这版 Emacs 配置
├── Vertico:commit A
├── Consult:commit B
└── Magit:commit C
</syntaxhighlight>


它记录的不是模糊的“使用最新版”,而是:
=== 查看 ==
# git -C travel/packages/vertico log --oneline --decorate -10


:这套配置经过测试时,各插件分别停留在哪个准确版本。
=== 升级 ===
 
git submodule update --remote -- travel/packages/vertico
这相当于给插件环境加了一把版本锁。
 
==== 整体回滚 ====
 
假设主仓库中有两个提交:
 
<syntaxhighlight lang="text">
提交 A:
Vertico = abc1234
 
提交 B:
Vertico = def5678 </syntaxhighlight>
 
Vertico 升级后出现问题,把主仓库恢复到提交 A,再执行:
 
<syntaxhighlight lang="bash">
git submodule update --init --recursive
</syntaxhighlight>
 
Vertico 就会重新回到:
 
<syntaxhighlight lang="text">
abc1234
</syntaxhighlight>
 
因此,回滚的不只是自己的 <code>.el</code> 配置,还包括与当时配置配套的插件版本。
 
普通嵌套 <code>git clone</code> 做不到这一点。即使主配置回到一个月前,插件仍可能停留在今天的新版本。
 
==== 异地完整复现 ====
 
在另一台电脑执行:
 
<syntaxhighlight lang="bash">
git clone --recurse-submodules \
  主仓库URL \
  ~/.emacs.d
</syntaxhighlight>
 
Git 会自动知道:
 
* 去哪个 URL 下载插件
* 把插件放在哪个目录
* 将插件切换到哪个 commit
 
恢复出来的插件环境与原电脑一致。
 
==== 明确记录插件升级 ====
 
使用 submodule 后,升级插件会被主仓库识别为一次正式变化:
 
<syntaxhighlight lang="text">
Vertico 原来:abc1234
Vertico 现在:def5678
</syntaxhighlight>
 
测试成功后可以提交:
 
<syntaxhighlight lang="bash">
git add travel/packages/vertico
git commit -m "Update Vertico"
</syntaxhighlight>
 
这让插件升级变成一种:
 
* 可查看
* 可测试
* 可提交
* 可撤销
 
的配置变化。
 
== 目录设计与初始化 ==
 
=== 推荐的 Emacs 目录结构 ===
 
第三方插件源码与自己的配置文件最好分开:
 
<syntaxhighlight lang="text">
~/.emacs.d/
├── .git/
├── .gitmodules
├── init.el
└── travel/
    ├── packages/
    │  ├── vertico/          Git submodule
    │  ├── consult/          Git submodule
    │  ├── orderless/        Git submodule
    │  └── magit/            Git submodule
    │
    ├── 10-qlzq/
    ├── 20-stable/
    │  └── init-vertico.el
    ├── 30-maybe/
    └── 40-test/
        └── init-consult.el
</syntaxhighlight>
 
其中:
 
<syntaxhighlight lang="text">
travel/packages/
</syntaxhighlight>
 
专门保存第三方插件原始仓库。
 
而:
 
<syntaxhighlight lang="text">
10-qlzq
20-stable
30-maybe
40-test
</syntaxhighlight>
 
保存自己写的配置文件,并表达对配置的理解程度和信任状态。
 
插件源码不必随着状态晋级而移动。例如:
 
<syntaxhighlight lang="text">
travel/packages/vertico/
</syntaxhighlight>
 
始终保持不动。
 
只有自己的配置文件移动:
 
<syntaxhighlight lang="text">
travel/40-test/init-vertico.el
→ travel/30-maybe/init-vertico.el
→ travel/20-stable/init-vertico.el
</syntaxhighlight>
 
这样不会反复修改 submodule 路径和 <code>.gitmodules</code>。
 
=== 初始化主仓库 ===
 
在整个 Emacs 配置目录建立主仓库:
 
<syntaxhighlight lang="bash">
cd ~/.emacs.d
git init -b main
</syntaxhighlight>
 
先提交自己的基础配置:
 
<syntaxhighlight lang="bash">
git add init.el travel
git commit -m "Initialize Emacs configuration"
</syntaxhighlight>
 
仓库建在:
 
<syntaxhighlight lang="text">
~/.emacs.d/
</syntaxhighlight>
 
而不是只建在:
 
<syntaxhighlight lang="text">
~/.emacs.d/travel/
</syntaxhighlight>
 
这样 <code>init.el</code> 和所有配置都能统一管理。
 
=== 添加插件 ===
 
以 Vertico 为例:
 
<syntaxhighlight lang="bash">
cd ~/.emacs.d
 
git submodule add
https://github.com/minad/vertico.git
travel/packages/vertico </syntaxhighlight>
 
检查状态:
 
<syntaxhighlight lang="bash">
git status
</syntaxhighlight>
 
一般会看到:
 
<syntaxhighlight lang="text">
new file:  .gitmodules
new file:  travel/packages/vertico
</syntaxhighlight>
 
提交:
 
<syntaxhighlight lang="bash">
git commit -m "Add Vertico submodule"
</syntaxhighlight>
 
<code>git submodule add</code> 本质上完成了三件事:
 
# 克隆插件仓库
 
# 把 URL 和路径写进 <code>.gitmodules</code>
 
# 让主仓库记录插件当前所在的 commit
 
=== 不要修改插件原始仓库 ===
 
不应把自己的配置写进:
 
<syntaxhighlight lang="text">
travel/packages/vertico/
</syntaxhighlight>
 
例如不要这样:
 
<syntaxhighlight lang="text">
travel/packages/vertico/
├── vertico.el
├── vertico-indexed.el
└── my-vertico-config.el
</syntaxhighlight>
 
因为这个目录属于 Vertico 自己的仓库。自行修改后,子模块会变成脏状态:
 
<syntaxhighlight lang="text">
modified content
</syntaxhighlight>
 
正确做法是:
 
<syntaxhighlight lang="text">
travel/packages/vertico/        插件原始源码
travel/40-test/init-vertico.el  自己的配置
</syntaxhighlight>
 
例如:
 
<syntaxhighlight lang="elisp">
(require 'vertico)
 
(vertico-mode 1) </syntaxhighlight>
 
原则是:
 
:插件仓库保持原汁原味;自己的 <code>require</code>、<code>setq</code>、hook、advice 和按键配置放在自己的配置目录。
 
== 插件升级、回滚与删除 ==
 
=== 查看所有插件版本 ===
 
在主仓库运行:
 
<syntaxhighlight lang="bash">
git submodule status
</syntaxhighlight>
 
可能显示:
 
<syntaxhighlight lang="text">
abc1234 travel/packages/vertico
def5678 travel/packages/consult
</syntaxhighlight>
 
前面的 commit 就是主仓库当前锁定的插件版本。
 
还可以设置更清楚的 submodule 差异显示:
 
<syntaxhighlight lang="bash">
git config diff.submodule log
git config status.submodulesummary 1
</syntaxhighlight>
 
以后运行:
 
<syntaxhighlight lang="bash">
git diff
</syntaxhighlight>
 
便能更直观地看到插件从哪些提交升级到了哪些提交。
 
查看某个插件自己的提交历史:
 
<syntaxhighlight lang="bash">
git -C travel/packages/vertico log --oneline --decorate -10
</syntaxhighlight>
 
这等价于先进入插件目录再执行 <code>git log</code>,但无需切换当前目录。
 
=== 升级一个插件 ===
 
推荐一次只升级一个插件:
 
<syntaxhighlight lang="bash">
cd ~/.emacs.d
 
git submodule update --remote -- travel/packages/vertico </syntaxhighlight>
 
此时 Vertico 会获取上游更新,并切换到远程跟踪分支的新提交。
 
主仓库会显示:
 
<syntaxhighlight lang="text">
modified: travel/packages/vertico
</syntaxhighlight>
 
这里的 <code>modified</code> 通常不是插件源码被改写了,而是:
 
<syntaxhighlight lang="text">
主仓库原来记录:Vertico commit A
插件现在位于:  Vertico commit B
</syntaxhighlight>
 
查看升级内容:
 
<syntaxhighlight lang="bash">
git diff --submodule=log
git diff --submodule=log
</syntaxhighlight>
然后正常使用几天,观察是否稳定。
=== 升级成功后保留新版本 ===
确认没有问题后:
<syntaxhighlight lang="bash">
git add travel/packages/vertico
git commit -m "Update Vertico"
</syntaxhighlight>
主仓库便正式从:
<syntaxhighlight lang="text">
Vertico commit A
</syntaxhighlight>
改为:
<syntaxhighlight lang="text">
Vertico commit B
</syntaxhighlight>
最好坚持:
:一次升级一个插件,一个插件形成一个独立提交。
例如:
<syntaxhighlight lang="text">
Update Vertico
Update Consult
Update Magit
</syntaxhighlight>


不要一次升级十几个插件后统一提交为:
=== 保留 ===
# git add travel/packages/vertico
# git commit -m "Update Vertico" ;; commit 了才正式获得了主仓库的承认


<syntaxhighlight lang="text">
一次升级一个插件,一个插件形成一个独立提交。
Update packages
</syntaxhighlight>
 
否则出问题时,很难判断究竟是哪一个插件造成的。


=== 放弃尚未提交的插件升级 ===
=== 放弃尚未提交的插件升级 ===


=== 未提交+放弃 ===
如果执行升级后发现新版本有问题,而且还没有提交:
如果执行升级后发现新版本有问题,而且还没有提交:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
git submodule update \
git submodule update \
Line 527: Line 89:
   --force \
   --force \
   -- travel/packages/vertico
   -- travel/packages/vertico
</syntaxhighlight>
Git 会让插件回到主仓库当前记录的旧 commit。
含义是:
<syntaxhighlight lang="text">
主仓库固定版本:commit A
刚才临时升级到:commit B
执行恢复后:    commit A
</syntaxhighlight>
</syntaxhighlight>


这就是最简单的“升级失败,退回旧版”。
这就是最简单的“升级失败,退回旧版”。


=== 升级已经提交,后来才发现问题 ===
=== 已提交+放弃 ===
 
假设升级 Vertico 的提交是:7ac39fe Update Vertico
假设升级 Vertico 的提交是:
 
<syntaxhighlight lang="text">
7ac39fe Update Vertico
</syntaxhighlight>


可以在主仓库运行:
可以在主仓库运行:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
git revert 7ac39fe
git revert 7ac39fe
Line 558: Line 104:
<code>git revert</code> 会新增一个提交,撤销那次插件升级;随后 <code>git submodule update</code> 让磁盘上的插件切回旧 commit。
<code>git revert</code> 会新增一个提交,撤销那次插件升级;随后 <code>git submodule update</code> 让磁盘上的插件切回旧 commit。


这种做法比随意修改子仓库历史更清楚,因为:
这种做法比随意修改子仓库历史更清楚,因为决定插件正式版本的是主仓库中的 gitlink,而不是子仓库当前碰巧停留在哪里。


:决定插件正式版本的是主仓库中的 gitlink,而不是子仓库当前碰巧停留在哪里。
=== detached ===
 
git status 时发现 submodule 经常处于 detached HEAD?
=== 为什么 submodule 经常处于 detached HEAD ===
 
进入子模块后,运行:
 
<syntaxhighlight lang="bash">
git status
</syntaxhighlight>
 
可能看到:
 
<syntaxhighlight lang="text">
HEAD detached at abc1234
</syntaxhighlight>


这在 submodule 中通常是正常现象。
这在 submodule 中通常是正常现象。


主仓库需要插件准确停在:
主仓库需要插件准确停在某个 commit,而不是随时变化。对只使用插件、不直接参与开发插件的人而言,这不是故障,而是版本锁定的正常状态。
 
<syntaxhighlight lang="text">
abc1234
</syntaxhighlight>
 
而不是自动跟随 <code>main</code>、<code>master</code> 等分支继续变化。


因此,submodule 默认常常直接检出某个 commit,形成 detached HEAD。
submodule 应该少升级、慢升级、一次一个、稳定后再提交。submodule 最大的价值不是方便追逐最新版,而是方便长期固定已验证版本。


对只使用插件、不直接参与开发插件的人而言,这不是故障,而是版本锁定的正常状态。
=== 删除 ===
 
# git rm travel/packages/vertico
=== 是否一次升级全部插件 ===
# git rm travel/40-test/init-vertico.el
 
# git commit -m "Remove Vertico" </syntaxhighlight>
技术上可以一次升级所有子模块:
 
<syntaxhighlight lang="bash">
git submodule update --remote --recursive
</syntaxhighlight>
 
但不建议日常这样做。
 
如果十几个插件同时升级后 Emacs 出问题,很难立即判断是:
 
* Vertico
* Consult
* Orderless
* Magit
* Transient
* 多个插件之间的兼容性变化
 
更稳妥的办法是:
 
<syntaxhighlight lang="bash">
git submodule update --remote -- travel/packages/vertico
</syntaxhighlight>
 
测试、提交,再升级下一个插件。
 
适合这种管理方式的原则是:
 
:少升级、慢升级、一次一个、稳定后再提交。
 
submodule 最大的价值不是方便追逐最新版,而是方便长期固定已验证版本。
 
=== 删除一个插件 ===
 
==== 永久删除 submodule ====
 
例如删除 Vertico:
 
<syntaxhighlight lang="bash">
cd ~/.emacs.d
 
git rm travel/packages/vertico
git commit -m "Remove Vertico" </syntaxhighlight>
 
同时删除自己的插件配置:
 
<syntaxhighlight lang="bash">
git rm travel/40-test/init-vertico.el
git commit -m "Remove Vertico configuration"
</syntaxhighlight>
 
也可以一次完成:
 
<syntaxhighlight lang="bash">
git rm travel/packages/vertico
git rm travel/40-test/init-vertico.el
git commit -m "Remove Vertico"
</syntaxhighlight>


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


==== 只在当前电脑暂时停用 ====
=== 暂停 ====
# git submodule deinit -- travel/packages/vertico


<syntaxhighlight lang="bash">
=== 不暂停 ===
git submodule deinit -- travel/packages/vertico
# git submodule update --init -- travel/packages/vertico
</syntaxhighlight>
 
这不会从主仓库历史中删除 Vertico,只是取消当前电脑上的初始化和工作目录。
 
重新恢复:
 
<syntaxhighlight lang="bash">
git submodule update --init -- travel/packages/vertico
</syntaxhighlight>
 
两者区别如下:
 
{|class="wikitable"
! 命令
 
! 含义
|-
|<code>git submodule deinit</code>
|当前电脑暂时不检出插件
|-
|<code>git rm</code>
|从主仓库中正式删除插件依赖
|}
 
== 异地恢复与日常同步 ==
 
=== 在另一台电脑恢复全部配置 ===
 
==== 克隆时直接取得所有子模块 ====


== 移机 ==
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
;; 一次性移机主仓库和子仓库
git clone --recurse-submodules \
git clone --recurse-submodules \
   主仓库URL \
   主仓库URL \
Line 692: Line 136:
</syntaxhighlight>
</syntaxhighlight>


这样主仓库和所有插件会一次性恢复。
或者,
 
==== 已经普通 clone 过主仓库 ====
 
如果已经执行:
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
;; 先移机主仓库
;; 再移机子仓库
git clone 主仓库URL ~/.emacs.d
git clone 主仓库URL ~/.emacs.d
</syntaxhighlight>
插件目录可能存在,但内容尚未初始化。
继续执行:
<syntaxhighlight lang="bash">
cd ~/.emacs.d
git submodule update --init --recursive
git submodule update --init --recursive
</syntaxhighlight>
</syntaxhighlight>


其中:
=== 日常更新 ===
 
* <code>--init</code>:初始化尚未初始化的子模块
* <code>--recursive</code>:如果插件内部还有子模块,也继续处理
 
=== 日常拉取主仓库更新 ===
 
在另一台电脑同步配置时:
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
cd ~/.emacs.d
git pull
git pull
git submodule update --init --recursive </syntaxhighlight>
git submodule update --init --recursive </syntaxhighlight>
 
</syntaxhighlight>
第一条命令更新:
 
* 自己的配置文件
* <code>.gitmodules</code>
* 插件版本指针


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


单纯运行:
单纯运行第一条命令不太够。
 
<syntaxhighlight lang="bash">
git pull
</syntaxhighlight>
 
只会更新主仓库中记录的指针,并不保证子模块工作目录已经切换到对应 commit。
 
== 使用边界与方案选择 ==
 
=== 临时测试是否必须使用 submodule ===
 
不一定。
 
如果只是临时试用一个插件,完全可以:
 
<syntaxhighlight lang="bash">
git clone URL travel/40-test/plugin
</syntaxhighlight>
 
不喜欢就直接删除:
 
<syntaxhighlight lang="bash">
rm -rf travel/40-test/plugin
</syntaxhighlight>
 
以下情况普通 clone 已经足够:
 
* 只试用几分钟或几天
* 随时准备删除
* 不打算同步到其他电脑
* 不在乎固定版本
* 不需要主配置与插件整体回滚
 
当决定长期保留后,再正式转为 submodule:
 
<syntaxhighlight lang="bash">
rm -rf travel/40-test/plugin
 
git submodule add URL travel/packages/plugin
git commit -m "Add plugin submodule" </syntaxhighlight>
 
当然,也可以从试用阶段就使用 submodule。删除 submodule 并不困难,只是 Git 历史中会留下加入和删除记录。
 
=== submodule 不会帮忙完成什么 ===


== 边界 ==
Git submodule 只负责源码仓库和版本关系,不是完整的 Emacs 包管理器。
Git submodule 只负责源码仓库和版本关系,不是完整的 Emacs 包管理器。


它不会自动完成:
它不会自动完成:
* 把插件加入 <code>load-path</code>
* 把插件加入 <code>load-path</code>
* 执行 <code>require</code>
* 执行 <code>require</code>
Line 797: Line 171:


需要自己继续添加依赖:
需要自己继续添加依赖:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
git submodule add 依赖仓库URL travel/packages/依赖名
git submodule add 依赖仓库URL travel/packages/依赖名
Line 804: Line 177:
然后自己配置加载关系。
然后自己配置加载关系。


这套方案的特点是:
Git submodule 的本质是,放弃包管理器的一部分自动化,换取透明、固定、可控和可复现。对于大量、频繁更新的插件,手工管理会比较繁琐;对于少量精选、长期固定、谨慎升级的插件,非常合适。
 
:放弃包管理器的一部分自动化,换取透明、固定、可控和可复现。
 
对于大量、频繁更新的插件,手工管理会比较繁琐;对于少量精选、长期固定、谨慎升级的插件,非常合适。
 
=== 普通 clone 与 submodule 对比 ===


== 对比 ==
{|class="wikitable"
{|class="wikitable"
! 能力
! 能力
Line 854: Line 222:
|}
|}


最核心的区别是:
最核心的区别是,普通 clone 插件只是碰巧存在于这台电脑。Git submodule,插件是主项目正式声明的一项依赖。
 
<syntaxhighlight lang="text">
普通 clone:
插件只是碰巧存在于这台电脑。
 
Git submodule:
插件是主项目正式声明的一项依赖。 </syntaxhighlight>
 
== 命令速查与完整工作流 ==
 
=== 常用命令速查 ===


== 工作流 ==
{|class="wikitable"
{|class="wikitable"
! 目的
! 目的
Line 905: Line 263:
|}
|}


=== 添加插件 ===
<syntaxhighlight lang="bash">
cd ~/.emacs.d
git submodule add
插件URL
travel/packages/插件名
git commit -m "Add 插件名" </syntaxhighlight>


=== 升级插件 ===


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">

Revision as of 20:49, 22 July 2026

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

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