Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/node/worktree/WorktreeManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
30 changes: 30 additions & 0 deletions src/node/worktree/WorktreeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, string> }
): Promise<void> {
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,
Expand Down