为什么要用 Git Submodule
最初搭建博客时,我把 Hugo Theme Stack 的所有文件直接复制、提交到了博客仓库里。主题文件成百上千,和自己的文章、配置搅在一起。这带来几个问题:
无法追踪上游更新:上游修了 bug、加了新功能,我完全不知道,知道了也没法一键合并。唯一的选择是手动下载新版本的主题,重新覆盖,然后祈祷不要把自己改过的地方冲掉。
提交历史被污染:几百个主题文件的增删改和自己的博客文章混在同一个 git log 里,一眼看去完全分不清哪些是自己的创作、哪些是第三方代码。
仓库臃肿:主题文件占了大头,真正属于我的内容反而被淹没了。
Git Submodule 是什么
Git Submodule 允许一个 Git 仓库(主仓库)引用另一个 Git 仓库(子模块)的特定 commit,而不是把它的文件复制过来。
可以这样理解:不用 submodule 时,你等于把别人的房子拆了,砖头一块块搬到自己院子里重新盖;用了 submodule,你只是在自家院子里插了一块路牌,写着「去那边那个房子」。
关键特性:
- 主仓库只记录一个指针(子模块的 repo URL + commit hash),不存储子模块的实际文件
git clone主仓库后,子模块目录是空的,需要git submodule update --init才会把子模块拉下来- 子模块有自己独立的
.git目录、独立的 commit 历史、独立的 remote - 子模块的版本是精确锁定的——它指向一个具体 commit,不会自动更新
Submodule 的实际优势
| 直接嵌入主题 | Git Submodule | |
|---|---|---|
| 查看改动 | git diff 里几百个主题文件混在一起 | git diff 只显示指针 commit 的变化 |
| 更新主题 | 手动下载、覆盖、手动合并 | cd themes/xxx && git pull |
| 上游修复 | 不知道、合并痛苦 | git fetch origin && git merge |
| 回滚 | 混在博客历史中难以定位 | 主仓库 git checkout 一个旧 commit 即可恢复当时引用的主题版本 |
| 定制 | 直接改文件 | 通过 Hugo 的覆盖机制(layouts/、assets/、static/)在不碰主题源码的前提下定制 |
背景
基于以上原因,我决定把主题改成 git submodule 管理。但过程并不顺利,踩了四个坑,逐一记录。
第一步:把嵌入的主题改成 submodule + fork
思路很清晰:
- Fork 一份
CaiJimmy/hugo-theme-stack到自己的 GitHub - 在 fork 里改好暗色模式的颜色(背景
#000000,卡片#121317) - 博客主仓库
git rm -r themes/hugo-theme-stack git submodule add指向上一步的 fork
这一步还算顺利。具体改动见 commit 52b389e。
坑一:submodule 指向的是 fork 的 v4.x,和博客用的 v3.9.x 不兼容
我的 fork 克隆的是上游 main 分支(v4.x),但博客一直用的是 v3.9.x。两个版本的目录结构、CSS 变量、模板写法都不一样。
现象:切 submodule 后,暗色模式的颜色没了,整体布局也有些差异。
最初的想法:切回 fork 的 v3.9.2 tag。但 fork 是直接从上游 fork 的 main,在 GitHub 上没法直接指定 fork 时的分支。
最终方案:submodule 直接指向上游 CaiJimmy/hugo-theme-stack 的 v3.9.2 tag,不要再维护一个 fork 了。主题定制统一通过 Hugo 的覆盖机制实现,不动主题源码。
| |
--force是因为.git/modules/themes/里还缓存着旧的 git 目录。不加--force会报A git directory for 'themes/hugo-theme-stack' is found locally。
坑二:Hugo v0.152.2 废弃 API,直接编译失败
GitHub Actions 用的 Hugo 版本是 0.152.2。在这个版本中:
resources.ToCSS→ 已移除,需用css.Sass.Site.LastChange→ 已移除,需用.Site.Lastmod
但 hugo-theme-stack v3.9.2 仍然在用这些废弃 API。Workflow 直接报错退出。
解决方案:Hugo 布局文件的优先级是「项目 layouts/ > 主题 layouts/」。因此直接照搬主题中的三个文件到项目目录,修改一行即可:
| 文件 | 修改 |
|---|---|
layouts/partials/head/style.html | resources.ToCSS → css.Sass |
layouts/partials/comments/provider/disqusjs.html | resources.ToCSS → css.Sass |
layouts/partials/head/opengraph/provider/base.html | .Site.LastChange → .Site.Lastmod |
| |
| |
坑三:assets/scss/custom.scss 暗色模式覆盖不生效(重点)
编译通过了,但暗色模式还是默认的 #303030 / #424242。
排查过程:我在项目根目录创建了 assets/scss/custom.scss,写入:
| |
主题的 assets/scss/style.scss 末尾有 @import "custom.scss",理论上 Hugo 应该优先加载项目的 assets/scss/custom.scss。
但实际情况是:主题的 assets/scss/ 目录下也有一个 custom.scss,虽然它只是一个空注释:
| |
Hugo 的 SCSS @import 解析器在遇到 @import "custom.scss" 时,会先在当前文件所在目录查找(即主题的 assets/scss/)。它找到了主题的那个空文件,加载了它,就停了。项目里的同名文件根本没被看到。
踩坑记录:
❌ 尝试覆盖
style.scss,把@import "custom.scss"改成@import "site-custom.scss",然后创建site-custom.scss。这个方案理论上可行,但实际操作中因为之前的编译产物一致(都是默认色),GitHub Actions 的部署环节peaceiris/actions-gh-pages检测到nothing to commit,跳过了部署,导致没有生效。❌ 尝试直接修改 submodule 内的主题文件 —— 本地能跑,但 CI 重新 clone 子模块后又还原了。
✅ 最终方案:直接覆盖
assets/scss/variables.scss。
Hugo 的资源查找(resources.Get)逻辑和 SCSS @import 解析逻辑不同。resources.Get 会优先使用项目中的文件而不是主题中的。所以只要创建一个包含暗色模式的 assets/scss/variables.scss,Hugo 就会用它代替主题的版本。
| |
这个方案不仅解决了 @import 优先级问题,而且改动集中在一个文件里,不依赖 custom.scss 的加载顺序。
坑四:部署日志显示成功,但 .github.io 仓库没更新
Workflow 日志里 Deploy Web 步骤显示:
| |
原因:peaceiris/actions-gh-pages@v4 默认行为会在目标仓库克隆一份,git rm -r * 清空,再 cp 新的构建产物进去,然后 commit。如果新的产物和上次完全一致(文件树 hash 相同),git 会认为没有变化,跳过 commit。
这发生在上一个坑的排查中 — 我们的 style.scss 覆盖方案编译出来的结果和默认主题一样(都是 #303030 / #424242),所以部署被跳过了。
只要修复真正生效(编译出不同的 CSS),部署就会重新触发。
总结:Hugo 主题定制的优先级规则
| 场景 | Hugo 的行为 | 推荐做法 |
|---|---|---|
| 布局覆盖 (layouts/) | 项目的 layouts/ 优先于主题 | 直接复制主题文件到项目,修改 |
| SCSS @import | 先从导入文件的目录查找 | 不要和主题的同名文件冲突,避免用通用的名字如 custom.scss |
| resources.Get + SCSS | 项目的 assets/ 优先于主题 | 覆盖 variables.scss 是最可靠的 |
| 静态资源 (static/) | 项目的 static/ 覆盖主题同名文件 | 放 static/img/avatar.png 等 |
关键教训:在 Hugo 中给主题改样式,特别是改 CSS 变量,最稳妥的方式永远是 直接在项目中放置 assets/scss/variables.scss,而不是依赖 custom.scss 的 @import 机制。
附:本次改动总览
| |
