From 5d0fe23924cd739e9806a77463ee1a9248021159 Mon Sep 17 00:00:00 2001 From: T3 Code PR Stack <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:37:28 +0200 Subject: [PATCH] test(stack): stop the git fixtures inheriting the caller's repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stack fixtures spawn git with the ambient environment. Git's repo-scoping variables — GIT_DIR, GIT_WORK_TREE and friends — override `cwd` when set, so a run that inherits them from a parent git process (a hook, or anything the ship gate invokes) would have `git init --bare ` operate on the developer's real checkout instead, setting core.bare on it and breaking status/add/commit. Scrub those variables before spawning, the same precaution `.githooks/pre-push` already takes with `git rev-parse --local-env-vars`. Honest scope: this is hardening, not a diagnosed fix. Someone's checkout did get core.bare set twice while working in this repository, and this was my hypothesis for it — but the experiment refutes it. Reverting the change and re-running the fixtures with GIT_DIR/GIT_WORK_TREE exported does not reproduce the corruption, so the real trigger is still unidentified and most likely lies elsewhere (`prepare` / install-git-hooks is the next place to look, since .git was touched at exactly those moments). The scrub is still correct on its own terms — a fixture must not depend on the environment of whatever invoked it — and `--bare` itself is not the problem: the fixtures need bare repos because git refuses to push into a non-bare checkout's current branch, and the tooling under test pushes to origin/upstream. Verified: 38/38 pass normally, and with GIT_DIR/GIT_WORK_TREE exported the fixtures no longer touch the real repository. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/rebase-pr-stack.test.ts | 49 ++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/scripts/rebase-pr-stack.test.ts b/scripts/rebase-pr-stack.test.ts index 3e4d99a7b1a1..ea8e36609f15 100644 --- a/scripts/rebase-pr-stack.test.ts +++ b/scripts/rebase-pr-stack.test.ts @@ -360,6 +360,44 @@ interface FixtureOptions { readonly advanceTopAfterIntegration?: boolean; } +/** + * Git's repository-scoping variables. Inherited from a parent git process — a + * hook, or anything the ship gate runs — they override `cwd` entirely, so these + * fixtures would operate on the developer's real checkout instead of the temp + * directory. `git init --bare ` then sets `core.bare = true` on the actual + * repository, which breaks `git status`, `add` and `commit` until someone + * notices and unsets it. + * + * This is the same precaution `.githooks/pre-push` takes with + * `git rev-parse --local-env-vars`; the list is hardcoded here so the fixture + * does not need a working git repository to discover it. + */ +const GIT_LOCAL_ENV_VARS = [ + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_INDEX_FILE", + "GIT_OBJECT_DIRECTORY", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_COMMON_DIR", + "GIT_NAMESPACE", + "GIT_CEILING_DIRECTORIES", + "GIT_PREFIX", + "GIT_SUPER_PREFIX", + "GIT_INTERNAL_SUPER_PREFIX", +] as const; + +const gitFixtureEnv = (): NodeJS.ProcessEnv => { + const env: NodeJS.ProcessEnv = { + ...process.env, + GIT_AUTHOR_NAME: "Stack Test", + GIT_AUTHOR_EMAIL: "stack-test@example.com", + GIT_COMMITTER_NAME: "Stack Test", + GIT_COMMITTER_EMAIL: "stack-test@example.com", + }; + for (const name of GIT_LOCAL_ENV_VARS) delete env[name]; + return env; +}; + function runGit( cwd: string, args: ReadonlyArray, @@ -368,13 +406,7 @@ function runGit( const result = NodeChildProcess.spawnSync("git", [...args], { cwd, encoding: "utf8", - env: { - ...process.env, - GIT_AUTHOR_NAME: "Stack Test", - GIT_AUTHOR_EMAIL: "stack-test@example.com", - GIT_COMMITTER_NAME: "Stack Test", - GIT_COMMITTER_EMAIL: "stack-test@example.com", - }, + env: gitFixtureEnv(), }); if (!options.allowFailure && result.status !== 0) { throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); @@ -412,6 +444,9 @@ function isAncestor(repository: string, parent: string, child: string): boolean const result = NodeChildProcess.spawnSync("git", ["merge-base", "--is-ancestor", parent, child], { cwd: repository, encoding: "utf8", + // Same scrub as the fixture writer: an inherited GIT_DIR would answer this + // question about the developer's repository rather than the fixture's. + env: gitFixtureEnv(), }); return result.status === 0; }