2022-04-28 14:28:03 +00:00
|
|
|
|
---
|
|
|
|
|
title: "使用 git hook 实现自动构建 Hugo 静态网站"
|
|
|
|
|
date: 2022-04-28T20:39:01+08:00
|
2022-05-09 08:34:05 +00:00
|
|
|
|
tags: [ git, git-hook, hugo ]
|
|
|
|
|
categories: [ tech ]
|
2022-05-18 14:54:44 +00:00
|
|
|
|
weight: 50
|
|
|
|
|
show_comments: true
|
2022-04-29 01:52:15 +00:00
|
|
|
|
draft: false
|
2022-04-28 14:28:03 +00:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
Hugo 是一个优秀的静态网站生成器,并且结构对 Git 十分友好,所以一般会将 Hugo 搭配 Git 使用来提供较高的可操作性,很多人还会搭配 GitHub Pages 来实现免服务器建立个人博客。但是在不使用 GitHub Pages 的情况下,使用静态博客就会不可避免地要重复构建网站,每次都要手动构建再上传构建结果未免过于繁琐,这篇文章将介绍已有自建服务器的情况下,通过 Git Hook 实现在推送时自动重新构建网站内容的方式。
|
|
|
|
|
|
|
|
|
|
<!--more-->
|
|
|
|
|
|
|
|
|
|
## 已有条件
|
|
|
|
|
|
|
|
|
|
在自己想要伺服网站的服务器上已有配置好的 Git 远程仓库和 HTTP 服务进程,并配置好默认指向的 `DocumentRoot`。
|
|
|
|
|
|
|
|
|
|
## 目标
|
|
|
|
|
|
|
|
|
|
我们希望在仓库推送时,触发一个脚本,此脚本会从仓库中检出在特定分支中的内容,并使用 Hugo 生成站点内容,并放到 HTTP 服务进程所指定的 `DocumentRoot` 目录下。此外,该脚本应当保留日志,以备未来出现错误时排错使用,如果该脚本使用临时目录,则应在使用结束后清理临时目录。在满足以上条件的情况下,该脚本应当尽可能降低对性能的消耗。
|
|
|
|
|
|
|
|
|
|
> 这种在某些动作触发时执行脚本的情况下,被执行的脚本一般称之为“回调”或者“钩子(hook)”,下文可能会有对这几个称呼的混用。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 前置知识 -- Git Hook
|
|
|
|
|
|
|
|
|
|
在一个 Git 仓库的 `.git/` 目录或裸仓库下有相当多的内容,其中 `hooks/` 就是存放特定动作触发时要执行的脚本,它们按照触发动作的不同,以不同的名字保存,比如 `post-receive` 就是在此仓库接收完成其他仓库推送内容以后要执行的脚本。**通常**在执行这些脚本前,当前工作目录(CWD)会先切换到裸仓库的根目录下或者普通仓库的根目录下。这些钩子的执行者是通过 SSH 建立连接的服务器上的用户、git-daemon 的运行用户等,所以在编写这些脚本时,要注意这些操作是否能够通过此用户已有的权限实现。
|
|
|
|
|
|
|
|
|
|
实际上按照文档,通常情况下 CWD 切换的位置是裸仓库的 `$GIT_DIR` 下和普通仓库的工作目录下,但是在默认情况下,`$GIT_DIR` 就是裸仓库的根目录、普通仓库的 `.git/` 目录。除此之外,有几个特殊的钩子在执行前 CWD 不区分是否是裸仓库,固定切换到 `$GIT_DIR` 目录下,这几个钩子如下
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
pre-receive, update, post-receive, post-update, push-to-checkout
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这些钩子按照触发动作不同必须使用该动作所指定的名称,如果同一个动作有多个钩子希望执行,那就需要自行编写脚本调用其他的钩子,或者将多个钩子做的事情合并在一个钩子中实现。对于这种需求有一个稍微规范的方式来供参考,即对某一个动作,编写一个脚本调用位于某个目录下的全部脚本,并将着一个脚本命名为特定的名称以在需要的时候进行触发,我的实现如下,参考并修改自 [stack overflow](https://stackoverflow.com/questions/26624368/handle-multiple-pre-commit-hooks/61341619#61341619)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
2022-04-28 14:57:20 +00:00
|
|
|
|
for hook in $(find "$(dirname "$0")"/post-receive.d -type f -perm -u=x); do
|
2022-04-28 14:28:03 +00:00
|
|
|
|
bash $hook
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
exit 0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
相比原答案的写法,我去掉了更换工作目录的部分,因为实现最终目的的脚本需要根据当前工作目录得出仓库的位置,此外我有两个钩子,它们互相不影响,不应当因为一个失败而拒绝执行另一个,所以去掉了对运行结果的判断。我的 `hooks/` 目录的结构如下,其中 `build-site` 是本次要编写的钩子,而另一个则是在收到推送以后更新 Git 的最近更新时间,用于配合 Cgit 正确显示最近活跃时间。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
hooks/:
|
|
|
|
|
total 8
|
|
|
|
|
-rwxr-xr-x 1 git git 92 Apr 28 13:21 post-receive
|
|
|
|
|
drwxr-xr-x 2 git git 4096 Apr 28 11:37 post-receive.d
|
|
|
|
|
|
|
|
|
|
hooks/post-receive.d:
|
|
|
|
|
total 8
|
|
|
|
|
-rwxr-xr-x 1 git git 812 Apr 28 11:37 build-site
|
|
|
|
|
-rwxr-xr-x 1 git git 641 Apr 28 06:42 last-modified
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 最终成果
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
|
|
|
|
branch=main
|
|
|
|
|
site_dir=/var/www/blog
|
|
|
|
|
log_file=/var/log/build-site.log
|
|
|
|
|
|
2022-04-29 01:52:15 +00:00
|
|
|
|
exec 1>> $log_file
|
2022-04-28 14:28:03 +00:00
|
|
|
|
exec 2>&1
|
|
|
|
|
|
|
|
|
|
# both are real path
|
|
|
|
|
new_repo=$(mktemp --directory)
|
|
|
|
|
git_dir=$(realpath .)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
echo ========= INFO ========
|
|
|
|
|
echo INFO: BUILD TIME: $(date -Iseconds)
|
2022-04-29 01:52:15 +00:00
|
|
|
|
echo INFO: SHELL: ${SHELL}
|
2022-04-28 14:28:03 +00:00
|
|
|
|
echo INFO: new_repo=$new_repo
|
|
|
|
|
echo INFO: git_dir=$git_dir
|
|
|
|
|
echo INFO: banch=$branch
|
|
|
|
|
echo INFO: site_dir=$site_dir
|
|
|
|
|
|
|
|
|
|
echo ===== CLONE REPO ======
|
|
|
|
|
# use "file://" to let depth take effect
|
2022-04-29 01:52:15 +00:00
|
|
|
|
echo INFO:command=git clone --depth=1 --branch=$branch file://$git_dir $new_repo
|
2022-04-28 14:28:03 +00:00
|
|
|
|
git clone --depth=1 --branch=$branch file://$git_dir $new_repo
|
|
|
|
|
|
|
|
|
|
echo === INIT SUBMODULE ====
|
2022-04-29 01:52:15 +00:00
|
|
|
|
echo git --work-tree=$new_repo --git-dir=$new_repo/.git -C $new_repo submodule update --init
|
|
|
|
|
git --work-tree=$new_repo --git-dir=$new_repo/.git -C $new_repo submodule update --init
|
2022-04-28 14:28:03 +00:00
|
|
|
|
|
|
|
|
|
echo ===== BUILD SITE ======
|
|
|
|
|
hugo --destination $site_dir --cleanDestinationDir --enableGitInfo -s $new_repo
|
|
|
|
|
|
|
|
|
|
echo ===== CLEAN TASK ======
|
|
|
|
|
echo remove the cloned repo $new_repo
|
|
|
|
|
rm -rf $new_repo
|
|
|
|
|
echo INFO: FINISH TIME: $(date -Iseconds)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
此脚本基本实现目标,在克隆仓库一步,可以有另外一种实现思路,就是通过 Git 的 `--git-dir` 和 `--work-tree` 以及 `-C` 三个参数实现某一个 Commit 的检出,并更新子模块,在这之中,更新子模块会有一定的问题,即不使用 `-C` 参数时会出现如下的错误信息,从报错信息上看这条错误是很不合理的,怀疑是 Git 尚存的缺陷,相关讨论可以看[这里](https://stackoverflow.com/questions/48767595/git-error-fatal-usr-libexec-git-core-git-submodule-cannot-be-used-without-a-w/64621032#64621032)。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
remote: fatal: /usr/libexec/git-core/git-submodule cannot be used without a working tree.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
此外,使用如上几个参数将裸仓库看作是普通仓库的 `$GIT_DIR` 会导致在裸仓库中创建一些额外的文件和目录,我觉得这是不好的,所以最后采用了克隆一个新的仓库的方法,将克隆深度设为 1 也是为了尽可能减少对性能的消耗,需要注意要使用 `file://` 格式的 URL 来指定原始仓库的路径,否则 `--depth` 参数将不会在本地文件系统中生效。
|
|
|
|
|
|
2022-04-29 01:52:15 +00:00
|
|
|
|
在初始化子模块的时候,`--work-tree` 和 `--git-dir` 参数是必须的,如果只保留 `-C` 参数则会出现下面的错误信息,但是奇怪的是如果在 bash 中手动运行该命令,又不会出现任何错误,只有在部署好,尝试推送仓库时,此错误才会在日志中显现,暂时认为是 git-shell 有我尚不知道的行为或者 Git 的 submodule 存在 BUG。
|
|
|
|
|
|
|
|
|
|
> 在实际尝试部署时,仅测试了单独 `-C` 和 `-C, --work-tree, --git-dir` 三个参数同时存在这两种情况,前者会出现上面描述的十分离奇且隐蔽的错误,后者正常工作。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
fatal: not a git repository: '.'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
实际的运行效果如下(内容从日志文件中获取)
|
2022-04-28 14:28:03 +00:00
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
========= INFO ========
|
2022-04-29 01:52:15 +00:00
|
|
|
|
INFO: BUILD TIME: 2022-04-29T01:41:15+00:00
|
|
|
|
|
INFO: SHELL: /usr/bin/git-shell
|
|
|
|
|
INFO: new_repo=/tmp/tmp.IjbpWdGlA8
|
2022-04-28 14:28:03 +00:00
|
|
|
|
INFO: git_dir=/srv/git-repo/pub/leafee98-blog.git
|
|
|
|
|
INFO: banch=main
|
|
|
|
|
INFO: site_dir=/var/www/blog
|
|
|
|
|
===== CLONE REPO ======
|
2022-04-29 01:52:15 +00:00
|
|
|
|
INFO:command=git clone --depth=1 --branch=main file:///srv/git-repo/pub/leafee98-blog.git /tmp/tmp.IjbpWdGlA8
|
|
|
|
|
Cloning into '/tmp/tmp.IjbpWdGlA8'...
|
2022-04-28 14:28:03 +00:00
|
|
|
|
=== INIT SUBMODULE ====
|
2022-04-29 01:52:15 +00:00
|
|
|
|
INFO:command=git --work-tree=/tmp/tmp.IjbpWdGlA8 --git-dir=/tmp/tmp.IjbpWdGlA8/.git -C /tmp/tmp.IjbpWdGlA8 submodule update --init
|
2022-04-28 14:28:03 +00:00
|
|
|
|
Submodule 'themes/hugo-theme-flat' (https://cgit.leafee98.com/hugo-theme-flat.git/) registered for path 'themes/hugo-theme-flat'
|
2022-04-29 01:52:15 +00:00
|
|
|
|
Cloning into '/tmp/tmp.IjbpWdGlA8/themes/hugo-theme-flat'...
|
2022-04-28 14:28:03 +00:00
|
|
|
|
Submodule path 'themes/hugo-theme-flat': checked out '4e73d5d1eb6f2d0d0cd375b422e60fd22077a29d'
|
|
|
|
|
===== BUILD SITE ======
|
|
|
|
|
Start building sites _
|
|
|
|
|
hugo v0.97.3-078053a43d746a26aa3d48cf1ec7122ae78a9bb4 linux/amd64 BuildDate=2022-04-18T17:22:19Z VendorInfo=gohugoio
|
|
|
|
|
|
|
|
|
|
| EN
|
|
|
|
|
-------------------+-----
|
|
|
|
|
Pages | 62
|
|
|
|
|
Paginator pages | 2
|
|
|
|
|
Non-page files | 0
|
|
|
|
|
Static files | 3
|
|
|
|
|
Processed images | 0
|
|
|
|
|
Aliases | 1
|
|
|
|
|
Sitemaps | 1
|
|
|
|
|
Cleaned | 0
|
|
|
|
|
|
2022-04-29 01:52:15 +00:00
|
|
|
|
Total in 312 ms
|
2022-04-28 14:28:03 +00:00
|
|
|
|
===== CLEAN TASK ======
|
2022-04-29 01:52:15 +00:00
|
|
|
|
remove the cloned repo /tmp/tmp.IjbpWdGlA8
|
|
|
|
|
INFO: FINISH TIME: 2022-04-29T01:41:17+00:00
|
2022-04-28 14:28:03 +00:00
|
|
|
|
```
|
|
|
|
|
|