diff --git a/src/node/worktree/WorktreeManager.test.ts b/src/node/worktree/WorktreeManager.test.ts index 9cec5dda1f..597e3925c0 100644 --- a/src/node/worktree/WorktreeManager.test.ts +++ b/src/node/worktree/WorktreeManager.test.ts @@ -17,6 +17,15 @@ function initGitRepo(projectPath: string): void { execSync('git commit -m "init"', { cwd: projectPath, stdio: "ignore" }); } +function initGitRepoWithSubmodule(projectPath: string, submoduleSourcePath: string): void { + initGitRepo(projectPath); + execSync(`git -c protocol.file.allow=always submodule add "${submoduleSourcePath}" deps/sub`, { + cwd: projectPath, + stdio: "ignore", + }); + execSync('git commit -m "add submodule"', { cwd: projectPath, stdio: "ignore" }); +} + function createNullInitLogger(): InitLogger { return { logStep: (_message: string) => undefined, @@ -53,6 +62,62 @@ describe("WorktreeManager constructor", () => { }); }); +describe("WorktreeManager.createWorkspace", () => { + it("initializes submodules in the created worktree", async () => { + const rootDir = await fsPromises.realpath( + await fsPromises.mkdtemp(path.join(os.tmpdir(), "worktree-manager-create-")) + ); + + try { + const submoduleSourcePath = path.join(rootDir, "submodule-source"); + await fsPromises.mkdir(submoduleSourcePath, { recursive: true }); + initGitRepo(submoduleSourcePath); + + const projectPath = path.join(rootDir, "repo"); + await fsPromises.mkdir(projectPath, { recursive: true }); + initGitRepoWithSubmodule(projectPath, submoduleSourcePath); + + const srcBaseDir = path.join(rootDir, "src"); + await fsPromises.mkdir(srcBaseDir, { recursive: true }); + + const manager = new WorktreeManager(srcBaseDir); + const initLogger = createNullInitLogger(); + + // Test-only: allow local-path submodule transport for temp fixture repos. + const previousAllowProtocol = process.env.GIT_ALLOW_PROTOCOL; + process.env.GIT_ALLOW_PROTOCOL = "file"; + + try { + const createResult = await manager.createWorkspace({ + projectPath, + branchName: "feature_with_submodule", + trunkBranch: "main", + initLogger, + }); + expect(createResult.success).toBe(true); + if (!createResult.success || !createResult.workspacePath) return; + + const submoduleStatus = execSync("git submodule status", { + cwd: createResult.workspacePath, + stdio: ["ignore", "pipe", "ignore"], + }) + .toString() + .trim(); + + expect(submoduleStatus.startsWith("-")).toBe(false); + } finally { + if (previousAllowProtocol === undefined) { + delete process.env.GIT_ALLOW_PROTOCOL; + } else { + process.env.GIT_ALLOW_PROTOCOL = previousAllowProtocol; + } + } + } finally { + await fsPromises.rm(rootDir, { recursive: true, force: true }); + } + }, 20_000); +}); + describe("WorktreeManager.deleteWorkspace", () => { it("deletes non-agent branches when removing worktrees (force)", async () => { const rootDir = await fsPromises.realpath( diff --git a/src/node/worktree/WorktreeManager.ts b/src/node/worktree/WorktreeManager.ts index bf0f8c8536..9f61582ee0 100644 --- a/src/node/worktree/WorktreeManager.ts +++ b/src/node/worktree/WorktreeManager.ts @@ -121,6 +121,10 @@ export class WorktreeManager { await this.fastForwardToOrigin(workspacePath, trunkBranch, initLogger, noHooksEnv); } + // Initialize/update submodules after any fast-forward because FF can + // update submodule gitlinks without checking out the new submodule commits. + await this.initializeSubmodules(workspacePath, initLogger, noHooksEnv); + return { success: true, workspacePath }; } catch (error) { return { @@ -225,6 +229,32 @@ export class WorktreeManager { } } + /** + * Initialize submodules for newly created workspaces. + * Best-effort: workspace creation should still succeed when submodule auth or + * network setup is unavailable. + */ + private async initializeSubmodules( + workspacePath: string, + initLogger: InitLogger, + noHooksEnv?: { env: Record } + ): Promise { + try { + initLogger.logStep("Initializing git submodules..."); + using submoduleProc = execFileAsync( + "git", + ["-C", workspacePath, "submodule", "update", "--init", "--recursive"], + noHooksEnv + ); + await submoduleProc.result; + initLogger.logStep("Git submodules initialized"); + } catch (error) { + initLogger.logStderr( + `Note: Failed to initialize git submodules (${getErrorMessage(error)}), continuing` + ); + } + } + async renameWorkspace( projectPath: string, oldName: string,