Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/server/src/project/RepositoryIdentityResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) =>
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions apps/server/src/vcs/VcsProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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" } : {}),
Expand Down
Loading