From 67814ed9d1586621455c2c64cafec0847b45165a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Nov 2026 17:49:02 +0000 Subject: [PATCH 1/4] test(server): worktree submodule checkout is non-interactive (reproduces #33017) Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 76 ++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 728c20c034ee2..48b53c005b77d 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -4943,6 +4943,82 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }), ); + it.effect.each([false, true] as const)( + "makes worktree submodule checkout non-interactive (progress: %s)", + (progress) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + const tempDir = yield* makeTmpDir("git-vcs-driver-ssh-env-"); + const sshLogPath = pathService.join(tempDir, "ssh-env.txt"); + const sshWrapperPath = yield* pathService.fromFileUrl( + new URL("./testing/GitVcsDriverCore.fixture.sh", import.meta.url), + ); + + const cwd = yield* makeTmpDir(); + const { initialBranch } = yield* initRepoWithCommit(cwd); + yield* writeTextFile( + cwd, + ".gitmodules", + '[submodule "private"]\n\tpath = private\n\turl = ssh://example.invalid/private.git\n', + ); + yield* git(cwd, ["add", "."]); + const gitlink = yield* git(cwd, ["rev-parse", "HEAD"]); + yield* git(cwd, ["update-index", "--add", "--cacheinfo", `160000,${gitlink},private`]); + yield* git(cwd, ["commit", "-m", "add private submodule"]); + + const worktreePath = pathService.join( + yield* makeTmpDir("git-worktrees-"), + "private-submodule-worktree", + ); + const finished = yield* Ref.make>( + [], + ); + + yield* Effect.gen(function* () { + const created = yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree( + { + cwd, + path: worktreePath, + refName: initialBranch, + newRefName: "feature/private-submodules", + }, + { + progress: { + onSubmodulesFinished: (result) => + Ref.update(finished, (items) => [...items, result]), + ...(progress ? { onSubmoduleLine: () => Effect.void } : {}), + }, + }, + ); + + assert.equal(created.worktree.path, worktreePath); + 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, + }), + ); + + const results = yield* Ref.get(finished); + assert.lengthOf(results, 1); + assert.isFalse(results[0]!.ok); + }), + ); + it.effect("creates worktrees under the configured worktrees directory", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; From cde68401119d217276f981920908e7809291b2a8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Nov 2026 17:55:45 +0000 Subject: [PATCH 2/4] fix(server): worktree submodule checkout no longer waits on hidden credential prompts Automatic `git submodule update` during worktree creation ran with the ambient environment, so a private submodule could open a credential prompt nobody can answer and leave worktree preparation hanging (timeoutMs is null on purpose). It now uses the same non-interactive policy as background status fetches and fetchRemote, with and without the progress callback, so an auth failure fails fast and is reported through onSubmodulesFinished. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 85a7e524b1a63..7ee36e6714438 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -4364,10 +4364,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* timeoutMs: null, ...(onSubmoduleLine ? { - env: { LANGUAGE: "C" }, + env: { ...STATUS_UPSTREAM_REFRESH_ENV, LANGUAGE: "C" }, progress: { onStdoutLine: onSubmoduleLine, onStderrLine: onSubmoduleLine }, } - : {}), + : { env: STATUS_UPSTREAM_REFRESH_ENV }), }, ), ), From 27db835d5094e372276cbe75c224761f28bc971b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Nov 2026 18:04:00 +0000 Subject: [PATCH 3/4] docs: worktree submodules clone without sign-in prompts Co-Authored-By: Claude Opus 5.5 --- docs/user/project-settings.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user/project-settings.md b/docs/user/project-settings.md index ef3b5eac32413..c55627ab3483d 100644 --- a/docs/user/project-settings.md +++ b/docs/user/project-settings.md @@ -194,6 +194,10 @@ itself, or **Skip** to leave them for a setup script. It resolves in the same or workspace default: a `"worktreeSubmodules"` value in the `t3.json` of the branch being checked out applies when the project and environment are both on **Inherit**. +Submodules are cloned without sign-in prompts, so a private submodule needs credentials Git +already has on the server, such as a saved credential helper login or an SSH agent key. Without +them, the worktree starts with that submodule empty. + ## Worktree location New worktrees go in the `worktrees` folder of the T3 home directory. To put them somewhere else, From fc9e8e1027a8b71f011c0295c941c1d7cceefefa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 20:39:28 +0000 Subject: [PATCH 4/4] refactor(server): name the shared non-interactive Git auth env for what it is AI-assisted; verified with the tests and tools listed above. --- apps/server/src/vcs/GitVcsDriverCore.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 8c4bfa8f2ee63..d11863a6bfe72 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -111,7 +111,7 @@ const LIST_REFS_REFRESH_COALESCE_TTL = Duration.seconds(5); const LIST_REFS_REFRESH_FAILURE_COOLDOWN = Duration.seconds(30); const STATUS_DEFAULT_BRANCH_CACHE_TTL = Duration.minutes(5); const STATUS_ORIGIN_EXISTS_CACHE_TTL = Duration.minutes(5); -const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({ +const NON_INTERACTIVE_GIT_AUTH_ENV = Object.freeze({ GCM_INTERACTIVE: "never", GIT_ASKPASS: "", GIT_TERMINAL_PROMPT: "0", @@ -1377,7 +1377,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: NON_INTERACTIVE_GIT_AUTH_ENV, fallbackErrorDetail: "Background Git fetch exited with a non-zero status.", timeoutMs: Duration.toMillis(STATUS_UPSTREAM_FETCH_TIMEOUT), forceKillAfterMs: 1_000, @@ -4417,10 +4417,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* timeoutMs: null, ...(onSubmoduleLine ? { - env: { ...STATUS_UPSTREAM_REFRESH_ENV, LANGUAGE: "C" }, + env: { ...NON_INTERACTIVE_GIT_AUTH_ENV, LANGUAGE: "C" }, progress: { onStdoutLine: onSubmoduleLine, onStderrLine: onSubmoduleLine }, } - : { env: STATUS_UPSTREAM_REFRESH_ENV }), + : { env: NON_INTERACTIVE_GIT_AUTH_ENV }), }, ), ), @@ -4582,7 +4582,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: NON_INTERACTIVE_GIT_AUTH_ENV, fallbackErrorDetail: `git fetch ${input.remoteName} failed`, }; // Another fetch in the same repository, such as the background status