refactor(core): resolve real git binary once on macOS (#58602)

This commit is contained in:
Kit Langton
2026-10-23 21:11:12 -07:00
committed by GitHub
Unverified
parent 6436011a52
commit a20ae6da09
4 changed files with 124 additions and 13 deletions
+4 -4
View File
@@ -339,7 +339,7 @@ const layer = Layer.effect(
) {
const result = yield* proc
.run(
ChildProcess.make(gitExecutable, repositoryArgs(repository, args), {
ChildProcess.make(gitExecutable(), repositoryArgs(repository, args), {
cwd: repository.worktree,
env: options?.index ? { ...options.env, GIT_INDEX_FILE: options.index } : options?.env,
extendEnv: true,
@@ -549,7 +549,7 @@ const layer = Layer.effect(
const result = yield* proc
.run(
ChildProcess.make(
gitExecutable,
gitExecutable(),
repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]),
{
cwd: input.repository.worktree,
@@ -1227,7 +1227,7 @@ const layer = Layer.effect(
cwd = repository.worktree,
) {
const result = yield* proc
.run(ChildProcess.make(gitExecutable, args, { cwd, extendEnv: true, stdin: "ignore" }))
.run(ChildProcess.make(gitExecutable(), args, { cwd, extendEnv: true, stdin: "ignore" }))
.pipe(
Effect.mapError(
(cause) => new WorktreeError({ operation, directory: worktreeDirectory, message: cause.message, cause }),
@@ -1324,7 +1324,7 @@ function run(cwd: string, proc: AppProcess.Interface, args: string[]) {
function execute(cwd: string, proc: AppProcess.Interface, args: string[]) {
return proc
.run(
ChildProcess.make(gitExecutable, args, {
ChildProcess.make(gitExecutable(), args, {
cwd,
extendEnv: true,
stdin: "ignore",
+2 -2
View File
@@ -44,7 +44,7 @@ export const Plugin = define({
init: (input) =>
Effect.gen(function* () {
const result = yield* processes.run(
ChildProcess.make(gitExecutable, ["init"], { cwd: input.worktree, stdin: "ignore" }),
ChildProcess.make(gitExecutable(), ["init"], { cwd: input.worktree, stdin: "ignore" }),
)
if (result.exitCode !== 0) return yield* Effect.fail(new Error("Git initialization failed"))
}),
@@ -209,7 +209,7 @@ function makeGit(proc: AppProcess.Interface, env?: Record<string, string>) {
const run = Effect.fnUntraced(
function* (args: string[], opts: { cwd: string; maxOutputBytes?: number }) {
const result = yield* proc.run(
ChildProcess.make(gitExecutable, [...cfg, ...args], {
ChildProcess.make(gitExecutable(), [...cfg, ...args], {
cwd: opts.cwd,
env,
extendEnv: true,
+53 -5
View File
@@ -1,11 +1,59 @@
import { existsSync, realpathSync } from "node:fs"
import path from "path"
import { lazy } from "./lazy.js"
import { which } from "./which.js"
export function resolveGitExecutable(platform: NodeJS.Platform, resolved: string | null) {
if (platform !== "win32" || !resolved) return "git"
return path.resolve(resolved)
const darwinShim = "/usr/bin/git"
const xcodeSelectLink = "/var/db/xcode_select_link"
const defaultDeveloperDirs = [
"/Applications/Xcode.app/Contents/Developer",
"/Library/Developer/CommandLineTools",
]
function readXcodeSelectLink(link: string) {
if (!existsSync(link)) return null
try {
return realpathSync(link)
} catch {
return null
}
}
const resolved = process.platform === "win32" ? which("git") : null
export function darwinDeveloperDirs(
developerDir = process.env.DEVELOPER_DIR?.trim(),
readLink: (link: string) => string | null = readXcodeSelectLink,
) {
if (developerDir) return [developerDir]
const selected = readLink(xcodeSelectLink)
return selected ? [selected, ...defaultDeveloperDirs] : defaultDeveloperDirs
}
export const gitExecutable = resolveGitExecutable(process.platform, resolved)
export function resolveGitExecutable(
platform: NodeJS.Platform,
resolved: string | null,
options?: {
developerDirs?: readonly string[]
exists?: (file: string) => boolean
},
) {
if (platform === "win32") return resolved ? path.resolve(resolved) : "git"
if (platform !== "darwin" || !resolved) return "git"
const full = path.posix.resolve(resolved)
if (full !== darwinShim) return full
const exists = options?.exists ?? existsSync
const dirs = options?.developerDirs ?? darwinDeveloperDirs()
return (
dirs
.filter((dir) => Boolean(dir) && path.posix.isAbsolute(dir))
.map((dir) => path.posix.join(dir, "usr/bin/git"))
.find((candidate) => candidate !== darwinShim && exists(candidate)) ?? "git"
)
}
export const gitExecutable = lazy(() =>
resolveGitExecutable(
process.platform,
process.platform === "win32" || process.platform === "darwin" ? which("git") : null,
),
)
+65 -2
View File
@@ -1,13 +1,76 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { resolveGitExecutable } from "@opencode/core/util/git-executable"
import { darwinDeveloperDirs, resolveGitExecutable } from "@opencode/core/util/git-executable"
describe("Git executable", () => {
test("uses an absolute executable on Windows", () => {
expect(resolveGitExecutable("win32", "bin/git.exe")).toBe(path.resolve("bin/git.exe"))
})
test("leaves lookup to the process outside Windows", () => {
test("uses a non-shim PATH executable on macOS without probing developer directories", () => {
let calls = 0
const resolved = resolveGitExecutable("darwin", "/opt/homebrew/bin/git", {
developerDirs: ["/Library/Developer/CommandLineTools"],
exists: () => {
calls += 1
return true
},
})
expect(resolved).toBe("/opt/homebrew/bin/git")
expect(calls).toBe(0)
})
test("resolves the Apple shim on macOS from developer directories", () => {
const resolved = resolveGitExecutable("darwin", "/usr/bin/git", {
developerDirs: ["/Applications/Xcode.app/Contents/Developer", "/Library/Developer/CommandLineTools"],
exists: (file) => file === "/Library/Developer/CommandLineTools/usr/bin/git",
})
expect(resolved).toBe("/Library/Developer/CommandLineTools/usr/bin/git")
})
test("falls back to PATH lookup on macOS when no developer tools exist or candidate is invalid", () => {
expect(
resolveGitExecutable("darwin", "/usr/bin/git", {
developerDirs: ["/Applications/Xcode.app/Contents/Developer", "/Library/Developer/CommandLineTools"],
exists: () => false,
}),
).toBe("git")
expect(
resolveGitExecutable("darwin", "/usr/bin/git", {
developerDirs: ["relative/dir"],
exists: () => true,
}),
).toBe("git")
expect(
resolveGitExecutable("darwin", "/usr/bin/git", {
developerDirs: ["/"],
exists: () => true,
}),
).toBe("git")
expect(
resolveGitExecutable("darwin", null, {
developerDirs: ["/Library/Developer/CommandLineTools"],
exists: () => true,
}),
).toBe("git")
})
test("orders macOS developer directory candidates by DEVELOPER_DIR then xcode-select link", () => {
expect(darwinDeveloperDirs("/Custom/Developer", () => "/Library/Developer/CommandLineTools")).toEqual([
"/Custom/Developer",
])
expect(darwinDeveloperDirs("", () => "/Applications/Xcode-beta.app/Contents/Developer")).toEqual([
"/Applications/Xcode-beta.app/Contents/Developer",
"/Applications/Xcode.app/Contents/Developer",
"/Library/Developer/CommandLineTools",
])
expect(darwinDeveloperDirs(undefined, () => null)).toEqual([
"/Applications/Xcode.app/Contents/Developer",
"/Library/Developer/CommandLineTools",
])
})
test("leaves lookup to the process on Linux", () => {
expect(resolveGitExecutable("linux", "/usr/bin/git")).toBe("git")
})