created: 2024-03-05T09:34:01.277Z
git hook を zx で書くとよかった
Git フック を zx で書くとすごいよかったという話。
よかったところ
- js だとややこしい文字列処理の実装が簡単
- 実装ファイルの拡張子を
.mjs
にして jsdoc を書くと補完がバリバリ効く - 実装ファイルの横に
.test.mjs
を置けばnode:assert
でテストがひととおり書ける - 「いまいるブランチを調べる」みたいなコマンドはシェルスクリプトっぽく書ける
zx はハマるところも少なくて本当にシェルと js の良いとこ取りだった。
基本的に nodejs に child_process.spawn
のシンタックスシュガーが入っただけのモノなので nodejs が書ければもうこれを使ったほうがいいと思う。
がんばれば TypeScript の文法を使えるらしいけど、たぶんそれはもうやりすぎなのでやらなかった。
実装
「ブランチ名の prefix に数字があったら GitHub の issue 番号扱いする」というよくあるもの。
拡張子が mjs なのがミソで、node にオプションをつけなくてもちゃんと動き、保管も効く。
#!/usr/bin/env zx
import { writeFileSync } from "node:fs";
/**
* @param {string} branchName
* @param {string} originalCommitMsg
* @returns {string}
*/
export function prepareCommitMsg(branchName, originalCommitMsg) {
if (originalCommitMsg.startsWith("fixup!")) {
return originalCommitMsg;
}
const match = branchName.match(/^(?:\w+\/)?(\d+)-/);
if (!match) {
return originalCommitMsg;
}
const issueNumber = match[1];
const padding = originalCommitMsg.startsWith(" ") ? "" : " ";
return `[#${issueNumber}]${padding}${originalCommitMsg}`;
}
async function main() {
const commitMsgFile = process.argv[3];
const originalCommitMsg = await $`cat ${commitMsgFile}`;
const branchName = await $`git rev-parse --abbrev-ref HEAD`;
const commitMsg = prepareCommitMsg(
branchName.stdout,
originalCommitMsg.stdout
);
writeFileSync(commitMsgFile, commitMsg);
}
main();
import * as assert from "node:assert";
import { prepareCommitMsg } from "./prepare-commit-msg.mjs";
const cases = [
[["123-test-branch", "bugfix"], "[#123] bugfix"],
[["123-test-branch", " bugfix"], "[#123] bugfix"],
[["test-branch", "bugfix"], "bugfix"],
[["topic/123-test-branch", "bugfix"], "[#123] bugfix"],
[["123-test-branch", "fixup! bugfix"], "fixup! bugfix"],
];
for (const c of cases) {
const [args, expected] = c;
assert.equal(prepareCommitMsg(...args), expected);
}
配置
prepare-commit-msg.mjs
みたいな名前をつけて、symlink を貼れば動く。
$ ln -s $(pwd)/scripts/git-hooks/prepare-commit-msg.mjs .git/hooks/prepare-commit-msg