From 014d8476cbb51680d8e90fa5cf70ff9fb4f2e560 Mon Sep 17 00:00:00 2001 From: Clowdydev <48922292+Clowdyffs@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:13:33 -0400 Subject: [PATCH 1/3] fix(server): new worktrees borrow submodule objects from the source checkout Submodule repositories are per worktree, so `git submodule update` cloned every submodule from its remote again even though the source checkout already had it in `.git/modules`. Use Git's `submodule.alternateLocation=superproject` to borrow from the source's `.git/modules/`, exposing the shared object store as an alternate for that one command, and `--dissociate` so the worktree keeps its own copy. If the borrowing command fails, retry the plain update. --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 10 ++++ apps/server/src/vcs/GitVcsDriverCore.ts | 61 +++++++++++++++----- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 84861d6dd4ae0..a78b9829af6e5 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -3180,6 +3180,13 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { const { initialBranch } = yield* initRepoWithCommit(cwd); yield* git(cwd, ["submodule", "add", submoduleRepo, "shared"]); yield* git(cwd, ["commit", "-m", "add submodule"]); + // The remote still advertises its refs but can no longer serve objects, so the + // worktree can only populate the submodule from the source's `.git/modules`. + const remoteObjects = pathService.join(submoduleRepo, ".git", "objects"); + yield* fileSystem.remove(remoteObjects, { recursive: true }); + yield* fileSystem.makeDirectory(pathService.join(remoteObjects, "pack"), { + recursive: true, + }); const worktreePath = pathService.join( yield* makeTmpDir("git-worktrees-"), @@ -3197,6 +3204,9 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { yield* fileSystem.exists(pathService.join(worktreePath, "shared", "SHARED.md")), true, ); + // The borrowed objects were copied, so the worktree survives the source losing them. + yield* fileSystem.remove(pathService.join(cwd, ".git", "modules"), { recursive: true }); + yield* git(pathService.join(worktreePath, "shared"), ["fsck", "--no-progress"]); }), ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 62798374d2aa5..f2938f5fc6faf 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -3804,19 +3804,54 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* yield* progress.onSubmodulesStarted(); } const onSubmoduleLine = progress?.onSubmoduleLine; - yield* runGit( - "GitVcsDriver.createWorktree.updateSubmodules", - worktreePath, - submoduleMode.value === "recursive" - ? ["submodule", "update", "--init", "--recursive"] - : ["submodule", "update", "--init"], - onSubmoduleLine - ? { - env: { LC_ALL: "C" }, - progress: { onStdoutLine: onSubmoduleLine, onStderrLine: onSubmoduleLine }, - } - : {}, - ).pipe( + const updateArgs = ["submodule", "update", "--init"]; + if (submoduleMode.value === "recursive") updateArgs.push("--recursive"); + const updateSubmodules = (args: ReadonlyArray, env?: NodeJS.ProcessEnv) => + runGit( + "GitVcsDriver.createWorktree.updateSubmodules", + worktreePath, + args, + onSubmoduleLine + ? { + env: { ...env, LC_ALL: "C" }, + progress: { onStdoutLine: onSubmoduleLine, onStderrLine: onSubmoduleLine }, + } + : { env }, + ); + yield* Effect.gen(function* () { + // Submodule repositories are per worktree, so Git would clone each one again even + // though the source already has it in `.git/modules`. `alternateLocation=superproject` + // borrows from `/modules/`, but a linked worktree shares the source's + // object store instead of listing it as an alternate. Present it as one for this + // command, which needs a separate (empty) primary store. Git skips sources it cannot + // borrow from, and `--dissociate` copies what it borrowed so the worktree stays + // independent of the source. + const sharedObjects = yield* runGitStdout( + "GitVcsDriver.createWorktree.objectsPath", + worktreePath, + ["rev-parse", "--git-path", "objects"], + ); + const emptyObjects = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3code-submodule-objects-", + }); + yield* updateSubmodules( + [ + "-c", + "submodule.alternateLocation=superproject", + "-c", + "submodule.alternateErrorStrategy=ignore", + ...updateArgs, + "--dissociate", + ], + { + GIT_OBJECT_DIRECTORY: emptyObjects, + GIT_ALTERNATE_OBJECT_DIRECTORIES: path.resolve(worktreePath, sharedObjects.trim()), + }, + ); + }).pipe( + Effect.scoped, + // Borrowing only saves time, so whatever it trips on must not leave submodules empty. + Effect.catch(() => updateSubmodules(updateArgs)), Effect.matchEffect({ onFailure: (cause) => Effect.logWarning("worktree submodule checkout failed; submodule paths are empty", { From 571801cb6a8efa5d30e9f98ecf5c66b85c562b8c Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 01:23:25 +0000 Subject: [PATCH 2/3] test(server): cover submodule borrowing fallback --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 122 +++++++++++-------- 1 file changed, 71 insertions(+), 51 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 0e4b7b6856da2..b25b27116faef 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -4037,62 +4037,82 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { }), ); - it.effect("checks out submodules in a new worktree", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const pathService = yield* Path.Path; + it.effect.each([false, true])( + "checks out submodules in a new worktree with borrowing failure %s", + (failBorrowing) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; - // Git blocks file submodule transports (CVE-2022-39253) despite repo config; local fixtures need env permission. - const previousAllowedProtocol = process.env.GIT_ALLOW_PROTOCOL; - process.env.GIT_ALLOW_PROTOCOL = "file"; - yield* Effect.addFinalizer(() => - Effect.sync(() => { - if (previousAllowedProtocol === undefined) { - delete process.env.GIT_ALLOW_PROTOCOL; - } else { - process.env.GIT_ALLOW_PROTOCOL = previousAllowedProtocol; - } - }), - ); + // Git blocks file submodule transports (CVE-2022-39253) despite repo config; local fixtures need env permission. + const previousAllowedProtocol = process.env.GIT_ALLOW_PROTOCOL; + process.env.GIT_ALLOW_PROTOCOL = "file"; + yield* Effect.addFinalizer(() => + Effect.sync(() => { + if (previousAllowedProtocol === undefined) { + delete process.env.GIT_ALLOW_PROTOCOL; + } else { + process.env.GIT_ALLOW_PROTOCOL = previousAllowedProtocol; + } + }), + ); - const submoduleRepo = yield* makeTmpDir("git-submodule-"); - yield* initRepoWithCommit(submoduleRepo); - yield* writeTextFile(submoduleRepo, "SHARED.md", "# shared\n"); - yield* git(submoduleRepo, ["add", "."]); - yield* git(submoduleRepo, ["commit", "-m", "shared"]); + const submoduleRepo = yield* makeTmpDir("git-submodule-"); + yield* initRepoWithCommit(submoduleRepo); + yield* writeTextFile(submoduleRepo, "SHARED.md", "# shared\n"); + yield* git(submoduleRepo, ["add", "."]); + yield* git(submoduleRepo, ["commit", "-m", "shared"]); - const cwd = yield* makeTmpDir(); - const { initialBranch } = yield* initRepoWithCommit(cwd); - yield* git(cwd, ["submodule", "add", submoduleRepo, "shared"]); - yield* git(cwd, ["commit", "-m", "add submodule"]); - // The remote still advertises its refs but can no longer serve objects, so the - // worktree can only populate the submodule from the source's `.git/modules`. - const remoteObjects = pathService.join(submoduleRepo, ".git", "objects"); - yield* fileSystem.remove(remoteObjects, { recursive: true }); - yield* fileSystem.makeDirectory(pathService.join(remoteObjects, "pack"), { - recursive: true, - }); + const cwd = yield* makeTmpDir(); + const { initialBranch } = yield* initRepoWithCommit(cwd); + yield* git(cwd, ["submodule", "add", submoduleRepo, "shared"]); + yield* git(cwd, ["commit", "-m", "add submodule"]); + // The remote still advertises its refs but can no longer serve objects, so the + // worktree can only populate the submodule from the source's `.git/modules`. + const remoteObjects = pathService.join(submoduleRepo, ".git", "objects"); + if (!failBorrowing) { + yield* fileSystem.remove(remoteObjects, { recursive: true }); + yield* fileSystem.makeDirectory(pathService.join(remoteObjects, "pack"), { + recursive: true, + }); + } - const worktreePath = pathService.join( - yield* makeTmpDir("git-worktrees-"), - "submodule-worktree", - ); - const driver = yield* GitVcsDriver.GitVcsDriver; - yield* driver.createWorktree({ - cwd, - path: worktreePath, - refName: initialBranch, - newRefName: "feature/submodules", - }); + const worktreePath = pathService.join( + yield* makeTmpDir("git-worktrees-"), + "submodule-worktree", + ); + const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; + let borrowingFailed = false; + const spawner = ChildProcessSpawner.make((command) => { + if ( + failBorrowing && + ChildProcess.isStandardCommand(command) && + command.args.includes("--dissociate") + ) { + borrowingFailed = true; + return Effect.succeed(makeFailedHandle("error: unknown option 'dissociate'")); + } + return delegate.spawn(command); + }); + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + ); + yield* driver.createWorktree({ + cwd, + path: worktreePath, + refName: initialBranch, + newRefName: "feature/submodules", + }); - assert.equal( - yield* fileSystem.exists(pathService.join(worktreePath, "shared", "SHARED.md")), - true, - ); - // The borrowed objects were copied, so the worktree survives the source losing them. - yield* fileSystem.remove(pathService.join(cwd, ".git", "modules"), { recursive: true }); - yield* git(pathService.join(worktreePath, "shared"), ["fsck", "--no-progress"]); - }), + assert.equal( + yield* fileSystem.exists(pathService.join(worktreePath, "shared", "SHARED.md")), + true, + ); + assert.equal(borrowingFailed, failBorrowing); + // The borrowed objects were copied, so the worktree survives the source losing them. + yield* fileSystem.remove(pathService.join(cwd, ".git", "modules"), { recursive: true }); + yield* git(pathService.join(worktreePath, "shared"), ["fsck", "--no-progress"]); + }), ); it.effect("still creates the worktree when submodule checkout fails", () => From faeb155409e2a7155dc56a3e0b118a2b44477493 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 2 Nov 2026 01:35:31 +0000 Subject: [PATCH 3/3] test(server): recognize borrowing in submodule cancellation --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index c91c26daf7c6e..365ada38cb5e8 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -1392,8 +1392,8 @@ it.effect("cancels a slow submodule checkout without reporting best-effort succe const spawner = ChildProcessSpawner.make((command) => { if ( ChildProcess.isStandardCommand(command) && - command.args[0] === "submodule" && - command.args[1] === "update" + command.args.includes("submodule") && + command.args.includes("update") ) { return Effect.acquireRelease( Effect.succeed(