leafee98-blog/content/posts/use-git-hook-to-build-hugo-site-automatically.md

160 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "使用 git hook 实现自动构建 Hugo 静态网站"
date: 2022-04-28T20:39:01+08:00
tags: [ git, git-hook, hugo ]
categories: [ tech ]
weight: 50
show_comments: true
draft: false
---
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
for hook in $(find "$(dirname "$0")"/post-receive.d -type f -perm -u=x); do
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
exec 1>> $log_file
exec 2>&1
# both are real path
new_repo=$(mktemp --directory)
git_dir=$(realpath .)
echo ========= INFO ========
echo INFO: BUILD TIME: $(date -Iseconds)
echo INFO: SHELL: ${SHELL}
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
echo INFO:command=git clone --depth=1 --branch=$branch file://$git_dir $new_repo
git clone --depth=1 --branch=$branch file://$git_dir $new_repo
echo === INIT SUBMODULE ====
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
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` 参数将不会在本地文件系统中生效。
在初始化子模块的时候,`--work-tree` 和 `--git-dir` 参数是必须的,如果只保留 `-C` 参数则会出现下面的错误信息,但是奇怪的是如果在 bash 中手动运行该命令,又不会出现任何错误,只有在部署好,尝试推送仓库时,此错误才会在日志中显现,暂时认为是 git-shell 有我尚不知道的行为或者 Git 的 submodule 存在 BUG。
> 在实际尝试部署时,仅测试了单独 `-C` 和 `-C, --work-tree, --git-dir` 三个参数同时存在这两种情况,前者会出现上面描述的十分离奇且隐蔽的错误,后者正常工作。
```
fatal: not a git repository: '.'
```
实际的运行效果如下(内容从日志文件中获取)
```
========= INFO ========
INFO: BUILD TIME: 2022-04-29T01:41:15+00:00
INFO: SHELL: /usr/bin/git-shell
INFO: new_repo=/tmp/tmp.IjbpWdGlA8
INFO: git_dir=/srv/git-repo/pub/leafee98-blog.git
INFO: banch=main
INFO: site_dir=/var/www/blog
===== CLONE REPO ======
INFO:command=git clone --depth=1 --branch=main file:///srv/git-repo/pub/leafee98-blog.git /tmp/tmp.IjbpWdGlA8
Cloning into '/tmp/tmp.IjbpWdGlA8'...
=== INIT SUBMODULE ====
INFO:command=git --work-tree=/tmp/tmp.IjbpWdGlA8 --git-dir=/tmp/tmp.IjbpWdGlA8/.git -C /tmp/tmp.IjbpWdGlA8 submodule update --init
Submodule 'themes/hugo-theme-flat' (https://cgit.leafee98.com/hugo-theme-flat.git/) registered for path 'themes/hugo-theme-flat'
Cloning into '/tmp/tmp.IjbpWdGlA8/themes/hugo-theme-flat'...
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
Total in 312 ms
===== CLEAN TASK ======
remove the cloned repo /tmp/tmp.IjbpWdGlA8
INFO: FINISH TIME: 2022-04-29T01:41:17+00:00
```