diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index 30a589c1..fa5faead 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -853,7 +853,10 @@ function isReadOnlyGitIntrospection(command) { } if (subcommand === 'diff') { - return args.length <= 1 && args.every(arg => ['--name-only', '--name-status'].includes(arg)); + const allowedDiffArgs = new Set(['--name-only', '--name-status', '--cached', '--staged', '--stat']); + // git diff without arguments is read-only introspection + if (args.length === 0) return true; + return args.length <= 2 && args.every(arg => allowedDiffArgs.has(arg)); } if (subcommand === 'log') { @@ -861,7 +864,25 @@ function isReadOnlyGitIntrospection(command) { } if (subcommand === 'show') { - return args.length === 1 && !args[0].startsWith('--') && /^[a-zA-Z0-9._:/ -]+$/.test(args[0]); + // Permite: git show , git show --stat, git show --name-only, + // git show --stat, git show --name-only + if (args.length === 0) return false; + if (args.length === 1) { + const arg = args[0]; + if (arg === '--stat' || arg === '--name-only') return true; + // ref + return !arg.startsWith('--') && /^[a-zA-Z0-9._:/ -]+$/.test(arg); + } + if (args.length === 2) { + const [first, second] = args; + // ref + flag + if (!first.startsWith('--') && /^[a-zA-Z0-9._:/ -]+$/.test(first) && + (second === '--stat' || second === '--name-only')) { + return true; + } + return false; + } + return false; } if (subcommand === 'branch') { diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 759a3a97..610b782a 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -1788,6 +1788,70 @@ function runTests() { 'destructive gate is exempt from dampening'); })) passed++; else failed++; + // --- Novos comandos Git read-only --- + console.log('\n Novos comandos Git read-only:'); + + clearState(); + if (test('allows git diff --cached', () => { + expectAllow('git diff --cached', 'git diff --cached'); + })) passed++; else failed++; + + clearState(); + if (test('allows git diff --staged', () => { + expectAllow('git diff --staged', 'git diff --staged'); + })) passed++; else failed++; + + clearState(); + if (test('allows git diff --stat', () => { + expectAllow('git diff --stat', 'git diff --stat'); + })) passed++; else failed++; + + clearState(); + if (test('allows git diff --name-only --cached', () => { + expectAllow('git diff --name-only --cached', 'git diff --name-only --cached'); + })) passed++; else failed++; + + clearState(); + if (test('allows git show --stat', () => { + expectAllow('git show --stat', 'git show --stat'); + })) passed++; else failed++; + + clearState(); + if (test('allows git show --name-only', () => { + expectAllow('git show --name-only', 'git show --name-only'); + })) passed++; else failed++; + + clearState(); + if (test('allows git show HEAD --stat', () => { + expectAllow('git show HEAD --stat', 'git show HEAD --stat'); + })) passed++; else failed++; + + clearState(); + if (test('allows git show HEAD --name-only', () => { + expectAllow('git show HEAD --name-only', 'git show HEAD --name-only'); + })) passed++; else failed++; + + // Garantir que comandos destrutivos continuam negados + clearState(); + if (test('still denies git reset --hard', () => { + expectDestructiveDeny('git reset --hard', 'git reset --hard'); + })) passed++; else failed++; + + clearState(); + if (test('still denies git checkout -f', () => { + expectDestructiveDeny('git checkout -f main', 'git checkout -f'); + })) passed++; else failed++; + + clearState(); + if (test('still denies git clean -fd', () => { + expectDestructiveDeny('git clean -fd', 'git clean -fd'); + })) passed++; else failed++; + + clearState(); + if (test('still denies git push --force', () => { + expectDestructiveDeny('git push --force origin main', 'git push --force'); + })) passed++; else failed++; + // Cleanup only the temp directory created by this test file. try { if (fs.existsSync(stateDir)) {