Skip to content
Bernard's Blog
Go back

如何使用 Git Hooks 设置创建和修改日期

Updated:

本文将介绍如何使用 pre-commit Git hook 自动填写 AstroPaper 博客主题前置元数据中的创建日期(pubDatetime)和修改日期(modDatetime)。

目录

随处可用

Git hooks 非常适合自动化任务,例如添加检查分支名称到提交信息,或者阻止你提交明文密码。它们最大的缺点是客户端 hooks 是按机器配置的。

你可以通过创建一个 hooks 目录并手动将它们复制到 .git/hooks 目录或设置符号链接来解决这个问题,但这都需要你记得去设置,而这不是我擅长的事情。

由于该项目使用 npm,我们可以利用一个名为 Husky 的包(AstroPaper 已安装此包)来自动安装 hooks。

更新!在 AstroPaper v4.3.0 中,pre-commit hook 已被移除,转而使用 GitHub Actions。不过,你可以轻松地自行安装 Husky

Hook 内容

我们希望这个 hook 在我们提交代码时运行,以更新日期并将其作为我们更改的一部分,因此我们将使用 pre-commit hook。AstroPaper 项目已经设置好了这个 hook,如果没有设置,你可以运行 npx husky add .husky/pre-commit 'echo "这是我们新的 pre-commit hook"'

导航到 hooks/pre-commit 文件,我们将添加以下一个或两个代码片段。

文件被编辑时更新修改日期


更新:

本节已更新为更智能的新版本 hook。它现在不会在文章发布前增加 modDatetime。首次发布时,将 draft 状态设置为 first,然后见证奇迹的发生。


# 已修改的文件,更新 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 已更新"
    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 "首次发布 $file,draft 设为 false,modDateTime 已移除"
    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 从 git 中获取已暂存待提交的文件。输出看起来像:

A       src/content/blog/setting-dates-via-git-hooks.md

开头的字母表示执行的操作,上例中文件已添加。修改过的文件显示 M

我们将输出传递给 grep 命令,查找已修改的行。该行需要以 M 开头(^(M)),后面有任意数量的字符(.*),并以 .md 文件扩展名结尾(.(md)$)。这将过滤掉未被修改的 markdown 文件 egrep -i "^(M).*\.(md)$"


改进 - 更精确

这可以进一步限定为只查找 blog 目录中的 markdown 文件,因为这些文件才有正确的前置元数据。


正则表达式将捕获两个部分:字母和文件路径。我们将这个列表传递给 while 循环以遍历匹配的行,并将字母赋值给 a,路径赋值给 b。我们现在先忽略 a

为了知道文件的 draft 状态,我们需要它的前置元数据。在以下代码中,我们使用 cat 获取文件内容,然后使用 awk 在前置元数据分隔符(---)处分割文件,并取第二个块(前置元数据,即 --- 之间的部分)。然后再次使用 awk 查找 draft 键并输出其值。

  filecontent=$(cat "$file")
  frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
  draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')

现在我们有了 draft 的值,我们将做三件事之一:将 modDatetime 设为当前时间(当 draft 为 false 时 if [ "$draft" = "false" ]; then),清除 modDatetime 并将 draft 设为 false(当 draft 设为 first 时 if [ "$draft" = "first" ]; then),或者什么都不做(其他情况)。

接下来的 sed 命令对我来说有点神奇,因为我不常用它,它是从另一篇关于类似操作的博客文章中复制来的。本质上,它是在文件的前置元数据标签(---)内查找 pubDatetime: 键,获取整行并将其替换为 pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/",即相同的键和当前日期时间(格式正确)。

这个替换是针对整个文件的,所以我们将结果放入临时文件(> tmp),然后将新文件移动(mv)到旧文件的位置,覆盖它。然后将其添加到 git 中,就像是我们自己做的更改一样。


注意

为了让 sed 生效,前置元数据中需要已经存在 modDatetime 键。还需要做一些其他更改才能使应用程序能够使用空白日期构建,请参见下文


为新文件添加日期

为新文件添加日期的过程同上,但这次我们要查找已添加(A)的行,并替换 pubDatetime 的值。

# 新文件,添加/更新 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


填充前置元数据

如果你的 IDE 支持代码片段,则可以选择创建自定义代码片段来填充前置元数据。AstroPaper v4 默认会为 VSCode 提供一个代码片段。

空的 modDatetime 更改

为了让 Astro 能够编译 markdown 并正常工作,它需要知道前置元数据中预期的内容。这是通过 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;
    }

Share this post on:

Previous Post
AstroPaper 5.0
Next Post
AstroPaper 4.0