Add commit message normalizer hook and install script

- scripts/normalize-commit-msg.js: capitalizes subject, strips trailing
  period, trims whitespace, warns when subject > 72 chars
- scripts/commit-msg: shell wrapper symlinked into .git/hooks/commit-msg
- scripts/install-hooks.sh: contributor setup script (sh scripts/install-hooks.sh)
- scripts/package.json: test runner + hooks:install npm script
- scripts/__tests__/normalize-commit-msg.test.js: 15 unit tests

Nightshift-Task: commit-normalize
Nightshift-Ref: https://github.com/marcus/nightshift
This commit is contained in:
2026-03-20 02:04:04 -04:00
parent ccd0fb2155
commit 73e7967735
6 changed files with 1484 additions and 0 deletions

95
scripts/normalize-commit-msg.js Executable file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env node
/**
* normalize-commit-msg.js
*
* Git commit-msg hook: reads the commit message file, applies normalization
* rules to the subject line, rewrites the file in place.
*
* Rules:
* 1. Trim leading/trailing whitespace from the subject line
* 2. Capitalize the first letter of the subject
* 3. Strip a trailing period from the subject
* 4. Warn (but do not block) if the subject exceeds 72 characters
*/
'use strict';
const fs = require('fs');
const MAX_SUBJECT_LEN = 72;
/**
* Normalize the subject line of a commit message.
* Returns { subject, warned } where warned is true if a length warning was emitted.
*
* @param {string} subject
* @returns {{ subject: string, warned: boolean }}
*/
function normalizeSubject(subject) {
let s = subject.trimEnd();
// Trim leading whitespace
s = s.trimStart();
// Capitalize first letter
if (s.length > 0) {
s = s[0].toUpperCase() + s.slice(1);
}
// Strip trailing period
if (s.endsWith('.')) {
s = s.slice(0, -1);
}
const warned = s.length > MAX_SUBJECT_LEN;
return { subject: s, warned };
}
/**
* Normalize a full commit message string.
* Only the subject line (first non-empty, non-comment line) is modified.
*
* @param {string} message
* @returns {{ message: string, warned: boolean }}
*/
function normalizeMessage(message) {
const lines = message.split('\n');
let warned = false;
// Find the subject line (first non-comment line)
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.startsWith('#')) {
const result = normalizeSubject(line);
lines[i] = result.subject;
warned = result.warned;
break;
}
}
return { message: lines.join('\n'), warned };
}
// Only run as a hook when invoked directly (not when required in tests)
if (require.main === module) {
const msgFile = process.argv[2];
if (!msgFile) {
process.stderr.write('commit-msg hook: no message file argument\n');
process.exit(1);
}
const original = fs.readFileSync(msgFile, 'utf8');
const { message, warned } = normalizeMessage(original);
if (warned) {
process.stderr.write(
`commit-msg warning: subject line exceeds ${MAX_SUBJECT_LEN} characters — consider shortening it.\n`
);
}
fs.writeFileSync(msgFile, message, 'utf8');
process.exit(0);
}
module.exports = { normalizeSubject, normalizeMessage };