diff --git a/scripts/hooks/check-console-log.js b/scripts/hooks/check-console-log.js index 03a5899f6..021d617ef 100755 --- a/scripts/hooks/check-console-log.js +++ b/scripts/hooks/check-console-log.js @@ -28,6 +28,18 @@ const EXCLUDED_PATTERNS = [ ]; const MAX_DIRECT_STDIN_BYTES = 16 * 1024 * 1024; + +// This Stop hook inspects the session's working directory. Inherited GIT_* +// variables from another Git process can silently redirect discovery and diff +// commands to a different repository, so give every Git call the same clean env. +function sessionGitOptions() { + const env = { ...process.env }; + for (const name of Object.keys(env)) { + if (name.toUpperCase().startsWith('GIT_')) delete env[name]; + } + return { env }; +} + let data = ''; let stdinBytes = 0; let oversized = false; @@ -62,15 +74,16 @@ function passThroughAndExit() { process.stdin.on('end', () => { try { - if (!isGitRepo()) { + const gitOptions = sessionGitOptions(); + if (!isGitRepo(gitOptions)) { passThroughAndExit(); return; } // git reports paths from the repository root, and the session can run in a // subdirectory of it, so read each file from the root. - const root = runCommand('git rev-parse --show-toplevel').output; - const files = getGitModifiedFiles(['\\.tsx?$', '\\.jsx?$']) + const root = runCommand('git rev-parse --show-toplevel', gitOptions).output; + const files = getGitModifiedFiles(['\\.tsx?$', '\\.jsx?$'], gitOptions) .filter(f => fs.existsSync(path.join(root, f))) .filter(f => !EXCLUDED_PATTERNS.some(pattern => pattern.test(f))); diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index cc2b6c21e..416c1c21f 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -537,23 +537,25 @@ function runCommand(cmd, options = {}) { } /** - * Check if current directory is a git repository + * Check if current directory is a git repository. + * @param {object} [options] - Options forwarded to runCommand, including env. */ -function isGitRepo() { - return runCommand('git rev-parse --git-dir').success; +function isGitRepo(options = {}) { + return runCommand('git rev-parse --git-dir', options).success; } /** * Get git modified files, optionally filtered by regex patterns * @param {string[]} patterns - Array of regex pattern strings to filter files. * Invalid patterns are silently skipped. + * @param {object} [options] - Options forwarded to every Git subprocess. * @returns {string[]} Array of modified file paths */ -function getGitModifiedFiles(patterns = []) { - if (!isGitRepo()) return []; +function getGitModifiedFiles(patterns = [], options = {}) { + if (!isGitRepo(options)) return []; // Paths from the repository root even when diff.relative is set. - const result = runCommand('git -c diff.relative=false diff --name-only HEAD'); + const result = runCommand('git -c diff.relative=false diff --name-only HEAD', options); if (!result.success) return []; let files = result.output.split('\n').filter(Boolean); diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index af257f8e0..2867737ea 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -1230,6 +1230,45 @@ async function runTests() { passed++; else failed++; + if ( + await asyncTest('uses the session repository despite inherited Git environment overrides', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-console-git-env-')); + try { + const makeRepo = (name, file) => { + const repo = path.join(root, name); + fs.mkdirSync(path.join(repo, 'src'), { recursive: true }); + execFileSync('git', ['init', '-q'], { cwd: repo }); + fs.writeFileSync(path.join(repo, 'src', file), 'export const value = 1;\n'); + execFileSync('git', ['add', '-A'], { cwd: repo }); + execFileSync('git', ['-c', 'user.email=t@t.local', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-q', '-m', 'seed'], { cwd: repo }); + return repo; + }; + const sessionRepo = makeRepo('session', 'local.ts'); + const foreignRepo = makeRepo('foreign', 'foreign.ts'); + fs.writeFileSync(path.join(foreignRepo, 'src', 'foreign.ts'), 'console.log("foreign");\n'); + const env = { + GIT_DIR: path.join(foreignRepo, '.git'), + GIT_WORK_TREE: foreignRepo, + GIT_INDEX_FILE: path.join(foreignRepo, '.git', 'index') + }; + + const clean = await runScript(path.join(scriptsDir, 'check-console-log.js'), '{}', env, sessionRepo); + assert.strictEqual(clean.code, 0); + assert.ok(!clean.stderr.includes('console.log found'), `foreign repository leaked into hook: ${clean.stderr}`); + + fs.writeFileSync(path.join(sessionRepo, 'src', 'local.ts'), 'console.log("local");\n'); + const local = await runScript(path.join(scriptsDir, 'check-console-log.js'), '{}', env, sessionRepo); + assert.strictEqual(local.code, 0); + assert.ok(local.stderr.includes('console.log found in src/local.ts'), local.stderr); + assert.ok(!local.stderr.includes('foreign.ts'), local.stderr); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + // session-end.js tests console.log('\nsession-end.js:');