修复 Hook 链继承仓库定位变量导致真实仓库被夹具污染
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m47s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 3m57s
Project CI / Native shell tests (push) Successful in 7m40s
Project CI / Backend tests (push) Successful in 8m27s
Project CI / AI game creator shell web tests (push) Successful in 12m53s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m47s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 3m57s
Project CI / Native shell tests (push) Successful in 7m40s
Project CI / Backend tests (push) Successful in 8m27s
Project CI / AI game creator shell web tests (push) Successful in 12m53s
- .husky/pre-commit 与 .husky/pre-push 入口清除 GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE 等仓库定位变量 - scripts/check-repository-ci.sh 增加同样的清理,保证本地门禁与 CI 走同一套隔离 - scripts/git-hooks.test.mjs 夹具 Git 调用前自检仓库归属并禁用 Hook,命令落到外部仓库时直接失败 - 守卫用例的子进程必须真的继承 GIT_DIR,避免隔离断言空转 - 更新开发运维文档与 shared-memory/pitfalls.md,记录链接工作树注入形态与配置修复步骤
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 链接工作树推送时 Git 会向 Hook 注入 GIT_DIR 等仓库定位变量;npm 链上的测试夹具会继承
|
||||
# 它们并绕过 cwd 写到真实仓库,这里统一清除,保证门禁只作用于当前工作树。
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-${1:-}}"
|
||||
head_ref="${REPOSITORY_CI_HEAD_REF:-${2:-HEAD}}"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
@@ -20,6 +21,8 @@ const packageJson = JSON.parse(
|
||||
);
|
||||
|
||||
test('pre-commit hook fixes staged imports and formatting without swallowing unstaged work', () => {
|
||||
assertGuardChildInheritsPoisonedEnvironment();
|
||||
|
||||
assert.equal(packageJson.scripts.prepare, 'husky');
|
||||
assert.equal(packageJson.scripts['format:staged'], 'lint-staged');
|
||||
assert.deepEqual(packageJson['lint-staged'], {
|
||||
@@ -29,9 +32,10 @@ test('pre-commit hook fixes staged imports and formatting without swallowing uns
|
||||
],
|
||||
'*.rs': ['node scripts/lint-staged-rustfmt.mjs'],
|
||||
});
|
||||
assert.equal(
|
||||
assertHookClearsGitEnvironment(
|
||||
'pre-commit',
|
||||
readFileSync(join(repoRoot, '.husky', 'pre-commit'), 'utf8'),
|
||||
'npm run format:staged\n',
|
||||
'npm run format:staged',
|
||||
);
|
||||
|
||||
const tempRepo = createTempDirectory('genarrative-git-hooks-');
|
||||
@@ -162,6 +166,8 @@ test('pre-commit hook fixes staged imports and formatting without swallowing uns
|
||||
});
|
||||
|
||||
test('pre-push runs repository parity only for master updates', () => {
|
||||
assertGuardChildInheritsPoisonedEnvironment();
|
||||
|
||||
const tempDir = createTempDirectory('genarrative-pre-push-');
|
||||
try {
|
||||
const npmLog = join(tempDir, 'repo', 'npm.log');
|
||||
@@ -291,10 +297,12 @@ test('hook fixtures do not mutate the calling linked worktree or its index', ()
|
||||
env: {
|
||||
...isolatedGitEnvironment(),
|
||||
NODE_TEST_CONTEXT: undefined,
|
||||
GENARRATIVE_HOOK_GUARD_CHILD: '1',
|
||||
GIT_DIR: gitDir,
|
||||
GIT_COMMON_DIR: join(outerRepo, '.git'),
|
||||
GIT_WORK_TREE: worktree,
|
||||
GIT_INDEX_FILE: indexPath,
|
||||
GIT_PREFIX: '',
|
||||
GIT_CONFIG_COUNT: '1',
|
||||
GIT_CONFIG_KEY_0: 'core.worktree',
|
||||
GIT_CONFIG_VALUE_0: worktree,
|
||||
@@ -353,7 +361,15 @@ test('Gitea Repository checks and master pre-push share the same repository comm
|
||||
);
|
||||
assert.match(repositoryScript, /npm run build/u);
|
||||
assert.match(repositoryScript, /git diff --check/u);
|
||||
assert.equal(prePushHook, 'npm run check:pre-push-master -- "$@"\n');
|
||||
assertHookClearsGitEnvironment(
|
||||
'pre-push',
|
||||
prePushHook,
|
||||
'npm run check:pre-push-master -- "$@"',
|
||||
);
|
||||
assert.match(
|
||||
repositoryScript,
|
||||
/^unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR/mu,
|
||||
);
|
||||
});
|
||||
|
||||
function readFileOrEmpty(path) {
|
||||
@@ -376,11 +392,38 @@ function toBashPath(path) {
|
||||
}
|
||||
|
||||
function git(cwd, ...args) {
|
||||
return execFileSync('git', args, {
|
||||
const env = isolatedGitEnvironment();
|
||||
assertFixtureRepository(cwd, env);
|
||||
return execFileSync(
|
||||
'git',
|
||||
['--no-pager', '-c', `core.hooksPath=${nullDevice}`, ...args],
|
||||
{
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 夹具命令必须落在夹具自己的仓库:继承的 GIT_DIR/GIT_WORK_TREE 优先级高于 cwd,曾让夹具把
|
||||
// 身份、core.bare 与 core.worktree 写进调用方的真实仓库。
|
||||
function assertFixtureRepository(cwd, env) {
|
||||
const probe = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env: isolatedGitEnvironment(),
|
||||
env,
|
||||
});
|
||||
if (probe.status !== 0) {
|
||||
// 此时夹具目录还不是仓库(例如首次 git init),命令本身会把它建起来。
|
||||
return;
|
||||
}
|
||||
const toplevel = realpathSync.native(probe.stdout.trim());
|
||||
const expected = realpathSync.native(resolve(cwd));
|
||||
assert.equal(
|
||||
toplevel,
|
||||
expected,
|
||||
`夹具 Git 命令会落到外部仓库:cwd=${expected} toplevel=${toplevel}`,
|
||||
);
|
||||
}
|
||||
|
||||
function isolatedGitEnvironment() {
|
||||
@@ -391,6 +434,41 @@ function isolatedGitEnvironment() {
|
||||
);
|
||||
}
|
||||
|
||||
// 守卫用例把本文件跑在毒化环境里;子进程必须真的继承 GIT_DIR,否则断言会空转。
|
||||
function assertGuardChildInheritsPoisonedEnvironment() {
|
||||
if (process.env.GENARRATIVE_HOOK_GUARD_CHILD !== '1') {
|
||||
return;
|
||||
}
|
||||
assert.ok(process.env.GIT_DIR, '守卫子进程必须继承 GIT_DIR');
|
||||
assert.equal(
|
||||
Object.hasOwn(isolatedGitEnvironment(), 'GIT_DIR'),
|
||||
false,
|
||||
'夹具子进程环境必须清除 GIT_DIR',
|
||||
);
|
||||
}
|
||||
|
||||
function assertHookClearsGitEnvironment(hookName, contents, command) {
|
||||
const lines = contents.trimEnd().split('\n');
|
||||
assert.equal(
|
||||
lines.at(-1),
|
||||
command,
|
||||
`${hookName} 最后一行必须保持原有钩子命令`,
|
||||
);
|
||||
const unsetLine = lines.find((line) => line.startsWith('unset '));
|
||||
assert.ok(unsetLine, `${hookName} 必须先清除 Git 注入的仓库定位变量`);
|
||||
const cleared = new Set(unsetLine.split(/\s+/u));
|
||||
for (const variable of [
|
||||
'GIT_DIR',
|
||||
'GIT_WORK_TREE',
|
||||
'GIT_INDEX_FILE',
|
||||
'GIT_COMMON_DIR',
|
||||
]) {
|
||||
assert.ok(cleared.has(variable), `${hookName} 必须清除 ${variable}`);
|
||||
}
|
||||
}
|
||||
|
||||
const nullDevice = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
||||
|
||||
function createTempDirectory(prefix) {
|
||||
const tempRoot = join(homedir(), 'data', 'tmp');
|
||||
mkdirSync(tempRoot, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user