mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-05-14 02:56:38 +02:00
* fix(#3129): replace bypassed bash regex with token-walk git-cmd.js classifier Root cause: gsd-validate-commit.sh used: if [[ "$CMD" =~ ^git[[:space:]]+commit ]] This regex silently bypasses Conventional Commits enforcement for: git -C /path commit -m ... (working-directory prefix) GIT_AUTHOR_NAME=x git commit (env-var prefix) /usr/bin/git commit -m ... (full-path executable) Fix: introduces hooks/lib/git-cmd.js with isGitSubcommand(cmd, sub) — a token-walk classifier that handles all four forms by: 1. Skipping leading VAR=VALUE env assignments 2. Validating the git executable (basename check for full-path support) 3. Consuming git global options (-C <path>, --git-dir=, -p, etc.) 4. Checking the subcommand token The hook delegates to this classifier via node shell-out. node is already called twice in this hook (config check + JSON parse), so no new runtime dependency. This becomes the single source of truth for all hooks that gate on git subcommands (pre-commit-review-gate, post-push-verify, etc.). Regression test: 27 assertions — tokenize correctness, 12 must-match cases (including all 3 bypass forms), 8 must-not-match cases, 3 source checks. All are real behavioral tests, not string comparisons. Suite: 7035/7035. Closes #3129. * fix(lint+hook+changeset): allow-test-rule, fix HOOK_DIR quote injection, fix changeset pr+typo
120 lines
5.1 KiB
JavaScript
120 lines
5.1 KiB
JavaScript
'use strict';
|
|
// allow-test-rule: reads hook shell script to verify delegation pattern — structural contract test, not source-grep
|
|
|
|
// Regression tests for bug #3129.
|
|
//
|
|
// gsd-validate-commit.sh used `[[ "$CMD" =~ ^git[[:space:]]+commit ]]` to
|
|
// detect git commit invocations. This regex silently bypasses Conventional
|
|
// Commits enforcement for three real git commit forms:
|
|
// 1. git -C /some/path commit -m "..." (working-directory prefix)
|
|
// 2. GIT_AUTHOR_NAME=x git commit "..." (env-var prefix)
|
|
// 3. /usr/bin/git commit -m "..." (full path)
|
|
//
|
|
// Fix: the hook delegates detection to hooks/lib/git-cmd.js isGitSubcommand(),
|
|
// a token-walk classifier that correctly handles all four forms. The module
|
|
// is the canonical single source of truth for all hooks that gate on git commits.
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
const fs = require('node:fs');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const { isGitSubcommand, tokenize } = require(path.join(ROOT, 'hooks', 'lib', 'git-cmd.js'));
|
|
|
|
// ── tokenize ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('git-cmd.js tokenize', () => {
|
|
test('splits bare command', () => {
|
|
assert.deepEqual(tokenize('git commit -m "msg"'), ['git', 'commit', '-m', 'msg']);
|
|
});
|
|
test('handles single-quoted args', () => {
|
|
assert.deepEqual(tokenize("git commit -m 'my message'"), ['git', 'commit', '-m', 'my message']);
|
|
});
|
|
test('handles env-prefix assignment', () => {
|
|
assert.deepEqual(
|
|
tokenize('GIT_AUTHOR_NAME=Alice git commit -m "fix"'),
|
|
['GIT_AUTHOR_NAME=Alice', 'git', 'commit', '-m', 'fix'],
|
|
);
|
|
});
|
|
test('handles -C path', () => {
|
|
assert.deepEqual(
|
|
tokenize('git -C /some/path commit -m "x"'),
|
|
['git', '-C', '/some/path', 'commit', '-m', 'x'],
|
|
);
|
|
});
|
|
});
|
|
|
|
// ── isGitSubcommand: must-match cases ────────────────────────────────────────
|
|
|
|
describe('git-cmd.js isGitSubcommand: should match commit', () => {
|
|
const cases = [
|
|
['bare form', 'git commit -m "feat: add thing"'],
|
|
['single-quoted message', "git commit -m 'fix: typo'"],
|
|
['with --no-verify', 'git commit --no-verify -m "wip"'],
|
|
['-C path form (bug #3129)', 'git -C /some/path commit -m "fix: x"'],
|
|
['env-prefix form (bug #3129)', 'GIT_AUTHOR_NAME=Alice git commit -m "fix"'],
|
|
['full-path form (bug #3129)', '/usr/bin/git commit -m "feat: y"'],
|
|
['multiple env vars', 'GIT_AUTHOR_NAME=A GIT_AUTHOR_EMAIL=b@c git commit -m "x"'],
|
|
['--git-dir= flag', 'git --git-dir=.git commit -m "x"'],
|
|
['--git-dir two-token', 'git --git-dir .git commit -m "x"'],
|
|
['--no-pager before subcommand', 'git --no-pager commit -m "x"'],
|
|
['-C + full path', '/usr/bin/git -C /proj commit -m "x"'],
|
|
['-p paginate flag', 'git -p commit -m "x"'],
|
|
];
|
|
for (const [desc, cmd] of cases) {
|
|
test(desc, () => {
|
|
assert.ok(isGitSubcommand(cmd, 'commit'), `Expected match for: ${cmd}`);
|
|
});
|
|
}
|
|
});
|
|
|
|
// ── isGitSubcommand: must-not-match cases ────────────────────────────────────
|
|
|
|
describe('git-cmd.js isGitSubcommand: should NOT match commit', () => {
|
|
const cases = [
|
|
['git push', 'git push origin main'],
|
|
['git status', 'git status'],
|
|
['git add', 'git add .'],
|
|
['git log', 'git log --oneline'],
|
|
['not git at all', 'npm install'],
|
|
['empty string', ''],
|
|
['git checkout (not commit)', 'git checkout main'],
|
|
['git -C path push', 'git -C /path push'],
|
|
];
|
|
for (const [desc, cmd] of cases) {
|
|
test(desc, () => {
|
|
assert.ok(!isGitSubcommand(cmd, 'commit'), `Expected NO match for: ${cmd}`);
|
|
});
|
|
}
|
|
});
|
|
|
|
// ── gsd-validate-commit.sh source check ──────────────────────────────────────
|
|
|
|
describe('gsd-validate-commit.sh delegates to git-cmd.js', () => {
|
|
const hookSrc = fs.readFileSync(
|
|
path.join(ROOT, 'hooks', 'gsd-validate-commit.sh'), 'utf8',
|
|
);
|
|
|
|
test('hook no longer uses the stale ^git\\s+commit bash regex', () => {
|
|
assert.ok(
|
|
!hookSrc.includes('^git[[:space:]]+commit'),
|
|
'gsd-validate-commit.sh still uses the bypassed regex — fix not applied',
|
|
);
|
|
});
|
|
|
|
test('hook delegates to git-cmd.js isGitSubcommand', () => {
|
|
assert.ok(
|
|
hookSrc.includes('git-cmd.js') && hookSrc.includes('isGitSubcommand'),
|
|
'gsd-validate-commit.sh does not reference git-cmd.js or isGitSubcommand',
|
|
);
|
|
});
|
|
|
|
test('hooks/lib/git-cmd.js exists at the expected install path', () => {
|
|
assert.ok(
|
|
fs.existsSync(path.join(ROOT, 'hooks', 'lib', 'git-cmd.js')),
|
|
'hooks/lib/git-cmd.js does not exist — library file missing',
|
|
);
|
|
});
|
|
});
|