基于 git-commit-hash,为发布文章添加版本号功能
工具git-hook, NodeJs, shell, husky

基于 git-commit-hash,为发布文章添加版本号功能

Published On
(C) unsplash
11分钟阅读e35bda0e
概述:给出实用代码例,将每次提交博文变更的 git-commit-hash 值作为博文的可见版本号

最前

如果博文目录和部署的站点源码同在一个项目中,则天然的可以使用每次创建(或更新)博文的 git-commit-hash 值作为博文的版本号。以下为本站当前实际投入使用的相关代码段分享,如果读者也有类似功能的需求,尽可按需取用。

(另外:本文的大部分代码:结合 Claude AI 3.5 问询结果,迭代数个版本 + 手动调优完成,已验证可行性)

正文

我们的最终成果,是在项目指定目录下持久化一份版本记录文件,形式如下:

src/path/to/post-version/versions.json
{
  "10_the-time-machine": {
    "hash": "42d402a9",
    "hashFull": "42d402a96dfb06de8cb1333aa24a76e3e00a7466"
  },
  "11_media-query-prompt-stamp": {
    "hash": "a1510497",
    "hashFull": "a1510497cef501ac54952d41e6b369cebd22cbdd"
  },
  "12_moc-on-saas-starter": {
    "hash": "3d2e9983",
    "hashFull": "3d2e9983cba65a3939f44031ddcce054ae95ce10"
  },
  "13_moc-of-cms-blog": {
    "hash": "69245ed8",
    "hashFull": "69245ed8692a9d065a8e1f9c80d65f841ddcbce8"
  }
}

基本思路

假定我们的博文存储路径在 /_posts 下,需要记录版本号的文件格式主要有: .md / .mdx 。

基本的工具函数和类型文件首先给出(在折叠代码块中),阅读后续的脚本代码,能够明晰其作用。

scripts/build/type.ts
scripts/build/base.ts

首先的,如果版本号的功能是在项目的半途引入(即已经有多篇本地博文,目前需要生成其 git#hash 版本号),显然的,需要批量处理。可以提供如下脚本:

scripts/build/generate-versions.ts
import fs from "node:fs";
import path from "node:path";

import {
  getHashAndLastUpdated,
  postsDir,
  regOnMdFormat,
  versionFile,
} from "~/scripts/build/base";
import { type VersionMap } from "~/scripts/build/type";

function findPostFiles(dir: string) {
  return fs
    .readdirSync(dir, { withFileTypes: true })
    .filter((entry) => entry.isFile() && regOnMdFormat.test(entry.name))
    .map((entry) => entry.name);
}

(function generateVersions() {
  const versions: VersionMap = Object.create(null);
  const posts = findPostFiles(postsDir);

  posts.forEach((post) => {
    const postFilePath = path.join(postsDir, post);

    try {
      const { hash, hashFull } = getHashAndLastUpdated(postFilePath);

      versions[post.replace(regOnMdFormat, "")] = {
        hash,
        hashFull,
        // format,
      };
    } catch (e) {
      console.error(`处理文件 ${post} 时出错:`, e);
    }
  });
  fs.writeFileSync(versionFile, JSON.stringify(versions, null, 2));
  console.log("✓ 版本文件生成完成");
})();
package.json
{
  "scripts": {
-   "build": "pnpm run generate-versions && next build",
    "generate-versions": "npx tsx scripts/build/generate-versions.ts",
  }
}

该段脚本应在初始化 versions.json 文件时只执行一次即可,后期非必要情况无需再次执行,因为我们主要依赖后续的另一个脚本文件:scripts/build/update-after-commit.ts

本地生成 versions.json 文件后,后续以其他形式对其作“局部”更新(而显然的, AI 一开始给出的回答,是每次 build 打包时运行 generate-versions.ts 来对 versions.json 文件的全覆盖操作)。那么很容易想到的,是介入 git#hook。

一种可行方案便是介入 git#hook#post-commit,脚本代码如下:

scripts/build/update-after-commit.ts
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

import { getHashAndLastUpdated, versionFile } from "./base";
import { VersionMap } from "./type";

function updateVersionsAfterCommit() {
  // 读取现有的 versions 文件
  let versions: VersionMap = {};
  if (fs.existsSync(versionFile)) {
    versions = JSON.parse(fs.readFileSync(versionFile, "utf-8"));
  }

  // 获取本次提交变更的文章
  const changedPosts = getChangedPostsInLastCommit();

  if (changedPosts.length === 0) {
    console.log("🔷 没有文章被更新,跳过(本地博文)版本(git#hash)更新\n\n");
    return;
  }

  // 只更新变更的文章
  changedPosts.forEach((filePath) => {
    const fileName = path.basename(filePath);
    const postId = fileName.replace(/\.(mdx?)$/, "");

    try {
      const { hash, hashFull } = getHashAndLastUpdated(filePath);
      versions[postId] = { hash, hashFull };
      console.log(`🔷 ✓ 更新文章版本: ${postId} -> ${hash}`);
    } catch (error) {
      console.error(`🔷 更新文章 ${postId} 版本失败:`, error);
    }
  });

  // 写入更新后的版本信息
  fs.writeFileSync(versionFile, JSON.stringify(versions, null, 2));
  console.log("🔷 ✓ 版本文件更新完成\n\n");
}

function getChangedPostsInLastCommit(): string[] {
  // 获取最近一次提交变更的文件
  const lastCommitFiles = execSync(
    "git diff-tree --no-commit-id --name-only -r HEAD",
    { encoding: "utf-8" },
  ).split("\n");

  // 过滤出 posts 目录下的 md/mdx 文件
  return lastCommitFiles.filter(
    (file) => file.startsWith("_posts/") && /\.(mdx?)$/.test(file),
  );
}

updateVersionsAfterCommit();
.husky/post-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# 如果是版本文件更新的提交,跳过执行
if [ "$VERSIONS_UPDATE" = "1" ]; then
  exit 0
fi

set -e

echo "\n\n ↳ 🟧 开始执行(本地博文)版本号(git#hash)更新..."
if npx tsx scripts/build/update-after-commit.ts; then
  # echo "🟧 ✓ 版本更新脚本执行成功"

  # 检查 versions.json 是否有更改
  if git diff --name-only | grep -q "src/path/to/post-version/versions.json"; then
    echo "🟧 发现版本文件有更新,准备提交..."
    git add src/path/to/post-version/versions.json
    # 设置环境变量标记这是一个版本更新提交
    VERSIONS_UPDATE=1 GIT_QUIET=1 git commit -m "omit: update versions.json" --no-verify
    echo "🟧 ✓ 版本文件已提交"
  # else
  #   echo "🟧 版本文件无更改,跳过提交"
  fi

  exit 0
else
  echo "🟧 ❌ 版本更新失败"
  exit 1
fi

其他感触

AI 加持下效率提升、人机角色的相互勘误与迭代

之前浏览一些博客站点时,便注意到有些博文使用了 commit-hash 作为文章版本号显示在阅读页面中,吾辈自搭建博客的早期,也有过想法实现该功能,但是后续并未成行,一方面原因可能就在于觉得麻烦:涉及到git 命令中的陌生条目、 shell 脚本语法,也想过搜寻网络上有没有类似分享,但遗憾是能看到一些零散片段分享,但是更完整的、系统的分享,并没有。

直到近期,在 AI 辅助下,吾辈顺利完成并上线了版本号功能。

以上主要有对 AI 加持下开发工作效率提升的小小感叹。下面就是小小吐槽部分:

前文提过,本文的大部分代码:是结合 Claude AI 3.5 问询结果,迭代数个版本 + 手动调优完成。为何会有这样的说明,一方面,需承认,个人的开发能力有限,所以确实是 AI 的回答和给出的参考代码,极大的提升了开发该功能模块的效率,但另一方面,也是经过了数个版本的迭代才得到妥善结果(个人需求表达不清可能也占一定因素,但主要的,还是过程中 AI 不可避免的生成了一些不严谨甚至于错误的回答,当前仍然需要人工勘误和反馈)

结合本文的例子说明:

吾辈一开始也并不明确应该介入哪一 git#hook (pre-commit / or: post-commit) 来获取 git#hash。在对 AI 后续提问过程中,AI 也先给出介入 pre-commit 的回答,不过吾辈后续给否了,

原因在于: git#hash 显然在提交前是还没有生成的,而 AI 提供的参考代码中,拿取的 hash 是该文件关联的历史最近一次提交的 hash 值。就算吾辈让他再作优化,也只是对文件是刚创建提交的(未能找到历史 hash 的),还是修改后提交的作了区分。但显然,这不是最佳的版本号记录实现,—— 页面中显示的是旧有 hash 值,而非文件的最新提交 hash。

另一个例子是,在 AI 提供的其中一版本 post-commit 的参考代码中,并未对 versions.json 文件的更新作单独提交,而是在更新 version.json 文件后,以 git commit --amend 的形式提交 —— 显然也是错误回答的实例,--amend 会覆盖刚获取到并记录的 hash 值。

以上,主要还是对上面的几段可用脚本作分享。

基于 git-commit-hash,为发布文章添加版本号功能

https://infen.cc/loc-blog/28_use-git-hash-as-local-posts-versioncode[Copy]
本文作者
Helen4i, TC
创建/发布于
Published On
更新/发布于
Updated On
许可协议
CC BY-NC-SA 4.0

转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。