Git submodule 的精准用法: Difference between revisions

From 清冽之泉
Jump to navigation Jump to search
No edit summary
 
(15 intermediate revisions by the same user not shown)
Line 1: Line 1:
Git submodule 适合这种情况:
Git submodule 适合这种情况:主项目需要别的子项目,主项目需要管理子项目的版本。git clone 的子项目只是碰巧存在于这台电脑;而 Git submodule,子项目是主项目正式声明的一项依赖,正式记录插件的 URL、保存路径和准确版本。Git submodule 的本质是,放弃包管理器的一部分自动化,换取透明、固定、可控和可复现。对于大量、频繁更新的子项目,手工管理会比较繁琐;对于少量精选、长期固定、谨慎升级的子项目,非常合适。
* 主项目需要别的子项目
* 主项目管理别的子项目的版本


== 概念 ==
== 概念 ==
Line 16: Line 14:


.gitmodules 内容大致如下:
.gitmodules 内容大致如下:
<syntaxhighlight lang="ini">
<syntaxhighlight lang="ini">
[submodule "travel/packages/vertico"]
[submodule "travel/packages/vertico"]
Line 24: Line 21:


submodule 还会记录一条指向某个 commit 的 gitlink。
submodule 还会记录一条指向某个 commit 的 gitlink。
{|class="wikitable"
{|class="wikitable"
! 仓库
! 仓库
Line 42: Line 38:
|}
|}


这种设计避免了两个问题:
这种设计有两个好处:
 
* 不把别人写的几百个插件源码文件伪装成自己的文件
# 不把别人写的几百个插件源码文件伪装成自己的文件
* 又能让配置仓库精确声明自己依赖哪个插件版本
# 又能让配置仓库精确声明自己依赖哪个插件版本


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


== 工作流 ==
== 工作流 ==
=== 配置 ===
<syntaxhighlight lang="bash" line>
# git config diff.submodule log
## 配置
# git config status.submodulesummary 1
git config diff.submodule log
git config status.submodulesummary 1


=== 添加 ===
## 添加
# cd ~/.emacs.d
cd ~/.emacs.d
# git init -b main
git init -b main
# git submodule add https://github.com/minad/vertico.git travel/packages/vertico
git submodule add https://github.com/minad/vertico.git travel/packages/vertico
# git status
git status
# git commit -m "Add Vertico submodule"
git commit -m "Add Vertico submodule"
# git submodule status


不要修改插件原始仓库。不应把自己的配置写进子仓库。
## 查看
git -C travel/packages/vertico log --oneline --decorate -10
git submodule status


=== 查看 ===
## 升级
# git -C travel/packages/vertico log --oneline --decorate -10
git submodule update --remote -- travel/packages/vertico
git diff --submodule=log


=== 升级 ===
## 保留
# git submodule update --remote -- travel/packages/vertico
git add travel/packages/vertico
# git diff --submodule=log
git commit -m "Update Vertico" # commit 了才正式获得了主仓库的承认


=== 保留 ===
## 放弃
# git add travel/packages/vertico
git submodule update --checkout --force -- travel/packages/vertico # 未提交时,放弃,或
# git commit -m "Update Vertico" ;; commit 了才正式获得了主仓库的承认
git revert 7ac39fe && git submodule update --init --recursive # 错提交后,放弃


一次升级一个插件,一个插件形成一个独立提交。
## 删除
git rm travel/packages/vertico
git rm travel/40-test/init-vertico.el
git commit -m "Remove Vertico"


=== 未提交+放弃 ===
## 暂停
如果执行升级后发现新版本有问题,而且还没有提交:
git submodule deinit -- travel/packages/vertico
<syntaxhighlight lang="bash">
git submodule update \
  --checkout \
  --force \
  -- travel/packages/vertico
</syntaxhighlight>


这就是最简单的“升级失败,退回旧版”。
## 不暂停
git submodule update --init -- travel/packages/vertico


=== 已提交+放弃 ===
## 移机
假设升级 Vertico 的提交是:7ac39fe Update Vertico
git clone --recurse-submodules 主仓库URL ~/.emacs.d


可以在主仓库运行:
## 同步
<syntaxhighlight lang="bash">
git pull
git revert 7ac39fe
git submodule update --init --recursive
</syntaxhighlight>
 
<code>git revert</code> 会新增一个提交,撤销那次插件升级;随后 <code>git submodule update</code> 让磁盘上的插件切回旧 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>
 
<code>git rm</code> 会同时更新主仓库索引和 <code>.gitmodules</code> 中相应的 submodule 记录。
 
=== 暂停 ===
# git submodule deinit -- travel/packages/vertico
 
=== 不暂停 ===
# git submodule update --init -- travel/packages/vertico
 
== 移机 ==
<syntaxhighlight lang="bash">
;; 一次性移机主仓库和子仓库
git clone --recurse-submodules \
  主仓库URL \
  ~/.emacs.d
</syntaxhighlight>
 
或者,
<syntaxhighlight lang="bash">
;; 先移机主仓库
;; 再移机子仓库
git clone 主仓库URL ~/.emacs.d
git submodule update --init --recursive
git submodule update --init --recursive
</syntaxhighlight>
</syntaxhighlight>


=== 日常更新 ===
== 经验 ==
<syntaxhighlight lang="bash">
* 不要修改插件原始仓库。不应把自己的配置写进子仓库。
git pull
* 一次升级一个插件,一个插件形成一个独立提交。
git submodule update --init --recursive </syntaxhighlight>
* submodule 经常处于 detached HEAD 是正常的。这是版本锁定的正常状态。
</syntaxhighlight>
* Git submodule 只管理源码仓库及其版本关系,它不是 Emacs 包管理器;插件依赖仍需自己检查并安装或加入 submodule。
 
第二条命令让本机插件真正切换到主仓库指定的版本。
 
单纯运行第一条命令不太够。
 
== 边界 ==
Git submodule 只负责源码仓库和版本关系,不是完整的 Emacs 包管理器。
 
它不会自动完成:
* 把插件加入 <code>load-path</code>
* 执行 <code>require</code>
* 安装插件依赖
* 生成 autoload
* 字节编译
* native compilation
* 编写 <code>setq</code>
* 添加 hook
* 配置快捷键
 
例如 Consult 依赖其他插件时,Git 不会自动读取 Emacs 的 <code>Package-Requires</code> 并安装依赖。
 
需要自己继续添加依赖:
<syntaxhighlight lang="bash">
git submodule add 依赖仓库URL travel/packages/依赖名
</syntaxhighlight>
 
然后自己配置加载关系。
 
Git submodule 的本质是,放弃包管理器的一部分自动化,换取透明、固定、可控和可复现。对于大量、频繁更新的插件,手工管理会比较繁琐;对于少量精选、长期固定、谨慎升级的插件,非常合适。
 
== 对比 ==
{|class="wikitable"
! 能力
! 普通嵌套 git clone
! Git submodule
|-
|插件能否运行
|能
|能
|-
|插件保持独立仓库
|能
|能
|-
|主仓库记录插件 URL
|否
|是
|-
|主仓库记录插件路径
|否
|是
|-
|主仓库锁定插件 commit
|否
|是
|-
|换电脑后一条命令恢复
|否
|是
|-
|主配置和插件整体回滚
|否
|是
|-
|正式记录插件升级
|否
|是
|-
|保留插件自己的完整历史
|是
|是
|}
 
最核心的区别是,普通 clone 插件只是碰巧存在于这台电脑。Git submodule,插件是主项目正式声明的一项依赖。
 
== 工作流 ==
{|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">
git submodule update \
  --remote \
  -- travel/packages/插件名
</syntaxhighlight>
 
=== 查看升级内容 ===
 
<syntaxhighlight lang="bash">
git diff --submodule=log
</syntaxhighlight>
 
=== 测试失败,恢复旧版 ===
 
<syntaxhighlight lang="bash">
git submodule update \
  --checkout \
  --force \
  -- travel/packages/插件名
</syntaxhighlight>
 
=== 测试成功,接受新版 ===
 
<syntaxhighlight lang="bash">
git add travel/packages/插件名
git commit -m "Update 插件名"
</syntaxhighlight>
 
=== 删除插件 ===
 
<syntaxhighlight lang="bash">
git rm travel/packages/插件名
git rm travel/对应状态目录/init-插件名.el
git commit -m "Remove 插件名"
</syntaxhighlight>
 
=== 换电脑完整恢复 ===
 
<syntaxhighlight lang="bash">
git clone --recurse-submodules \
  主仓库URL \
  ~/.emacs.d
</syntaxhighlight>
 
== 总结 ==
 
普通 <code>git clone</code> 只是:
 
:把插件下载到了当前电脑。
 
<code>git submodule add</code> 则是:
 
:让主仓库正式记录插件的 URL、保存路径和准确版本。
 
Git submodule Emacs 插件管理最大的意义是:
 
:对插件版本状态进行精确控制,使整套配置可以稳定锁定、明确升级、快速回滚,并在其他电脑上一键复现。

Latest revision as of 21:43, 22 July 2026

Git submodule 适合这种情况:主项目需要别的子项目,主项目需要管理子项目的版本。git clone 的子项目只是碰巧存在于这台电脑;而 Git submodule,子项目是主项目正式声明的一项依赖,正式记录插件的 URL、保存路径和准确版本。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 最重要的价值:

固定插件版本
它记录的不是模糊的“使用最新版”,而是,这套配置经过测试时,各插件分别停留在哪个准确版本。这相当于给插件环境加了一把版本锁。
整体回滚
主仓库回滚到过去某个 commit 后,再执行 git submodule update --init --recursive,各子仓库会恢复到该主仓库 commit 当时所记录的版本。普通嵌套 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 -C travel/packages/vertico log --oneline --decorate -10
git submodule status

## 升级 
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 # 未提交时,放弃,或
git revert 7ac39fe && git submodule update --init --recursive # 错提交后,放弃

## 删除 
git rm travel/packages/vertico
git rm travel/40-test/init-vertico.el
git commit -m "Remove Vertico"

## 暂停
git submodule deinit -- travel/packages/vertico

## 不暂停
git submodule update --init -- travel/packages/vertico

## 移机
git clone --recurse-submodules 主仓库URL ~/.emacs.d

## 同步
git pull
git submodule update --init --recursive

经验

  • 不要修改插件原始仓库。不应把自己的配置写进子仓库。
  • 一次升级一个插件,一个插件形成一个独立提交。
  • submodule 经常处于 detached HEAD 是正常的。这是版本锁定的正常状态。
  • Git submodule 只管理源码仓库及其版本关系,它不是 Emacs 包管理器;插件依赖仍需自己检查并安装或加入 submodule。