diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 6dab329c04ebc..d5440408ca65e 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -4530,45 +4530,57 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }), ); - it.effect("makes background upstream status fetches non-interactive", () => - Effect.gen(function* () { - const cwd = yield* makeTmpDir(); - const tempDir = yield* makeTmpDir("git-vcs-driver-ssh-env-"); - const { initialBranch } = yield* initRepoWithCommit(cwd); - const fileSystem = yield* FileSystem.FileSystem; - const pathService = yield* Path.Path; - const sshLogPath = pathService.join(tempDir, "ssh-env.txt"); - const sshWrapperPath = yield* pathService.fromFileUrl( - new URL("./testing/GitVcsDriverCore.fixture.sh", import.meta.url), - ); + it.effect.each([false, true])( + "makes background and workspace fetches non-interactive (preserve askpass: %s)", + (preserveAskpass) => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + const tempDir = yield* makeTmpDir("git-vcs-driver-ssh-env-"); + const { initialBranch } = yield* initRepoWithCommit(cwd); + const fileSystem = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + const sshLogPath = pathService.join(tempDir, "ssh-env.txt"); + const sshWrapperPath = yield* pathService.fromFileUrl( + new URL("./testing/GitVcsDriverCore.fixture.sh", import.meta.url), + ); - yield* git(cwd, ["remote", "add", "origin", "ssh://example.invalid/repo.git"]); - yield* git(cwd, ["update-ref", `refs/remotes/origin/${initialBranch}`, "HEAD"]); - yield* git(cwd, ["branch", "--set-upstream-to", `origin/${initialBranch}`]); + yield* git(cwd, ["remote", "add", "origin", "ssh://example.invalid/repo.git"]); + yield* git(cwd, ["update-ref", `refs/remotes/origin/${initialBranch}`, "HEAD"]); + yield* git(cwd, ["branch", "--set-upstream-to", `origin/${initialBranch}`]); - yield* Effect.gen(function* () { - yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd); - - assert.deepEqual((yield* fileSystem.readFileString(sshLogPath)).trim().split(/\r?\n/), [ - "GCM_INTERACTIVE=never", - "GIT_ASKPASS=", - "GIT_TERMINAL_PROMPT=0", - "SSH_ASKPASS=", - "SSH_ASKPASS_REQUIRE=never", - ]); - }).pipe( - Effect.provideService(HostProcess.Environment, { - ...(yield* HostProcess.Environment), - GIT_SSH: sshWrapperPath, - GCM_INTERACTIVE: "always", - GIT_ASKPASS: "git-askpass", - GIT_TERMINAL_PROMPT: "1", - SSH_ASKPASS: "ssh-askpass", - SSH_ASKPASS_REQUIRE: "force", - T3_TEST_SSH_ASKPASS_LOG: sshLogPath, - }), - ); - }), + yield* Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const expected = [ + "GCM_INTERACTIVE=never", + `GIT_ASKPASS=${preserveAskpass ? "git-askpass" : ""}`, + "GIT_TERMINAL_PROMPT=0", + "SSH_ASKPASS=", + "SSH_ASKPASS_REQUIRE=never", + ]; + const readLog = fileSystem + .readFileString(sshLogPath) + .pipe(Effect.map((log) => log.trim().split(/\r?\n/))); + + yield* driver.statusDetails(cwd); + assert.deepEqual(yield* readLog, expected); + yield* fileSystem.remove(sshLogPath); + + yield* Effect.result(driver.fetchRemote({ cwd, remoteName: "origin" })); + assert.deepEqual(yield* readLog, expected); + }).pipe( + Effect.provideService(HostProcess.Environment, { + ...(yield* HostProcess.Environment), + GIT_SSH: sshWrapperPath, + GCM_INTERACTIVE: "always", + GIT_ASKPASS: "git-askpass", + GIT_TERMINAL_PROMPT: "1", + SSH_ASKPASS: "ssh-askpass", + SSH_ASKPASS_REQUIRE: "force", + T3_TEST_SSH_ASKPASS_LOG: sshLogPath, + T3CODE_GIT_PRESERVE_ASKPASS: preserveAskpass ? "1" : "0", + }), + ); + }), ); it.effect("reuses the no-upstream fallback ahead count for default-branch delta", () => diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 2557f287aec45..6d184596dbb0b 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -129,6 +129,11 @@ const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({ SSH_ASKPASS: "", SSH_ASKPASS_REQUIRE: "never", } satisfies NodeJS.ProcessEnv); +const upstreamRefreshEnvironment = Effect.map(HostProcess.Environment, (environment) => + environment.T3CODE_GIT_PRESERVE_ASKPASS === "1" + ? { ...STATUS_UPSTREAM_REFRESH_ENV, GIT_ASKPASS: environment.GIT_ASKPASS ?? "" } + : STATUS_UPSTREAM_REFRESH_ENV, +); const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const; const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100; const NON_REPOSITORY_STATUS_DETAILS = Object.freeze({ @@ -1403,7 +1408,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* // healthy writer. A separate safety limit prevents a hung transport from // occupying its worker slot indefinitely. const started = yield* executeGit("GitVcsDriver.fetchRemoteForStatus", fetchCwd, args, { - env: STATUS_UPSTREAM_REFRESH_ENV, + env: yield* upstreamRefreshEnvironment, fallbackErrorDetail: "Background Git fetch exited with a non-zero status.", timeoutMs: Duration.toMillis(STATUS_UPSTREAM_FETCH_TIMEOUT), }).pipe( @@ -4893,7 +4898,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const fetchRemote = Effect.fn("fetchRemote")(function* (input: GitVcsDriver.GitFetchRemoteInput) { const args = ["fetch", "--quiet", "--", input.remoteName]; const options = { - env: STATUS_UPSTREAM_REFRESH_ENV, + env: yield* upstreamRefreshEnvironment, fallbackErrorDetail: `git fetch ${input.remoteName} failed`, }; // Another fetch in the same repository, such as the background status diff --git a/docs/user/source-control.md b/docs/user/source-control.md index 9e679f5534c30..03987139b8498 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -26,6 +26,12 @@ Install Git and configure authentication on the machine running your T3 Code ser environment, do this on the remote machine. After signing in, open **Settings → Source control** and choose **Rescan**. +Background fetches and workspace preparation never prompt for credentials, so T3 Code clears +`GIT_ASKPASS` for them. If your server supplies credentials through a non-interactive `GIT_ASKPASS` +helper, set `T3CODE_GIT_PRESERVE_ASKPASS=1` in the server's environment (for the background +service, in [`service.env`](./background-service.md#configure-the-service-environment)) to keep +it. Only use this for a helper that never opens a dialog. + ### GitHub T3 Code talks to GitHub's API directly and only needs a token. Any of these works, in this