本文将介绍如何使用 pre-commit Git Hook 自动更新 AstroPaper 博客主题 frontmatter 中的创建日期(pubDatetime)和修改日期(modDatetime)。
目录
处处可用
Git hooks 是自动化任务的好帮手,比如向提交信息中添加或检查分支名,或者阻止你将明文密钥提交到仓库。但最大的缺陷是客户端钩子是按机器独立的。
解决这个问题可以在本地创建一个 hooks 目录,然后手动复制到 .git/hooks 目录,或者创建符号链接,但这些都需要你记得去做——而这恰好是我最不擅长的事。
由于该项目使用 npm,我们可以借助 Husky 来自动安装钩子( AstroPaper 已内置此依赖)。
钩子
由于我们希望这个钩子在提交代码时运行,以便更新日期并将其纳入变更记录,所以选用 pre-commit 钩子。AstroPaper 项目已经配置好了,但如果是从零开始,可以运行 npx husky add .husky/pre-commit 'echo "This is our new pre-commit hook"'。
打开 hooks/pre-commit 文件,添加以下一个或两个代码片段。
文件被编辑时更新修改日期
更新:
此部分已更新为更智能的新版钩子。现在只有文章发布后才会递增 modDatetime。首次发布时,将 draft 状态设为 first,然后静待魔法发生。
# Modified files, update the modDatetime
git diff --cached --name-status |
grep -i '^M.*\.md$' |
while read _ file; do
filecontent=$(cat "$file")
frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')
if [ "$draft" = "false" ]; then
echo "$file modDateTime updated"
cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
mv tmp $file
git add $file
fi
if [ "$draft" = "first" ]; then
echo "First release of $file, draft set to false and modDateTime removed"
cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime:/" | sed "/---.*/,/---.*/s/^draft:.*$/draft: false/" > tmp
mv tmp $file
git add $file
fi
done
git diff --cached --name-status 会获取已暂存待提交的文件。输出格式如下:
A src/content/blog/setting-dates-via-git-hooks.md
行首的字母表示操作类型,上例中文件是被添加的(Added)。修改过的文件以 M 开头。
将输出通过管道传给 grep 命令,逐一检查每行,找出被修改的文件。行的格式需要以 M 开头((M)),后面跟任意字符(.*),最后以 .md 扩展名结尾(.(md)$)。这样就能过滤掉非 markdown 文件的修改行 egrep -i "^(M).*\.(md)$"。
改进 —— 更精确的匹配
可以进一步限制只查找 blog 目录下的 markdown 文件,因为只有这些文件才具备正确的 frontmatter 结构。
正则表达式会捕获两个部分:操作字母和文件路径。然后将这个列表通过管道传给 while 循环,遍历所有匹配的行,将字母赋值给 a,路径赋值给 b。这里暂时忽略 a。
要获取文件的草稿状态,需要解析其 frontmatter。以下代码用 cat 获取文件内容,再用 awk 以 frontmatter 分隔符(---)拆分,取第二块(即 frontmatter,即两个 --- 之间的部分)。接着再次使用 awk 查找 draft 键并打印其值。
filecontent=$(cat "$file")
frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')
获取到 draft 的值后,根据其内容执行以下三种操作之一:设置为当前时间(当 draft 为 false 时 if [ "$draft" = "false" ]; then)、清空 modDatetime 并将 draft 设为 false(当 draft 为 first 时 if [ "$draft" = "first" ]; then),或者什么也不做(其他情况)。
接下来的 sed 命令对我来说是有点神奇的,因为我不常用它,这个方案是从另一篇实现类似功能的博客文章中借鉴的。简而言之,它在文件 frontmatter 分隔符(---)范围内查找 modDatetime: 键,获取完整行,然后用格式正确的当前时间替换该行。
由于替换操作是在整个文件上下文中进行的,所以先写入临时文件(> tmp),再通过 mv 将新文件覆盖到原文件位置。然后把这个改动通过 git add 提交,就像我们自己做了这个修改一样。
注意
sed 命令要生效,frontmatter 中必须已经存在 modDatetime 键。如果要让应用在日期为空时也能构建,还需要做一些其他改动,见下文。
为新文件添加日期
为新文件添加日期的流程与上述相同,但这次要找的是新增的行(A),并将 pubDatetime 的值替换为当前日期时间。
# New files, add/update the pubDatetime
git diff --cached --name-status | egrep -i "^(A).*\.(md)$" | while read a b; do
cat $b | sed "/---.*/,/---.*/s/^pubDatetime:.*$/pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
mv tmp $b
git add $b
done
改进 —— 只循环一次
可以利用 a 变量在循环内部做判断,在一个循环中同时更新 modDatetime 或添加 pubDatetime。
填充 frontmatter
如果你的 IDE 支持代码片段,可以创建一个自定义片段来快速填充 frontmatter。AstroPaper v4 将默认附带 VSCode 片段。
modDatetime 为空时的修改
为了让 Astro 能够编译 markdown 并完成后续处理,它需要知道 frontmatter 中有哪些预期字段。这是通过 src/content/config.ts 中的配置来定义的。
要允许键存在但值为空,需要编辑第 10 行,添加 .nullable() 函数。
const blog = defineCollection({
type: "content",
schema: ({ image }) =>
z.object({
author: z.string().default(SITE.author),
pubDatetime: z.date(),
modDatetime: z.date().optional(),
modDatetime: z.date().optional().nullable(),
title: z.string(),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
tags: z.array(z.string()).default(["others"]),
ogImage: image().or(z.string()).optional(),
description: z.string(),
canonicalURL: z.string().optional(),
readingTime: z.string().optional(),
}),
});
为了消除 IDE 在博客引擎文件中的报错,我还做了以下修改:
-
在
src/layouts/Layout.astro第 15 行添加| null,变为export interface Props { title?: string; author?: string; description?: string; ogImage?: string; canonicalURL?: string; pubDatetime?: Date; modDatetime?: Date | null; } -
在
src/components/Datetime.tsx第 5 行添加| null,变为interface DatetimesProps { pubDatetime: string | Date; modDatetime: string | Date | undefined | null; }
评论