Merge pull request #1550 from Tibsfox/fix/commit-docs-hook-1395-v2
feat(hooks): add check-commit guard for commit_docs enforcement
This commit is contained in:
@@ -430,6 +430,11 @@ async function runCommand(command, args, cwd, raw) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'check-commit': {
|
||||
commands.cmdCheckCommit(cwd, raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'commit-to-subrepo': {
|
||||
const message = args[1];
|
||||
const filesIndex = args.indexOf('--files');
|
||||
|
||||
@@ -960,6 +960,39 @@ function cmdStats(cwd, format, raw) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a commit should be allowed based on commit_docs config.
|
||||
* When commit_docs is false, rejects commits that stage .planning/ files.
|
||||
* Intended for use as a pre-commit hook guard.
|
||||
*/
|
||||
function cmdCheckCommit(cwd, raw) {
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
// If commit_docs is true (or not set), allow all commits
|
||||
if (config.commit_docs !== false) {
|
||||
output({ allowed: true, reason: 'commit_docs_enabled' }, raw, 'allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
// commit_docs is false — check if any .planning/ files are staged
|
||||
try {
|
||||
const staged = execSync('git diff --cached --name-only', { cwd, encoding: 'utf-8' }).trim();
|
||||
const planningFiles = staged.split('\n').filter(f => f.startsWith('.planning/') || f.startsWith('.planning\\'));
|
||||
|
||||
if (planningFiles.length > 0) {
|
||||
error(
|
||||
`commit_docs is false but ${planningFiles.length} .planning/ file(s) are staged:\n` +
|
||||
planningFiles.map(f => ` ${f}`).join('\n') +
|
||||
`\n\nTo unstage: git reset HEAD ${planningFiles.join(' ')}`
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// git diff --cached failed (no staged files or not a git repo) — allow
|
||||
}
|
||||
|
||||
output({ allowed: true, reason: 'no_planning_files_staged' }, raw, 'allowed');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cmdGenerateSlug,
|
||||
cmdCurrentTimestamp,
|
||||
@@ -976,4 +1009,5 @@ module.exports = {
|
||||
cmdTodoMatchPhase,
|
||||
cmdScaffold,
|
||||
cmdStats,
|
||||
cmdCheckCommit,
|
||||
};
|
||||
|
||||
@@ -1694,3 +1694,60 @@ describe('stats command', () => {
|
||||
assert.strictEqual(output.phases[0].status, 'Executed', 'progress should show Executed without verification');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// check-commit command (#1395)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('check-commit command', () => {
|
||||
const { createTempGitProject } = require('./helpers.cjs');
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempGitProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('allows commit when commit_docs is true', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ commit_docs: true })
|
||||
);
|
||||
const result = runGsdTools('check-commit', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.allowed, true);
|
||||
});
|
||||
|
||||
test('allows commit when no .planning/ files staged and commit_docs is false', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ commit_docs: false })
|
||||
);
|
||||
// Stage a non-planning file
|
||||
fs.writeFileSync(path.join(tmpDir, 'src.js'), 'console.log("hi")');
|
||||
execSync('git add src.js', { cwd: tmpDir, stdio: 'pipe' });
|
||||
|
||||
const result = runGsdTools('check-commit', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.allowed, true);
|
||||
});
|
||||
|
||||
test('blocks commit when .planning/ files staged and commit_docs is false', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ commit_docs: false })
|
||||
);
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
|
||||
execSync('git add .planning/STATE.md', { cwd: tmpDir, stdio: 'pipe' });
|
||||
|
||||
const result = runGsdTools('check-commit', tmpDir);
|
||||
assert.ok(!result.success, 'should block commit');
|
||||
assert.ok(result.error.includes('.planning/'), 'error should mention .planning/ files');
|
||||
assert.ok(result.error.includes('unstage'), 'error should suggest unstage command');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user