authorgravatar for 76637177+SunkenInTime@users.noreply.github.comDara Adedeji <76637177+SunkenInTime@users.noreply.github.com> 2026-10-10 02:08:22-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 23:08:22-07:00
log9cc57680cb5f762f553acd1fa5f3efcf17d8fc67
tree264aeae3e12300cef198a95a930ec787da17c025
parentc3243f05088c3c20e9439f285fc6d7b0bfe1747d
signature Signed by PGP key B5690EEEBB952194

perf(server): run Git for Windows' real git.exe, not its launcher (#17707)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

3 files changed, 144 insertions(+), 3 deletions(-)

apps/server/src/vcs/GitVcsDriverCore.ts+8-2
......@@ -32,6 +32,7 @@ import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3
3232import * as HostProcess from "@t3tools/shared/HostProcess";
3333import { compactTraceAttributes } from "@t3tools/shared/observability";
3434import { decodeJsonResult } from "@t3tools/shared/schemaJson";
35import { resolveSpawnCommand } from "@t3tools/shared/shell";
3536import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";
3637import { resolveProjectFileBackedSetting } from "@t3tools/shared/projectSettings";
3738import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts";
......@@ -977,11 +978,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
977978 ...input.env,
978979 ...trace2Monitor.env,
979980 };
981 const spawnEnv = { ...env, ...windowsLongPathConfigEnv(hostPlatform, env) };
982 const resolved = yield* resolveSpawnCommand("git", [], { env: spawnEnv });
983 // A git.cmd wrapper would need cmd.exe, which cuts multi-line commit
984 // messages at the first newline; leave that case to Node's lookup.
985 const executable = resolved.shell ? "git" : resolved.command;
980986 const child = yield* commandSpawner
981987 .spawn(
982 ChildProcess.make("git", commandInput.args, {
988 ChildProcess.make(executable, commandInput.args, {
983989 cwd: commandInput.cwd,
984 env: { ...env, ...windowsLongPathConfigEnv(hostPlatform, env) },
990 env: spawnEnv,
985991 }),
986992 )
987993 .pipe(
packages/shared/src/shell.test.ts+89
......@@ -15,6 +15,7 @@ import {
1515 listLoginShellCandidates,
1616 mergePathEntries,
1717 mergePathValues,
18 preferGitForWindowsBinary,
1819 readEnvironmentFromLoginShell,
1920 readEnvironmentFromWindowsShell,
2021 readPathFromLaunchctl,
......@@ -511,6 +512,70 @@ effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => {
511512 );
512513});
513514
515describe("preferGitForWindowsBinary", () => {
516 const files =
517 (...paths: Array<string>) =>
518 (filePath: string) =>
519 paths.includes(filePath);
520
521 it("runs the git.exe Git for Windows' launcher would start", () => {
522 // 2.56+ on x64. The PATHEXT scan returns the extension in PATHEXT's case.
523 expect(
524 preferGitForWindowsBinary(
525 "C:\\Program Files\\Git\\cmd\\git.EXE",
526 {},
527 files(
528 "C:\\Program Files\\Git\\ucrt64\\bin\\git.exe",
529 "C:\\Program Files\\Git\\mingw64\\bin\\git.exe",
530 ),
531 ),
532 ).toBe("C:\\Program Files\\Git\\ucrt64\\bin\\git.exe");
533 // Before 2.56, and the portable build's bin launcher.
534 expect(
535 preferGitForWindowsBinary(
536 "D:\\PortableGit\\bin\\git.exe",
537 {},
538 files("D:\\PortableGit\\mingw64\\bin\\git.exe"),
539 ),
540 ).toBe("D:\\PortableGit\\mingw64\\bin\\git.exe");
541 expect(
542 preferGitForWindowsBinary(
543 "C:\\Program Files\\Git\\cmd\\git.exe",
544 {},
545 files("C:\\Program Files\\Git\\clangarm64\\bin\\git.exe"),
546 ),
547 ).toBe("C:\\Program Files\\Git\\clangarm64\\bin\\git.exe");
548 });
549
550 it("keeps the launcher when MSYSTEM is set", () => {
551 // The real git.exe only adds its own folders to PATH when MSYSTEM is unset;
552 // without them a `#!/bin/sh` hook cannot start.
553 expect(
554 preferGitForWindowsBinary(
555 "C:\\Program Files\\Git\\cmd\\git.exe",
556 { MSYSTEM: "MINGW64" },
557 files("C:\\Program Files\\Git\\mingw64\\bin\\git.exe"),
558 ),
559 ).toBe("C:\\Program Files\\Git\\cmd\\git.exe");
560 });
561
562 it("keeps the launcher when no git.exe sits beside it", () => {
563 expect(preferGitForWindowsBinary("C:\\Program Files\\Git\\cmd\\git.exe", {}, () => false)).toBe(
564 "C:\\Program Files\\Git\\cmd\\git.exe",
565 );
566 });
567
568 it("leaves other gits and other launchers alone", () => {
569 const everything = () => true;
570 expect(preferGitForWindowsBinary("C:\\Users\\me\\scoop\\shims\\git.exe", {}, everything)).toBe(
571 "C:\\Users\\me\\scoop\\shims\\git.exe",
572 );
573 expect(preferGitForWindowsBinary("C:\\Program Files\\Git\\cmd\\gitk.exe", {}, everything)).toBe(
574 "C:\\Program Files\\Git\\cmd\\gitk.exe",
575 );
576 });
577});
578
514579effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => {
515580 it.effect("runs Windows executables directly without a shell", () =>
516581 Effect.gen(function* () {
......@@ -627,6 +692,30 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => {
627692 }).pipe(Effect.provideService(CommandResolutionCache, new Map())),
628693 );
629694
695 it.effect("keeps launcher swaps apart for environments with and without MSYSTEM", () =>
696 Effect.gen(function* () {
697 let scans = 0;
698 const scan: SpawnExecutableResolver = () => {
699 scans++;
700 return "C:\\Git\\cmd\\git.exe";
701 };
702 const resolve = (env: NodeJS.ProcessEnv) =>
703 resolveSpawnCommand("git", [], {
704 env: { PATH: "C:\\Git\\cmd", PATHEXT: ".EXE", ...env },
705 }).pipe(
706 Effect.provideService(HostProcess.Platform, "win32"),
707 Effect.provideService(SpawnExecutableResolution, scan),
708 );
709
710 yield* resolve({});
711 yield* resolve({});
712 expect(scans).toBe(1);
713 // A swap made without MSYSTEM must not reach a child that has it set.
714 yield* resolve({ MSYSTEM: "MINGW64" });
715 expect(scans).toBe(2);
716 }).pipe(Effect.provideService(CommandResolutionCache, new Map())),
717 );
718
630719 it.effect("does not fall back to a shell for unresolved Windows commands", () =>
631720 Effect.gen(function* () {
632721 const command = yield* resolveSpawnCommand("missing & calc", ["unsafe & value"], {
packages/shared/src/shell.ts+47-1
......@@ -693,6 +693,47 @@ export const resolveCommandPath = Effect.fn("shell.resolveCommandPath")(function
693693 });
694694});
695695
696// Git for Windows 2.56 moved x64 builds from mingw64 to ucrt64; ARM64 builds
697// live in clangarm64 and 32-bit ones in mingw32.
698const GIT_FOR_WINDOWS_BUILDS = ["ucrt64", "clangarm64", "mingw64", "mingw32"] as const;
699
700function isFileSync(filePath: string): boolean {
701 try {
702 return NodeFS.statSync(filePath).isFile();
703 } catch {
704 return false;
705 }
706}
707
708/**
709 * Swaps Git for Windows' launcher (`<Git>\cmd\git.exe`, the only git its
710 * installer puts on PATH, or the portable build's `<Git>\bin\git.exe`) for the
711 * git.exe it starts. The launcher costs a second process on every git command,
712 * and each launch leaks a kernel token reference that slows process creation
713 * machine-wide until reboot. The real binary sets HOME itself, but adds its own
714 * folders to PATH for hooks, ssh and credential helpers only when MSYSTEM is
715 * unset, so the launcher stays when MSYSTEM is set. Anything else is returned
716 * as is.
717 */
718export function preferGitForWindowsBinary(
719 executable: string,
720 env: NodeJS.ProcessEnv,
721 isFile: (filePath: string) => boolean = isFileSync,
722): string {
723 if (env.MSYSTEM) return executable;
724 const path = NodePath.win32;
725 if (path.basename(executable).toLowerCase() !== "git.exe") return executable;
726 const launcherDirectory = path.dirname(executable);
727 const launcherFolder = path.basename(launcherDirectory).toLowerCase();
728 if (launcherFolder !== "cmd" && launcherFolder !== "bin") return executable;
729 const installRoot = path.dirname(launcherDirectory);
730 for (const build of GIT_FOR_WINDOWS_BUILDS) {
731 const candidate = path.join(installRoot, build, "bin", "git.exe");
732 if (isFile(candidate)) return candidate;
733 }
734 return executable;
735}
736
696737// Untraced because it runs before most spawns and returns at once off Windows.
697738export const resolveSpawnCommand = Effect.fnUntraced(function* (
698739 command: string,
......@@ -724,6 +765,7 @@ export const resolveSpawnCommand = Effect.fnUntraced(function* (
724765 platform,
725766 resolvePathEnvironmentVariable(env),
726767 resolveWindowsPathExtensions(env).join(";"),
768 env.MSYSTEM ? "msystem" : "",
727769 command,
728770 ].join(COMMAND_RESOLUTION_CACHE_KEY_SEPARATOR);
729771 const nowNanos = yield* Clock.currentTimeNanos;
......@@ -732,7 +774,11 @@ export const resolveSpawnCommand = Effect.fnUntraced(function* (
732774 if (cached !== undefined && cached.expiresAtNanos > nowNanos) {
733775 resolvedExecutable = cached.resolvedPath;
734776 } else {
735 resolvedExecutable = resolveExecutable(command, platform, env) ?? null;
777 // Cached with the scan: its file checks would otherwise run before every
778 // git launch. A Git upgrade that moves the real binary can fail git for up
779 // to the cache lifetime.
780 const found = resolveExecutable(command, platform, env);
781 resolvedExecutable = found === undefined ? null : preferGitForWindowsBinary(found, env);
736782 if (!explicitPath && resolvedExecutable !== null) {
737783 cacheCommandResolution(cache, cacheKey, resolvedExecutable, nowNanos);
738784 }