最前
如果博文目录和部署的站点源码同在一个项目中,则天然的可以使用每次创建(或更新)博文的 git-commit-hash 值作为博文的版本号。以下为本站当前实际投入使用的相关代码段分享,如果读者也有类似功能的需求,尽可按需取用。
(另外:本文的大部分代码:结合 Claude AI 3.5 问询结果,迭代数个版本 + 手动调优完成,已验证可行性)
正文
我们的最终成果,是在项目指定目录下持久化一份版本记录文件,形式如下:
{
"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 。
基本的工具函数和类型文件首先给出(在折叠代码块中),阅读后续的脚本代码,能够明晰其作用。
首先的,如果版本号的功能是在项目的半途引入(即已经有多篇本地博文,目前需要生成其 git#hash 版本号),显然的,需要批量处理。可以提供如下脚本:
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("✓ 版本文件生成完成");
})();
{
"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,脚本代码如下:
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();
#!/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]转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。