|
|
| 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"> |
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
</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 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 插件管理最大的意义是:
- 对插件版本状态进行精确控制,使整套配置可以稳定锁定、明确升级、快速回滚,并在其他电脑上一键复现。