Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
56 changes: 56 additions & 0 deletions src/node/worktree/WorktreeManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ 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",
});
// Local-path submodules are used only in tests; allow file transport so
// `git submodule update --init --recursive` can run inside worktrees.
execSync("git config protocol.file.allow always", { 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 +65,50 @@ 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();

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 {
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
39 changes: 39 additions & 0 deletions src/node/worktree/WorktreeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export class WorktreeManager {
initLogger.logStep("Syncing .muxignore files...");
await syncMuxignoreFiles(projectPath, workspacePath);

// Initialize/update submodules in the workspace so worktree mode matches
// the initial project clone behavior users expect.
await this.initializeSubmodules(workspacePath, initLogger, noHooksEnv);
Comment thread
taskylizard marked this conversation as resolved.
Outdated

// For existing branches, fast-forward to latest origin (best-effort)
// Only if local can fast-forward (preserves unpushed work)
if (shouldUseOrigin && branchExists) {
Expand Down Expand Up @@ -225,6 +229,41 @@ 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",
"protocol.file.allow=always",
Comment thread
taskylizard marked this conversation as resolved.
Outdated
"-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