From f27e51301d5b72c6fc98873a81891c668d9977d9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 09:55:07 +0000 Subject: [PATCH 1/2] test(server): a project reads its own repository when the server inherited GIT_DIR Fails on main: Git child processes inherit GIT_DIR from the server, so the project shows another repository's branch (#35099). AI-assisted; verified with the tests and tools listed above. --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 0fce1ca55f52..1288d000fc10 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -6,6 +6,7 @@ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { assert, it, describe } from "@effect/vitest"; +import { vi } from "vite-plus/test"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Deferred from "effect/Deferred"; @@ -649,6 +650,25 @@ it.effect.each(["bare", "unborn"] as const)( ).pipe(Effect.provide(layerTest)), ); +it.effect("reads the project's own repository when the server inherited GIT_DIR", () => + Effect.scoped( + Effect.gen(function* () { + const path = yield* Path.Path; + const cwd = yield* makeTmpDir(); + const other = yield* makeTmpDir(); + const { initialBranch } = yield* initRepoWithCommit(cwd); + yield* initRepoWithCommit(other); + yield* git(other, ["switch", "-c", "other-repository"]); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* Effect.addFinalizer(() => Effect.sync(() => vi.unstubAllEnvs())); + vi.stubEnv("GIT_DIR", path.join(other, ".git")); + assert.equal((yield* driver.statusDetailsLocal(cwd)).branch, initialBranch); + const refs = yield* driver.listRefs({ cwd }); + assert.isFalse(refs.refs.some((ref) => ref.name === "other-repository")); + }), + ).pipe(Effect.provide(layerTest)), +); + it.effect.each(["on a branch", "detached", "unborn"] as const)( "reads only the repository root for review requests that ignore the branch when %s", (state) => From 79f4643f507da55dd2d2d58f39c9b8d02e528b64 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 10:02:34 +0000 Subject: [PATCH 2/2] fix(server): projects read their own Git repository when the server inherited GIT_DIR Git child processes inherited repository selectors such as GIT_DIR, GIT_WORK_TREE and GIT_INDEX_FILE from the server, so a server started from a Git hook showed that repository's branch and changes in every project. Unset the inherited selectors for every Git spawn; a command's own env may still set them. Fixes pingdotgg/t3code#35099 AI-assisted; verified with the tests and tools listed above. --- .../src/project/RepositoryIdentityResolver.ts | 11 +++++--- apps/server/src/vcs/GitVcsDriver.ts | 1 - apps/server/src/vcs/GitVcsDriverCore.ts | 10 +++++-- apps/server/src/vcs/VcsProcess.ts | 26 +++++++++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/apps/server/src/project/RepositoryIdentityResolver.ts b/apps/server/src/project/RepositoryIdentityResolver.ts index cdf9dc5e1b30..11ae71589528 100644 --- a/apps/server/src/project/RepositoryIdentityResolver.ts +++ b/apps/server/src/project/RepositoryIdentityResolver.ts @@ -12,7 +12,7 @@ import * as Path from "effect/Path"; import * as ProcessRunner from "../processRunner.ts"; import { sourceControlDefinitions } from "../sourceControl/sourceControlDefinitions.ts"; -import { explicitBareRepositoryEnv } from "../vcs/VcsProcess.ts"; +import { explicitBareRepositoryEnv, unsetInheritedRepositoryEnv } from "../vcs/VcsProcess.ts"; import * as HostProcess from "@t3tools/shared/HostProcess"; const DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY = 512; @@ -137,12 +137,17 @@ class RepositoryIdentityCacheKey extends Data.Class<{ readonly isBare: boolean; }> {} +const gitEnv = (platform: NodeJS.Platform): NodeJS.ProcessEnv => ({ + ...unsetInheritedRepositoryEnv(process.env, platform), + ...explicitBareRepositoryEnv(process.env, platform), +}); + const resolveRepositoryIdentityCacheKey = Effect.fn("RepositoryIdentityResolver.resolveCacheKey")( function* (cwd: string) { const processRunner = yield* ProcessRunner.ProcessRunner; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const env = explicitBareRepositoryEnv(process.env, yield* HostProcess.Platform); + const env = gitEnv(yield* HostProcess.Platform); // `git -C` cannot enter a missing folder or a file, so every probe below would fail. const isFolder = yield* fileSystem.stat(cwd).pipe( @@ -231,7 +236,7 @@ const resolveRepositoryIdentityFromCacheKey = Effect.fn( .run({ command: "git", args: ["-C", cacheKey.rootPath, ...(cacheKey.isBare ? ["--git-dir=."] : []), "remote", "-v"], - env: explicitBareRepositoryEnv(process.env, yield* HostProcess.Platform), + env: gitEnv(yield* HostProcess.Platform), timeoutBehavior: "timedOutResult", }) .pipe(Effect.option); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index dbc87e088d90..ff0eac7c0009 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -986,7 +986,6 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( const tempIndexPath = path.join(gitCommonDir, `t3-checkpoint-index-${indexId}`); const hostEnv = yield* HostProcess.Environment; const commitEnv: NodeJS.ProcessEnv = { - ...hostEnv, GIT_INDEX_FILE: tempIndexPath, GIT_AUTHOR_NAME: "T3 Code", GIT_AUTHOR_EMAIL: "t3code@users.noreply.github.com", diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index e04bd48a8c84..d644fc9662e5 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -42,7 +42,11 @@ import { resolveProjectFileBackedSetting } from "@t3tools/shared/projectSettings import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts"; import * as GitVcsDriver from "./GitVcsDriver.ts"; import { removeEmptyDirectory, type RemoveEmptyDirectoryError } from "./removeEmptyDirectory.ts"; -import { explicitBareRepositoryEnv, gitConfigEnv } from "./VcsProcess.ts"; +import { + explicitBareRepositoryEnv, + gitConfigEnv, + unsetInheritedRepositoryEnv, +} from "./VcsProcess.ts"; import { makeWorktreeCheckoutCache } from "./worktreeCheckoutCache.ts"; import { removeLinkedWorktree } from "./worktreeRemoval.ts"; import { resolveWorktreesDirectory } from "../worktreesDirectory.ts"; @@ -993,8 +997,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }), ), ); + const hostEnv = yield* HostProcess.Environment; const baseEnv = { - ...(yield* HostProcess.Environment), + ...hostEnv, + ...unsetInheritedRepositoryEnv(hostEnv, hostPlatform), // Concurrent status polls must not take index.lock to save refreshed indexes. GIT_OPTIONAL_LOCKS: "0", ...input.env, diff --git a/apps/server/src/vcs/VcsProcess.ts b/apps/server/src/vcs/VcsProcess.ts index c379caf48847..4bfdcc90037e 100644 --- a/apps/server/src/vcs/VcsProcess.ts +++ b/apps/server/src/vcs/VcsProcess.ts @@ -134,6 +134,29 @@ export const gitConfigEnv = ( export const explicitBareRepositoryEnv = (env: NodeJS.ProcessEnv, platform: NodeJS.Platform) => gitConfigEnv(env, "safe.bareRepository", "explicit", platform); +const REPOSITORY_SELECTORS = new Set([ + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_COMMON_DIR", + "GIT_INDEX_FILE", + "GIT_OBJECT_DIRECTORY", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", +]); + +/** + * Unsets repository selectors the server inherited, such as a Git hook's `GIT_DIR`, so Git finds + * the repository from cwd. Spread it before a command's own env, which may still set them. + */ +export const unsetInheritedRepositoryEnv = ( + inherited: NodeJS.ProcessEnv, + platform: NodeJS.Platform, +): NodeJS.ProcessEnv => + Object.fromEntries( + Object.keys(inherited) + .filter((name) => REPOSITORY_SELECTORS.has(platform === "win32" ? name.toUpperCase() : name)) + .map((name) => [name, undefined]), + ); + const classifyNonZeroExit = ( stderr: string, classifyFailure: VcsProcessInput["classifyFailure"], @@ -210,6 +233,9 @@ export const make = Effect.gen(function* () { ...(input.stdin !== undefined ? { stdin: input.stdin } : {}), ...(input.onStdoutChunk !== undefined ? { onStdoutChunk: input.onStdoutChunk } : {}), env: { + ...(input.command === "git" + ? unsetInheritedRepositoryEnv(process.env, hostPlatform) + : {}), // Failures are read by English patterns; without LANG, macOS Git follows the system language. // LANGUAGE leaves LC_CTYPE, which hooks inherit, alone. ...(input.command === "git" ? { LANGUAGE: "C" } : {}),