跳转到内容
文韬博宇的个人主页
返回

【转载】如何使用 Git Hooks 设置创建和修改日期

更新于:
· 约 3 分钟· 约 1134 字阅读0编辑页面
本文已超过两年未更新,部分内容可能已过时,请注意甄别。

本文将介绍如何使用 pre-commit Git Hook 自动更新 AstroPaper 博客主题 frontmatter 中的创建日期(pubDatetime)和修改日期(modDatetime)。

目录

处处可用

Git hooks 是自动化任务的好帮手,比如向提交信息中添加或检查分支名,或者阻止你将明文密钥提交到仓库。但最大的缺陷是客户端钩子是按机器独立的。

解决这个问题可以在本地创建一个 hooks 目录,然后手动复制到 .git/hooks 目录,或者创建符号链接,但这些都需要你记得去做——而这恰好是我最不擅长的事。

由于该项目使用 npm,我们可以借助 Husky 来自动安装钩子( AstroPaper 已内置此依赖)。

Note

更新!AstroPaper v4.3.0 中已移除了 pre-commit 钩子,改为使用 GitHub Actions。不过你也可以自行安装 Husky

钩子

由于我们希望这个钩子在提交代码时运行,以便更新日期并将其纳入变更记录,所以选用 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 在博客引擎文件中的报错,我还做了以下修改:

  1. src/layouts/Layout.astro 第 15 行添加 | null,变为

    export interface Props {
      title?: string;
      author?: string;
      description?: string;
      ogImage?: string;
      canonicalURL?: string;
      pubDatetime?: Date;
      modDatetime?: Date | null;
    }
  2. src/components/Datetime.tsx 第 5 行添加 | null,变为

    interface DatetimesProps {
      pubDatetime: string | Date;
      modDatetime: string | Date | undefined | null;
    }

编辑页面
分享这篇文章:

评论


上一篇
【转载】如何在 AstroPaper 中集成 Giscus 评论功能