fix(hooks): end a here-document commit message only at its exact delimiter line

The closing pattern accepted spaces and tabs around the delimiter for both forms, so a body line such as '  EOF' ended the message early. Bash ends <<EOF only at a line that is exactly EOF, and <<-EOF also after leading tabs. The body is now read line by line from the command, and the existing message pattern is back to what it was on main.
This commit is contained in:
L4XB committed 2026-10-23 03:04:13 +02:00
1 parent 7695b357c7
commit 51fa8c3088
2 files changed
+34 -7

No files matched your search

+27 -7
View File
@@ -166,6 +166,25 @@ function findFileIssues(filePath) {
return issues;
}
/**
* The body of the here-document that `value` starts with, as in `$(cat <<'EOF'`, or null.
* It ends at the line that holds only the delimiter, after leading tabs for <<-, which also
* strips them from the body.
* @param {string} value
* @returns {string|null}
*/
function heredocBody(value) {
const opener = /^\$\(\s*cat\s+<<(-?)\s*(['"]?)([A-Za-z_][\w.-]*)\2[ \t]*\r?\n/.exec(value);
if (!opener) return null;
const [, stripTabs, , delimiter] = opener;
const lines = value.slice(opener[0].length).split('\n').map(line => {
const text = line.replace(/\r$/, '');
return stripTabs ? text.replace(/^\t+/, '') : text;
});
const end = lines.indexOf(delimiter);
return end === -1 ? null : lines.slice(0, end).join('\n');
}
/**
* Validate commit message format
* @param {string} command
@@ -175,15 +194,16 @@ function validateCommitMessage(command) {
// Extract commit message from command (quote-aware: when quoted, capture to the
// matching closing quote, consuming escaped chars (\") so an embedded escaped
// quote does not truncate the subject, and allowing the OTHER quote char inside
// the body; when unquoted, capture the full remaining tail, not just the first token).
// A message written as -m "$(cat <<'EOF' ... EOF)", as Claude Code writes them, is
// the body of that here-document.
const messageMatch = command.match(/(?:-m|--message)[=\s]+(?:"\$\(\s*cat\s+<<(?<stripTabs>-?)\s*(?<quote>['"]?)(?<delimiter>[A-Za-z_][\w.-]*)\k<quote>[ \t]*\r?\n(?<heredoc>[\s\S]*?)\r?\n[ \t]*\k<delimiter>[ \t]*\r?(?:\n|$)|"(?<double>(?:\\.|[^"\\])*)"|'(?<single>(?:\\.|[^'\\])*)'|(?<bare>[^"']+?)\s*$)/);
// the body; when unquoted, capture the full remaining tail, not just the first token)
const messageMatch = command.match(/(?:-m|--message)[=\s]+(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^"']+?)\s*$)/);
if (!messageMatch) return null;
const { stripTabs, heredoc, double, single, bare } = messageMatch.groups;
// <<- strips the leading tabs of each line of the here-document
const message = stripTabs ? heredoc.replace(/^\t+/gm, '') : heredoc ?? double ?? single ?? bare;
// A message written as -m "$(cat <<'EOF' ... EOF)", as Claude Code writes them, is the
// body of that here-document. It is read from the command itself, since it may hold quotes.
const heredoc = messageMatch[1]?.startsWith('$(')
? heredocBody(command.slice(messageMatch.index + messageMatch[0].indexOf('"') + 1))
: null;
const message = heredoc ?? messageMatch[1] ?? messageMatch[2] ?? messageMatch[3];
// The format rules apply to the subject: git drops leading blank lines and trailing
// whitespace, and the first line left is the subject.
const subject = (message.split('\n').find(line => line.trim()) || '').trimEnd();
@@ -525,6 +525,13 @@ if (test('still reports a bad subject inside a here-document', () => {
assert.deepStrictEqual(res.issues.map(i => i.type), ['format', 'punctuation']);
})) passed++; else failed++;
if (test('ends a here-document only at a line that holds just the delimiter', () => {
const res = hook.validateCommitMessage(
'git commit -m "$(cat <<\'EOF\'\nfix: keep reading\n\n EOF\nEOF \nmore body\nEOF\n)"'
);
assert.strictEqual(res.message, 'fix: keep reading\n\n EOF\nEOF \nmore body');
})) passed++; else failed++;
if (test('reads a <<- here-document without its leading tabs', () => {
const res = hook.validateCommitMessage('git commit -m "$(cat <<-"END"\n\tfix: tabbed subject\n\tEND\n)"');
assert.strictEqual(res.message, 'fix: tabbed subject');