Skip to content
86 changes: 49 additions & 37 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () =>
Expand Down
9 changes: 7 additions & 2 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GitVcsDriver.GitStatusDetails>({
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/user/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading