Git submodule 的精准用法: Difference between revisions
Created page with "# 用 Git Submodule 管理 Emacs 插件 Git submodule 适合这种情况: > 一个主项目依赖若干外部 Git 仓库,但又希望这些外部仓库继续保持独立,并且由主项目精确指定它们的版本。 在我的 Emacs 配置中: ```text ~/.emacs.d/ ``` 是主仓库;Vertico、Consult、Magit 等第三方插件是独立仓库。使用 submodule 后,主仓库能够记录每个插件的: * 仓库 URL; * 保存路径; * 当前锁定的..." |
No edit summary |
||
| Line 1: | Line 1: | ||
Git submodule 适合这种情况: | Git submodule 适合这种情况: | ||
:一个主项目依赖若干外部 Git 仓库,但又希望这些外部仓库继续保持独立,并且由主项目精确指定它们的版本。 | |||
在 Emacs 配置中,可以把: | |||
<syntaxhighlight lang="text"> | |||
~/.emacs.d/ | ~/.emacs.d/ | ||
</syntaxhighlight> | |||
视为主仓库,把 Vertico、Consult、Magit 等第三方插件视为独立仓库。 | |||
* 仓库 | 使用 submodule 后,主仓库可以记录每个插件的: | ||
* | |||
* 当前锁定的准确 | * 仓库 URL | ||
* 保存路径 | |||
* 当前锁定的准确 commit | |||
这样就能对整套插件环境进行精确控制、整体回滚和异地复现。 | 这样就能对整套插件环境进行精确控制、整体回滚和异地复现。 | ||
== 核心原理 == | |||
=== 普通 git clone 为什么不够 === | |||
假设已经在 | 假设已经在 <code>~/.emacs.d/</code> 建立了 Git 仓库,然后直接执行: | ||
<syntaxhighlight lang="bash"> | |||
cd ~/.emacs.d/travel/packages | cd ~/.emacs.d/travel/packages | ||
git clone https://github.com/minad/vertico.git | git clone https://github.com/minad/vertico.git | ||
</syntaxhighlight> | |||
目录会变成: | 目录会变成: | ||
<syntaxhighlight lang="text"> | |||
~/.emacs.d/ 主仓库 | ~/.emacs.d/ 主仓库 | ||
└── travel/packages/ | └── travel/packages/ | ||
└── vertico/ 另一个独立 Git 仓库 | └── vertico/ 另一个独立 Git 仓库 | ||
└── .git/ | └── .git/ | ||
</syntaxhighlight> | |||
Vertico 当然可以正常运行。Emacs 并不关心插件是通过哪种方式下载的,只要源码位于 | Vertico 当然可以正常运行。Emacs 并不关心插件是通过哪种方式下载的,只要源码位于 <code>load-path</code> 中即可。 | ||
但对外层的 | 但对外层的 <code>~/.emacs.d</code> 主仓库而言,这个嵌套仓库只是一个未经正式登记的外部目录。主仓库没有完整记录: | ||
* Vertico 从哪个 URL | * Vertico 从哪个 URL 下载 | ||
* | * 应该保存在哪个目录 | ||
* 应该使用哪个 | * 应该使用哪个 commit | ||
* | * 换电脑后应该怎样恢复 | ||
* 主配置回滚时,Vertico | * 主配置回滚时,Vertico 应该回到哪个版本 | ||
普通 | 普通 <code>git clone</code> 解决的是: | ||
:把插件下载到当前电脑。 | |||
Git submodule 解决的是: | Git submodule 解决的是: | ||
:让主仓库正式声明:本项目依赖这个外部仓库,它应放在这个路径,并固定使用这个准确版本。 | |||
=== submodule 到底记录了什么 === | |||
执行: | 执行: | ||
<syntaxhighlight lang="bash"> | |||
git submodule add \ | git submodule add \ | ||
https://github.com/minad/vertico.git \ | https://github.com/minad/vertico.git \ | ||
travel/packages/vertico | travel/packages/vertico | ||
</syntaxhighlight> | |||
Git 会记录两类信息。 | Git 会记录两类信息。 | ||
==== .gitmodules:记录 URL 和路径 ==== | |||
主仓库根目录会出现: | 主仓库根目录会出现: | ||
<syntaxhighlight lang="text"> | |||
.gitmodules | .gitmodules | ||
</syntaxhighlight> | |||
内容大致如下: | 内容大致如下: | ||
<syntaxhighlight lang="ini"> | |||
[submodule "travel/packages/vertico"] | [submodule "travel/packages/vertico"] | ||
path = travel/packages/vertico | path = travel/packages/vertico | ||
url = https://github.com/minad/vertico.git | url = https://github.com/minad/vertico.git | ||
</syntaxhighlight> | |||
它记录: | 它记录: | ||
<syntaxhighlight lang="text"> | |||
插件 URL: https://github.com/minad/vertico.git | 插件 URL: https://github.com/minad/vertico.git | ||
保存位置: travel/packages/vertico | 保存位置: travel/packages/vertico | ||
</syntaxhighlight> | |||
<code>.gitmodules</code> 是主仓库里的普通文本文件,应当提交进 Git。 | |||
==== gitlink:记录准确 commit ==== | |||
主仓库不会把 Vertico 的所有源码文件当成自己的文件追踪,而是在: | 主仓库不会把 Vertico 的所有源码文件当成自己的文件追踪,而是在: | ||
<syntaxhighlight lang="text"> | |||
travel/packages/vertico | travel/packages/vertico | ||
</syntaxhighlight> | |||
这个路径上记录一个特殊条目,指向 Vertico 的某个 commit。 | 这个路径上记录一个特殊条目,指向 Vertico 的某个 commit。 | ||
| Line 108: | Line 106: | ||
例如: | 例如: | ||
<syntaxhighlight lang="text"> | |||
Vertico → 9f84c31 | Vertico → 9f84c31 | ||
</syntaxhighlight> | |||
这类特殊引用通常称为 | 这类特殊引用通常称为 <code>gitlink</code>。 | ||
因此,主仓库记录的是: | 因此,主仓库记录的是: | ||
:当前这套 Emacs 配置应当使用 Vertico 的 <code>9f84c31</code> 版本。 | |||
插件的完整开发历史仍然保存在 Vertico | 插件的完整开发历史仍然保存在 Vertico 自己的仓库里,并不会被复制进主仓库。 | ||
=== 主仓库和插件仓库的职责分工 === | |||
{| class="wikitable" | |||
! 仓库 | |||
| | | ! 负责记录什么 | | ||
| ----------------------------- | | |||
| | | <code>~/.emacs.d</code> 主仓库 | | ||
| Vertico 子仓库 | | 自己的配置文件,以及每个插件应使用哪个 commit | | ||
| Consult 子仓库 | | - | | ||
| Magit 子仓库 | | Vertico 子仓库 | | ||
| Vertico 源码和 Vertico 自己的完整开发历史 | | |||
| - | | |||
| Consult 子仓库 | | |||
| Consult 源码和 Consult 自己的完整开发历史 | | |||
| - | | |||
| Magit 子仓库 | | |||
| Magit 源码和 Magit 自己的完整开发历史 | | |||
| } | | |||
这种设计避免了两个问题: | 这种设计避免了两个问题: | ||
# 不把别人写的几百个插件源码文件伪装成自己的文件 | |||
# 又能让配置仓库精确声明自己依赖哪个插件版本 | |||
=== submodule 最重要的价值 === | |||
==== 固定插件版本 ==== | |||
主仓库可以准确表达: | 主仓库可以准确表达: | ||
<syntaxhighlight lang="text"> | |||
这版 Emacs 配置 | 这版 Emacs 配置 | ||
├── Vertico:commit A | ├── Vertico:commit A | ||
├── Consult:commit B | ├── Consult:commit B | ||
└── Magit:commit C | └── Magit:commit C | ||
</syntaxhighlight> | |||
它记录的不是模糊的“使用最新版”,而是: | 它记录的不是模糊的“使用最新版”,而是: | ||
:这套配置经过测试时,各插件分别停留在哪个准确版本。 | |||
这相当于给插件环境加了一把版本锁。 | 这相当于给插件环境加了一把版本锁。 | ||
==== 整体回滚 ==== | |||
假设主仓库中有两个提交: | 假设主仓库中有两个提交: | ||
<syntaxhighlight lang="text"> | |||
提交 A: | 提交 A: | ||
Vertico = abc1234 | Vertico = abc1234 | ||
提交 B: | 提交 B: | ||
Vertico = def5678 | Vertico = def5678 </syntaxhighlight> | ||
Vertico 升级后出现问题,把主仓库恢复到提交 A,再执行: | Vertico 升级后出现问题,把主仓库恢复到提交 A,再执行: | ||
<syntaxhighlight lang="bash"> | |||
git submodule update --init --recursive | git submodule update --init --recursive | ||
</syntaxhighlight> | |||
Vertico 就会重新回到: | Vertico 就会重新回到: | ||
<syntaxhighlight lang="text"> | |||
abc1234 | abc1234 | ||
</syntaxhighlight> | |||
因此,回滚的不只是自己的 | 因此,回滚的不只是自己的 <code>.el</code> 配置,还包括与当时配置配套的插件版本。 | ||
普通嵌套 | 普通嵌套 <code>git clone</code> 做不到这一点。即使主配置回到一个月前,插件仍可能停留在今天的新版本。 | ||
==== 异地完整复现 ==== | |||
在另一台电脑执行: | 在另一台电脑执行: | ||
<syntaxhighlight lang="bash"> | |||
git clone --recurse-submodules \ | git clone --recurse-submodules \ | ||
主仓库URL \ | 主仓库URL \ | ||
~/.emacs.d | ~/.emacs.d | ||
</syntaxhighlight> | |||
Git 会自动知道: | Git 会自动知道: | ||
* 去哪个 URL | * 去哪个 URL 下载插件 | ||
* | * 把插件放在哪个目录 | ||
* 将插件切换到哪个 | * 将插件切换到哪个 commit | ||
恢复出来的插件环境与原电脑一致。 | 恢复出来的插件环境与原电脑一致。 | ||
==== 明确记录插件升级 ==== | |||
使用 submodule 后,升级插件会被主仓库识别为一次正式变化: | 使用 submodule 后,升级插件会被主仓库识别为一次正式变化: | ||
<syntaxhighlight lang="text"> | |||
Vertico 原来:abc1234 | Vertico 原来:abc1234 | ||
Vertico 现在:def5678 | Vertico 现在:def5678 | ||
</syntaxhighlight> | |||
测试成功后可以提交: | 测试成功后可以提交: | ||
<syntaxhighlight lang="bash"> | |||
git add travel/packages/vertico | git add travel/packages/vertico | ||
git commit -m "Update Vertico" | git commit -m "Update Vertico" | ||
</syntaxhighlight> | |||
这让插件升级变成一种: | 这让插件升级变成一种: | ||
* | * 可查看 | ||
* | * 可测试 | ||
* | * 可提交 | ||
* | * 可撤销 | ||
的配置变化。 | 的配置变化。 | ||
== 目录设计与初始化 == | |||
=== 推荐的 Emacs 目录结构 === | |||
第三方插件源码与自己的配置文件最好分开: | 第三方插件源码与自己的配置文件最好分开: | ||
<syntaxhighlight lang="text"> | |||
~/.emacs.d/ | ~/.emacs.d/ | ||
├── .git/ | ├── .git/ | ||
| Line 252: | Line 257: | ||
└── 40-test/ | └── 40-test/ | ||
└── init-consult.el | └── init-consult.el | ||
</syntaxhighlight> | |||
其中: | 其中: | ||
<syntaxhighlight lang="text"> | |||
travel/packages/ | travel/packages/ | ||
</syntaxhighlight> | |||
专门保存第三方插件原始仓库。 | 专门保存第三方插件原始仓库。 | ||
| Line 264: | Line 269: | ||
而: | 而: | ||
<syntaxhighlight lang="text"> | |||
10-qlzq | 10-qlzq | ||
20-stable | 20-stable | ||
30-maybe | 30-maybe | ||
40-test | 40-test | ||
</syntaxhighlight> | |||
保存自己写的配置文件,并表达对配置的理解程度和信任状态。 | |||
插件源码不必随着状态晋级而移动。例如: | 插件源码不必随着状态晋级而移动。例如: | ||
<syntaxhighlight lang="text"> | |||
travel/packages/vertico/ | travel/packages/vertico/ | ||
</syntaxhighlight> | |||
始终保持不动。 | 始终保持不动。 | ||
只有自己的配置文件移动: | |||
<syntaxhighlight lang="text"> | |||
travel/40-test/init-vertico.el | travel/40-test/init-vertico.el | ||
→ travel/30-maybe/init-vertico.el | → travel/30-maybe/init-vertico.el | ||
→ travel/20-stable/init-vertico.el | → travel/20-stable/init-vertico.el | ||
</syntaxhighlight> | |||
这样不会反复修改 submodule 路径和 <code>.gitmodules</code>。 | |||
=== 初始化主仓库 === | |||
在整个 Emacs 配置目录建立主仓库: | 在整个 Emacs 配置目录建立主仓库: | ||
<syntaxhighlight lang="bash"> | |||
cd ~/.emacs.d | cd ~/.emacs.d | ||
git init -b main | git init -b main | ||
</syntaxhighlight> | |||
先提交自己的基础配置: | 先提交自己的基础配置: | ||
<syntaxhighlight lang="bash"> | |||
git add init.el travel | git add init.el travel | ||
git commit -m "Initialize Emacs configuration" | git commit -m "Initialize Emacs configuration" | ||
</syntaxhighlight> | |||
仓库建在: | 仓库建在: | ||
<syntaxhighlight lang="text"> | |||
~/.emacs.d/ | ~/.emacs.d/ | ||
</syntaxhighlight> | |||
而不是只建在: | 而不是只建在: | ||
<syntaxhighlight lang="text"> | |||
~/.emacs.d/travel/ | ~/.emacs.d/travel/ | ||
</syntaxhighlight> | |||
这样 | 这样 <code>init.el</code> 和所有配置都能统一管理。 | ||
=== 添加插件 === | |||
以 Vertico 为例: | 以 Vertico 为例: | ||
<syntaxhighlight lang="bash"> | |||
cd ~/.emacs.d | cd ~/.emacs.d | ||
git submodule add | git submodule add | ||
https://github.com/minad/vertico.git | |||
travel/packages/vertico </syntaxhighlight> | |||
检查状态: | 检查状态: | ||
<syntaxhighlight lang="bash"> | |||
git status | git status | ||
</syntaxhighlight> | |||
一般会看到: | 一般会看到: | ||
<syntaxhighlight lang="text"> | |||
new file: .gitmodules | new file: .gitmodules | ||
new file: travel/packages/vertico | new file: travel/packages/vertico | ||
</syntaxhighlight> | |||
提交: | 提交: | ||
<syntaxhighlight lang="bash"> | |||
git commit -m "Add Vertico submodule" | git commit -m "Add Vertico submodule" | ||
</syntaxhighlight> | |||
<code>git submodule add</code> 本质上完成了三件事: | |||
# 克隆插件仓库 | |||
# 把 URL 和路径写进 <code>.gitmodules</code> | |||
# 让主仓库记录插件当前所在的 commit | |||
=== 不要修改插件原始仓库 === | |||
不应把自己的配置写进: | 不应把自己的配置写进: | ||
<syntaxhighlight lang="text"> | |||
travel/packages/vertico/ | travel/packages/vertico/ | ||
</syntaxhighlight> | |||
例如不要这样: | 例如不要这样: | ||
<syntaxhighlight lang="text"> | |||
travel/packages/vertico/ | travel/packages/vertico/ | ||
├── vertico.el | ├── vertico.el | ||
├── vertico-indexed.el | ├── vertico-indexed.el | ||
└── my-vertico-config.el | └── my-vertico-config.el | ||
</syntaxhighlight> | |||
因为这个目录属于 Vertico 自己的仓库。自行修改后,子模块会变成脏状态: | 因为这个目录属于 Vertico 自己的仓库。自行修改后,子模块会变成脏状态: | ||
<syntaxhighlight lang="text"> | |||
modified content | modified content | ||
</syntaxhighlight> | |||
正确做法是: | 正确做法是: | ||
<syntaxhighlight lang="text"> | |||
travel/packages/vertico/ 插件原始源码 | travel/packages/vertico/ 插件原始源码 | ||
travel/40-test/init-vertico.el | travel/40-test/init-vertico.el 自己的配置 | ||
</syntaxhighlight> | |||
例如: | 例如: | ||
<syntaxhighlight lang="elisp"> | |||
(require 'vertico) | (require 'vertico) | ||
(vertico-mode 1) | (vertico-mode 1) </syntaxhighlight> | ||
原则是: | 原则是: | ||
> | :插件仓库保持原汁原味;自己的 <code>require</code>、<code>setq</code>、hook、advice 和按键配置放在自己的配置目录。 | ||
== 插件升级、回滚与删除 == | |||
=== 查看所有插件版本 === | |||
在主仓库运行: | 在主仓库运行: | ||
<syntaxhighlight lang="bash"> | |||
git submodule status | git submodule status | ||
</syntaxhighlight> | |||
可能显示: | 可能显示: | ||
<syntaxhighlight lang="text"> | |||
abc1234 travel/packages/vertico | abc1234 travel/packages/vertico | ||
def5678 travel/packages/consult | def5678 travel/packages/consult | ||
</syntaxhighlight> | |||
前面的 commit 就是主仓库当前锁定的插件版本。 | 前面的 commit 就是主仓库当前锁定的插件版本。 | ||
| Line 427: | Line 426: | ||
还可以设置更清楚的 submodule 差异显示: | 还可以设置更清楚的 submodule 差异显示: | ||
<syntaxhighlight lang="bash"> | |||
git config diff.submodule log | git config diff.submodule log | ||
git config status.submodulesummary 1 | git config status.submodulesummary 1 | ||
</syntaxhighlight> | |||
以后运行: | 以后运行: | ||
<syntaxhighlight lang="bash"> | |||
git diff | git diff | ||
</syntaxhighlight> | |||
便能更直观地看到插件从哪些提交升级到了哪些提交。 | 便能更直观地看到插件从哪些提交升级到了哪些提交。 | ||
| Line 442: | Line 441: | ||
查看某个插件自己的提交历史: | 查看某个插件自己的提交历史: | ||
<syntaxhighlight lang="bash"> | |||
git -C travel/packages/vertico log --oneline --decorate -10 | git -C travel/packages/vertico log --oneline --decorate -10 | ||
</syntaxhighlight> | |||
这等价于先进入插件目录再执行 <code>git log</code>,但无需切换当前目录。 | |||
=== 升级一个插件 === | |||
推荐一次只升级一个插件: | 推荐一次只升级一个插件: | ||
<syntaxhighlight lang="bash"> | |||
cd ~/.emacs.d | cd ~/.emacs.d | ||
git submodule update --remote -- travel/packages/vertico | git submodule update --remote -- travel/packages/vertico </syntaxhighlight> | ||
此时 Vertico 会获取上游更新,并切换到远程跟踪分支的新提交。 | 此时 Vertico 会获取上游更新,并切换到远程跟踪分支的新提交。 | ||
| Line 464: | Line 460: | ||
主仓库会显示: | 主仓库会显示: | ||
<syntaxhighlight lang="text"> | |||
modified: travel/packages/vertico | modified: travel/packages/vertico | ||
</syntaxhighlight> | |||
这里的 | 这里的 <code>modified</code> 通常不是插件源码被改写了,而是: | ||
<syntaxhighlight lang="text"> | |||
主仓库原来记录:Vertico commit A | 主仓库原来记录:Vertico commit A | ||
插件现在位于: Vertico commit B | 插件现在位于: 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 add travel/packages/vertico | ||
git commit -m "Update Vertico" | git commit -m "Update Vertico" | ||
</syntaxhighlight> | |||
主仓库便正式从: | 主仓库便正式从: | ||
<syntaxhighlight lang="text"> | |||
Vertico commit A | Vertico commit A | ||
</syntaxhighlight> | |||
改为: | 改为: | ||
<syntaxhighlight lang="text"> | |||
Vertico commit B | Vertico commit B | ||
</syntaxhighlight> | |||
最好坚持: | 最好坚持: | ||
:一次升级一个插件,一个插件形成一个独立提交。 | |||
例如: | 例如: | ||
<syntaxhighlight lang="text"> | |||
Update Vertico | Update Vertico | ||
Update Consult | Update Consult | ||
Update Magit | Update Magit | ||
</syntaxhighlight> | |||
不要一次升级十几个插件后统一提交为: | 不要一次升级十几个插件后统一提交为: | ||
<syntaxhighlight lang="text"> | |||
Update packages | Update packages | ||
</syntaxhighlight> | |||
否则出问题时,很难判断究竟是哪一个插件造成的。 | 否则出问题时,很难判断究竟是哪一个插件造成的。 | ||
=== 放弃尚未提交的插件升级 === | |||
如果执行升级后发现新版本有问题,而且还没有提交: | 如果执行升级后发现新版本有问题,而且还没有提交: | ||
<syntaxhighlight lang="bash"> | |||
git submodule update \ | git submodule update \ | ||
--checkout \ | --checkout \ | ||
--force \ | --force \ | ||
-- travel/packages/vertico | -- travel/packages/vertico | ||
</syntaxhighlight> | |||
Git 会让插件回到主仓库当前记录的旧 commit。 | Git 会让插件回到主仓库当前记录的旧 commit。 | ||
| Line 543: | Line 535: | ||
含义是: | 含义是: | ||
<syntaxhighlight lang="text"> | |||
主仓库固定版本:commit A | 主仓库固定版本:commit A | ||
刚才临时升级到:commit B | 刚才临时升级到:commit B | ||
执行恢复后: commit A | 执行恢复后: commit A | ||
</syntaxhighlight> | |||
这就是最简单的“升级失败,退回旧版”。 | 这就是最简单的“升级失败,退回旧版”。 | ||
=== 升级已经提交,后来才发现问题 === | |||
假设升级 Vertico 的提交是: | 假设升级 Vertico 的提交是: | ||
<syntaxhighlight lang="text"> | |||
7ac39fe Update Vertico | 7ac39fe Update Vertico | ||
</syntaxhighlight> | |||
可以在主仓库运行: | 可以在主仓库运行: | ||
<syntaxhighlight lang="bash"> | |||
git revert 7ac39fe | git revert 7ac39fe | ||
git submodule update --init --recursive | git submodule update --init --recursive | ||
</syntaxhighlight> | |||
<code>git revert</code> 会新增一个提交,撤销那次插件升级;随后 <code>git submodule update</code> 让磁盘上的插件切回旧 commit。 | |||
这种做法比随意修改子仓库历史更清楚,因为: | 这种做法比随意修改子仓库历史更清楚,因为: | ||
:决定插件正式版本的是主仓库中的 gitlink,而不是子仓库当前碰巧停留在哪里。 | |||
=== 为什么 submodule 经常处于 detached HEAD === | |||
进入子模块后,运行: | 进入子模块后,运行: | ||
<syntaxhighlight lang="bash"> | |||
git status | git status | ||
</syntaxhighlight> | |||
可能看到: | 可能看到: | ||
<syntaxhighlight lang="text"> | |||
HEAD detached at abc1234 | HEAD detached at abc1234 | ||
</syntaxhighlight> | |||
这在 submodule 中通常是正常现象。 | 这在 submodule 中通常是正常现象。 | ||
| Line 594: | Line 582: | ||
主仓库需要插件准确停在: | 主仓库需要插件准确停在: | ||
<syntaxhighlight lang="text"> | |||
abc1234 | abc1234 | ||
</syntaxhighlight> | |||
而不是自动跟随 | 而不是自动跟随 <code>main</code>、<code>master</code> 等分支继续变化。 | ||
因此,submodule 默认常常直接检出某个 commit,形成 detached HEAD。 | 因此,submodule 默认常常直接检出某个 commit,形成 detached HEAD。 | ||
| Line 604: | Line 592: | ||
对只使用插件、不直接参与开发插件的人而言,这不是故障,而是版本锁定的正常状态。 | 对只使用插件、不直接参与开发插件的人而言,这不是故障,而是版本锁定的正常状态。 | ||
=== 是否一次升级全部插件 === | |||
技术上可以一次升级所有子模块: | 技术上可以一次升级所有子模块: | ||
<syntaxhighlight lang="bash"> | |||
git submodule update --remote --recursive | git submodule update --remote --recursive | ||
</syntaxhighlight> | |||
但不建议日常这样做。 | 但不建议日常这样做。 | ||
| Line 618: | Line 604: | ||
如果十几个插件同时升级后 Emacs 出问题,很难立即判断是: | 如果十几个插件同时升级后 Emacs 出问题,很难立即判断是: | ||
* | * Vertico | ||
* | * Consult | ||
* | * Orderless | ||
* | * Magit | ||
* | * Transient | ||
* | * 多个插件之间的兼容性变化 | ||
更稳妥的办法是: | 更稳妥的办法是: | ||
<syntaxhighlight lang="bash"> | |||
git submodule update --remote -- travel/packages/vertico | git submodule update --remote -- travel/packages/vertico | ||
</syntaxhighlight> | |||
测试、提交,再升级下一个插件。 | 测试、提交,再升级下一个插件。 | ||
适合这种管理方式的原则是: | |||
:少升级、慢升级、一次一个、稳定后再提交。 | |||
submodule 最大的价值不是方便追逐最新版,而是方便长期固定已验证版本。 | 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 记录。 | |||
==== 只在当前电脑暂时停用 ==== | |||
<syntaxhighlight lang="bash"> | |||
git submodule deinit -- 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"> | |||
git clone --recurse-submodules \ | git clone --recurse-submodules \ | ||
主仓库URL \ | 主仓库URL \ | ||
~/.emacs.d | ~/.emacs.d | ||
</syntaxhighlight> | |||
这样主仓库和所有插件会一次性恢复。 | 这样主仓库和所有插件会一次性恢复。 | ||
==== 已经普通 clone 过主仓库 ==== | |||
如果已经执行: | 如果已经执行: | ||
<syntaxhighlight lang="bash"> | |||
git clone 主仓库URL ~/.emacs.d | git clone 主仓库URL ~/.emacs.d | ||
</syntaxhighlight> | |||
插件目录可能存在,但内容尚未初始化。 | 插件目录可能存在,但内容尚未初始化。 | ||
| Line 665: | Line 708: | ||
继续执行: | 继续执行: | ||
<syntaxhighlight lang="bash"> | |||
cd ~/.emacs.d | cd ~/.emacs.d | ||
git submodule update --init --recursive | git submodule update --init --recursive | ||
</syntaxhighlight> | |||
其中: | 其中: | ||
* | * <code>--init</code>:初始化尚未初始化的子模块 | ||
* | * <code>--recursive</code>:如果插件内部还有子模块,也继续处理 | ||
=== 日常拉取主仓库更新 === | |||
在另一台电脑同步配置时: | 在另一台电脑同步配置时: | ||
<syntaxhighlight lang="bash"> | |||
cd ~/.emacs.d | cd ~/.emacs.d | ||
git pull | git pull | ||
git submodule update --init --recursive | git submodule update --init --recursive </syntaxhighlight> | ||
第一条命令更新: | 第一条命令更新: | ||
* | * 自己的配置文件 | ||
* | * <code>.gitmodules</code> | ||
* | * 插件版本指针 | ||
第二条命令让本机插件真正切换到主仓库指定的版本。 | 第二条命令让本机插件真正切换到主仓库指定的版本。 | ||
| Line 698: | Line 738: | ||
单纯运行: | 单纯运行: | ||
<syntaxhighlight lang="bash"> | |||
git pull | git pull | ||
</syntaxhighlight> | |||
只会更新主仓库中记录的指针,并不保证子模块工作目录已经切换到对应 commit。 | 只会更新主仓库中记录的指针,并不保证子模块工作目录已经切换到对应 commit。 | ||
== 使用边界与方案选择 == | |||
=== 临时测试是否必须使用 submodule === | |||
不一定。 | 不一定。 | ||
| Line 765: | Line 752: | ||
如果只是临时试用一个插件,完全可以: | 如果只是临时试用一个插件,完全可以: | ||
<syntaxhighlight lang="bash"> | |||
git clone URL travel/40-test/plugin | git clone URL travel/40-test/plugin | ||
</syntaxhighlight> | |||
不喜欢就直接删除: | 不喜欢就直接删除: | ||
<syntaxhighlight lang="bash"> | |||
rm -rf travel/40-test/plugin | rm -rf travel/40-test/plugin | ||
</syntaxhighlight> | |||
以下情况普通 clone 已经足够: | 以下情况普通 clone 已经足够: | ||
* | * 只试用几分钟或几天 | ||
* | * 随时准备删除 | ||
* | * 不打算同步到其他电脑 | ||
* | * 不在乎固定版本 | ||
* | * 不需要主配置与插件整体回滚 | ||
当决定长期保留后,再正式转为 submodule: | 当决定长期保留后,再正式转为 submodule: | ||
<syntaxhighlight lang="bash"> | |||
rm -rf travel/40-test/plugin | rm -rf travel/40-test/plugin | ||
git submodule add URL travel/packages/plugin | git submodule add URL travel/packages/plugin | ||
git commit -m "Add plugin submodule" | git commit -m "Add plugin submodule" </syntaxhighlight> | ||
当然,也可以从试用阶段就使用 submodule。删除 submodule 并不困难,只是 Git 历史中会留下加入和删除记录。 | 当然,也可以从试用阶段就使用 submodule。删除 submodule 并不困难,只是 Git 历史中会留下加入和删除记录。 | ||
=== submodule 不会帮忙完成什么 === | |||
Git submodule 只负责源码仓库和版本关系,不是完整的 Emacs 包管理器。 | Git submodule 只负责源码仓库和版本关系,不是完整的 Emacs 包管理器。 | ||
| Line 802: | Line 786: | ||
它不会自动完成: | 它不会自动完成: | ||
* 把插件加入 | * 把插件加入 <code>load-path</code> | ||
* 执行 | * 执行 <code>require</code> | ||
* | * 安装插件依赖 | ||
* 生成 | * 生成 autoload | ||
* | * 字节编译 | ||
* native | * native compilation | ||
* | * 编写 <code>setq</code> | ||
* 添加 | * 添加 hook | ||
* | * 配置快捷键 | ||
例如 Consult 依赖其他插件时,Git 不会自动读取 Emacs 的 | 例如 Consult 依赖其他插件时,Git 不会自动读取 Emacs 的 <code>Package-Requires</code> 并安装依赖。 | ||
需要自己继续添加依赖: | 需要自己继续添加依赖: | ||
<syntaxhighlight lang="bash"> | |||
git submodule add 依赖仓库URL travel/packages/依赖名 | git submodule add 依赖仓库URL travel/packages/依赖名 | ||
</syntaxhighlight> | |||
然后自己配置加载关系。 | 然后自己配置加载关系。 | ||
| Line 824: | Line 808: | ||
这套方案的特点是: | 这套方案的特点是: | ||
:放弃包管理器的一部分自动化,换取透明、固定、可控和可复现。 | |||
对于大量、频繁更新的插件,手工管理会比较繁琐;对于少量精选、长期固定、谨慎升级的插件,非常合适。 | 对于大量、频繁更新的插件,手工管理会比较繁琐;对于少量精选、长期固定、谨慎升级的插件,非常合适。 | ||
=== 普通 clone 与 submodule 对比 === | |||
{| class="wikitable" | |||
! 能力 | |||
! 普通嵌套 <code>git clone</code> | |||
| | | ! Git submodule | | ||
| --- | | --------------- | | ||
| 插件能否运行 | | 插件能否运行 | | ||
| 插件保持独立仓库 | | 能 | | ||
| 主仓库记录插件 URL | | 能 | | ||
| 主仓库记录插件路径 | | - | | ||
| 主仓库锁定插件 commit | | | 插件保持独立仓库 | | ||
| 换电脑后一条命令恢复 | | 能 | | ||
| 主配置和插件整体回滚 | | 能 | | ||
| 正式记录插件升级 | | - | | ||
| 保留插件自己的完整历史 | | 主仓库记录插件 URL | | ||
| 否 | | |||
| 是 | | |||
| - | | |||
| 主仓库记录插件路径 | | |||
| 否 | | |||
| 是 | | |||
| - | | |||
| 主仓库锁定插件 commit | | |||
| 否 | | |||
| 是 | | |||
| - | | |||
| 换电脑后一条命令恢复 | | |||
| 否 | | |||
| 是 | | |||
| - | | |||
| 主配置和插件整体回滚 | | |||
| 否 | | |||
| 是 | | |||
| - | | |||
| 正式记录插件升级 | | |||
| 否 | | |||
| 是 | | |||
| - | | |||
| 保留插件自己的完整历史 | | |||
| 是 | | |||
| 是 | | |||
| } | | |||
最核心的区别是: | 最核心的区别是: | ||
<syntaxhighlight lang="text"> | |||
普通 clone: | 普通 clone: | ||
插件只是碰巧存在于这台电脑。 | 插件只是碰巧存在于这台电脑。 | ||
Git submodule: | Git submodule: | ||
插件是主项目正式声明的一项依赖。 | 插件是主项目正式声明的一项依赖。 </syntaxhighlight> | ||
== 命令速查与完整工作流 == | |||
=== 常用命令速查 === | |||
| | {| class="wikitable" | ||
! 目的 | |||
--- | | ! 命令 | | ||
| ------------------------------------------------------------ | | |||
| 添加插件 | | |||
| <code>git submodule add URL PATH</code> | | |||
| - | | |||
| 查看所有插件版本 | | |||
| <code>git submodule status</code> | | |||
| - | | |||
| 升级一个插件 | | |||
| <code>git submodule update --remote -- PATH</code> | | |||
| - | | |||
| 查看插件升级内容 | | |||
| <code>git diff --submodule=log</code> | | |||
| - | | |||
| 接受插件升级 | | |||
| <code>git add PATH && git commit</code> | | |||
| - | | |||
| 放弃未提交的升级 | | |||
| <code>git submodule update --checkout --force -- PATH</code> | | |||
| - | | |||
| 初始化全部插件 | | |||
| <code>git submodule update --init --recursive</code> | | |||
| - | | |||
| 克隆主仓库和全部插件 | | |||
| <code>git clone --recurse-submodules URL</code> | | |||
| - | | |||
| 暂时停用本机插件 | | |||
| <code>git submodule deinit -- PATH</code> | | |||
| - | | |||
| 永久删除插件 | | |||
| <code>git rm PATH && git commit</code> | | |||
| - | | |||
| 查看插件自己的日志 | | |||
| <code>git -C PATH log --oneline</code> | | |||
| } | | |||
=== 添加插件 === | |||
<syntaxhighlight lang="bash"> | |||
cd ~/.emacs.d | cd ~/.emacs.d | ||
git submodule add | git submodule add | ||
插件URL | |||
travel/packages/插件名 | |||
git commit -m "Add 插件名" | git commit -m "Add 插件名" </syntaxhighlight> | ||
=== 升级插件 === | |||
<syntaxhighlight lang="bash"> | |||
git submodule update \ | git submodule update \ | ||
--remote \ | --remote \ | ||
-- travel/packages/插件名 | -- travel/packages/插件名 | ||
</syntaxhighlight> | |||
=== 查看升级内容 === | |||
<syntaxhighlight lang="bash"> | |||
git diff --submodule=log | git diff --submodule=log | ||
</syntaxhighlight> | |||
=== 测试失败,恢复旧版 === | |||
<syntaxhighlight lang="bash"> | |||
git submodule update \ | git submodule update \ | ||
--checkout \ | --checkout \ | ||
--force \ | --force \ | ||
-- travel/packages/插件名 | -- travel/packages/插件名 | ||
</syntaxhighlight> | |||
=== 测试成功,接受新版 === | |||
<syntaxhighlight lang="bash"> | |||
git add travel/packages/插件名 | git add travel/packages/插件名 | ||
git commit -m "Update 插件名" | git commit -m "Update 插件名" | ||
</syntaxhighlight> | |||
=== 删除插件 === | |||
<syntaxhighlight lang="bash"> | |||
git rm travel/packages/插件名 | git rm travel/packages/插件名 | ||
git rm travel/对应状态目录/init-插件名.el | git rm travel/对应状态目录/init-插件名.el | ||
git commit -m "Remove 插件名" | git commit -m "Remove 插件名" | ||
</syntaxhighlight> | |||
=== 换电脑完整恢复 === | |||
<syntaxhighlight lang="bash"> | |||
git clone --recurse-submodules \ | git clone --recurse-submodules \ | ||
主仓库URL \ | 主仓库URL \ | ||
~/.emacs.d | ~/.emacs.d | ||
</syntaxhighlight> | |||
== 总结 == | |||
普通 | 普通 <code>git clone</code> 只是: | ||
:把插件下载到了当前电脑。 | |||
<code>git submodule add</code> 则是: | |||
:让主仓库正式记录插件的 URL、保存路径和准确版本。 | |||
Git submodule 对 Emacs 插件管理最大的意义是: | Git submodule 对 Emacs 插件管理最大的意义是: | ||
:对插件版本状态进行精确控制,使整套配置可以稳定锁定、明确升级、快速回滚,并在其他电脑上一键复现。 | |||
Revision as of 21:08, 16 July 2026
Git submodule 适合这种情况:
- 一个主项目依赖若干外部 Git 仓库,但又希望这些外部仓库继续保持独立,并且由主项目精确指定它们的版本。
在 Emacs 配置中,可以把:
~/.emacs.d/
视为主仓库,把 Vertico、Consult、Magit 等第三方插件视为独立仓库。
使用 submodule 后,主仓库可以记录每个插件的:
- 仓库 URL
- 保存路径
- 当前锁定的准确 commit
这样就能对整套插件环境进行精确控制、整体回滚和异地复现。
核心原理
普通 git clone 为什么不够
假设已经在 ~/.emacs.d/ 建立了 Git 仓库,然后直接执行:
cd ~/.emacs.d/travel/packages
git clone https://github.com/minad/vertico.git
目录会变成:
~/.emacs.d/ 主仓库
└── travel/packages/
└── vertico/ 另一个独立 Git 仓库
└── .git/
Vertico 当然可以正常运行。Emacs 并不关心插件是通过哪种方式下载的,只要源码位于 load-path 中即可。
但对外层的 ~/.emacs.d 主仓库而言,这个嵌套仓库只是一个未经正式登记的外部目录。主仓库没有完整记录:
- Vertico 从哪个 URL 下载
- 应该保存在哪个目录
- 应该使用哪个 commit
- 换电脑后应该怎样恢复
- 主配置回滚时,Vertico 应该回到哪个版本
普通 git clone 解决的是:
- 把插件下载到当前电脑。
Git submodule 解决的是:
- 让主仓库正式声明:本项目依赖这个外部仓库,它应放在这个路径,并固定使用这个准确版本。
submodule 到底记录了什么
执行:
git submodule add \
https://github.com/minad/vertico.git \
travel/packages/vertico
Git 会记录两类信息。
.gitmodules:记录 URL 和路径
主仓库根目录会出现:
.gitmodules
内容大致如下:
[submodule "travel/packages/vertico"]
path = travel/packages/vertico
url = https://github.com/minad/vertico.git
它记录:
插件 URL: https://github.com/minad/vertico.git
保存位置: travel/packages/vertico
.gitmodules 是主仓库里的普通文本文件,应当提交进 Git。
gitlink:记录准确 commit
主仓库不会把 Vertico 的所有源码文件当成自己的文件追踪,而是在:
travel/packages/vertico
这个路径上记录一个特殊条目,指向 Vertico 的某个 commit。
例如:
Vertico → 9f84c31
这类特殊引用通常称为 gitlink。
因此,主仓库记录的是:
- 当前这套 Emacs 配置应当使用 Vertico 的
9f84c31版本。
插件的完整开发历史仍然保存在 Vertico 自己的仓库里,并不会被复制进主仓库。
主仓库和插件仓库的职责分工
| 仓库 |
这种设计避免了两个问题:
submodule 最重要的价值固定插件版本主仓库可以准确表达: 这版 Emacs 配置
├── Vertico:commit A
├── Consult:commit B
└── Magit:commit C
它记录的不是模糊的“使用最新版”,而是:
这相当于给插件环境加了一把版本锁。 整体回滚假设主仓库中有两个提交: 提交 A:
Vertico = abc1234
提交 B:
Vertico = def5678
Vertico 升级后出现问题,把主仓库恢复到提交 A,再执行: git submodule update --init --recursive
Vertico 就会重新回到: abc1234
因此,回滚的不只是自己的 普通嵌套 异地完整复现在另一台电脑执行: git clone --recurse-submodules \
主仓库URL \
~/.emacs.d
Git 会自动知道:
恢复出来的插件环境与原电脑一致。 明确记录插件升级使用 submodule 后,升级插件会被主仓库识别为一次正式变化: Vertico 原来:abc1234
Vertico 现在:def5678
测试成功后可以提交: git add travel/packages/vertico
git commit -m "Update Vertico"
这让插件升级变成一种:
的配置变化。 目录设计与初始化推荐的 Emacs 目录结构第三方插件源码与自己的配置文件最好分开: ~/.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
其中: travel/packages/
专门保存第三方插件原始仓库。 而: 10-qlzq
20-stable
30-maybe
40-test
保存自己写的配置文件,并表达对配置的理解程度和信任状态。 插件源码不必随着状态晋级而移动。例如: travel/packages/vertico/
始终保持不动。 只有自己的配置文件移动: travel/40-test/init-vertico.el
→ travel/30-maybe/init-vertico.el
→ travel/20-stable/init-vertico.el
这样不会反复修改 submodule 路径和 初始化主仓库在整个 Emacs 配置目录建立主仓库: cd ~/.emacs.d
git init -b main
先提交自己的基础配置: git add init.el travel
git commit -m "Initialize Emacs configuration"
仓库建在: ~/.emacs.d/
而不是只建在: ~/.emacs.d/travel/
这样 添加插件以 Vertico 为例: cd ~/.emacs.d
git submodule add
https://github.com/minad/vertico.git
travel/packages/vertico
检查状态: git status
一般会看到: new file: .gitmodules
new file: travel/packages/vertico
提交: git commit -m "Add Vertico submodule"
不要修改插件原始仓库不应把自己的配置写进: travel/packages/vertico/
例如不要这样: travel/packages/vertico/
├── vertico.el
├── vertico-indexed.el
└── my-vertico-config.el
因为这个目录属于 Vertico 自己的仓库。自行修改后,子模块会变成脏状态: modified content
正确做法是: travel/packages/vertico/ 插件原始源码
travel/40-test/init-vertico.el 自己的配置
例如: (require 'vertico)
(vertico-mode 1)
原则是:
插件升级、回滚与删除查看所有插件版本在主仓库运行: git submodule status
可能显示: abc1234 travel/packages/vertico
def5678 travel/packages/consult
前面的 commit 就是主仓库当前锁定的插件版本。 还可以设置更清楚的 submodule 差异显示: git config diff.submodule log
git config status.submodulesummary 1
以后运行: git diff
便能更直观地看到插件从哪些提交升级到了哪些提交。 查看某个插件自己的提交历史: git -C travel/packages/vertico log --oneline --decorate -10
这等价于先进入插件目录再执行 升级一个插件推荐一次只升级一个插件: cd ~/.emacs.d
git submodule update --remote -- travel/packages/vertico
此时 Vertico 会获取上游更新,并切换到远程跟踪分支的新提交。 主仓库会显示: modified: travel/packages/vertico
这里的 主仓库原来记录:Vertico commit A
插件现在位于: Vertico commit B
查看升级内容: git diff --submodule=log
然后正常使用几天,观察是否稳定。 升级成功后保留新版本确认没有问题后: git add travel/packages/vertico
git commit -m "Update Vertico"
主仓库便正式从: Vertico commit A
改为: Vertico commit B
最好坚持:
例如: Update Vertico
Update Consult
Update Magit
不要一次升级十几个插件后统一提交为: Update packages
否则出问题时,很难判断究竟是哪一个插件造成的。 放弃尚未提交的插件升级如果执行升级后发现新版本有问题,而且还没有提交: git submodule update \
--checkout \
--force \
-- travel/packages/vertico
Git 会让插件回到主仓库当前记录的旧 commit。 含义是: 主仓库固定版本:commit A
刚才临时升级到:commit B
执行恢复后: commit A
这就是最简单的“升级失败,退回旧版”。 升级已经提交,后来才发现问题假设升级 Vertico 的提交是: 7ac39fe Update Vertico
可以在主仓库运行: git revert 7ac39fe
git submodule update --init --recursive
这种做法比随意修改子仓库历史更清楚,因为:
为什么 submodule 经常处于 detached HEAD进入子模块后,运行: git status
可能看到: HEAD detached at abc1234
这在 submodule 中通常是正常现象。 主仓库需要插件准确停在: abc1234
而不是自动跟随 因此,submodule 默认常常直接检出某个 commit,形成 detached HEAD。 对只使用插件、不直接参与开发插件的人而言,这不是故障,而是版本锁定的正常状态。 是否一次升级全部插件技术上可以一次升级所有子模块: git submodule update --remote --recursive
但不建议日常这样做。 如果十几个插件同时升级后 Emacs 出问题,很难立即判断是:
更稳妥的办法是: git submodule update --remote -- travel/packages/vertico
测试、提交,再升级下一个插件。 适合这种管理方式的原则是:
submodule 最大的价值不是方便追逐最新版,而是方便长期固定已验证版本。 删除一个插件永久删除 submodule例如删除 Vertico: cd ~/.emacs.d
git rm travel/packages/vertico
git commit -m "Remove Vertico"
同时删除自己的插件配置: git rm travel/40-test/init-vertico.el
git commit -m "Remove Vertico configuration"
也可以一次完成: git rm travel/packages/vertico
git rm travel/40-test/init-vertico.el
git commit -m "Remove Vertico"
只在当前电脑暂时停用git submodule deinit -- travel/packages/vertico
这不会从主仓库历史中删除 Vertico,只是取消当前电脑上的初始化和工作目录。 重新恢复: git submodule update --init -- travel/packages/vertico
两者区别如下:
|
|---|