Skip to content

Commit cbafcf8

Browse files
committed
fix(workspace): ignore linked node_modules in worktrees
1 parent dc54c67 commit cbafcf8

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

packages/local-runner/src/index.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,27 @@ test("workspace manager links node_modules into prepared workspaces when availab
250250
await manager.cleanup(workspacePath);
251251
});
252252

253+
test("workspace manager ignores linked node_modules in git workspaces", async () => {
254+
const repo = await createRealGitRepo();
255+
await mkdir(join(repo, "node_modules"), { recursive: true });
256+
await writeFile(join(repo, "node_modules", ".placeholder"), "ok\n");
257+
const manager = new FileSystemWorkspaceManager();
258+
259+
const { workspacePath } = await manager.prepare({
260+
sourceRepoPath: repo,
261+
workBranch: "devagent/workflow/node-modules-ignore",
262+
isolation: "git-worktree",
263+
baseRef: "main",
264+
});
265+
266+
const status = execFileSync("git", ["status", "--short"], {
267+
cwd: workspacePath,
268+
encoding: "utf-8",
269+
}).trim();
270+
assert.equal(status, "");
271+
await manager.cleanup(workspacePath);
272+
});
273+
253274
test("workspace manager reopens an existing git branch without resetting it", async () => {
254275
const repo = await createRealGitRepo();
255276
const manager = new FileSystemWorkspaceManager();

packages/local-runner/src/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EventEmitter } from "node:events";
22
import { mkdir, readFile, rm, writeFile, cp, stat, readdir, symlink, lstat } from "node:fs/promises";
33
import { appendFileSync, existsSync } from "node:fs";
4-
import { dirname, join, resolve } from "node:path";
4+
import { dirname, isAbsolute, join, resolve } from "node:path";
55
import { execFile } from "node:child_process";
66
import type { ChildProcess } from "node:child_process";
77
import {
@@ -77,6 +77,23 @@ async function execFileStdout(command: string, args: string[], cwd: string): Pro
7777
});
7878
}
7979

80+
async function ignoreWorkspaceEntry(workspacePath: string, entry: string): Promise<void> {
81+
try {
82+
const rawExcludePath = (await execFileStdout("git", ["rev-parse", "--git-path", "info/exclude"], workspacePath)).trim();
83+
const excludePath = isAbsolute(rawExcludePath) ? rawExcludePath : join(workspacePath, rawExcludePath);
84+
await mkdir(dirname(excludePath), { recursive: true });
85+
const current = existsSync(excludePath) ? await readFile(excludePath, "utf-8") : "";
86+
const lines = current.split("\n").map((line) => line.trim()).filter(Boolean);
87+
if (lines.includes(entry) || lines.includes(`/${entry}`)) {
88+
return;
89+
}
90+
const next = current.endsWith("\n") || current.length === 0 ? `${current}/${entry}\n` : `${current}\n/${entry}\n`;
91+
await writeFile(excludePath, next);
92+
} catch {
93+
// Temp copies or non-git workspaces do not need git excludes.
94+
}
95+
}
96+
8097
async function copyRepoContents(sourceRepoPath: string, workspacePath: string): Promise<void> {
8198
await mkdir(workspacePath, { recursive: true });
8299
for (const entry of await readdir(sourceRepoPath)) {
@@ -126,6 +143,7 @@ async function linkSharedDependencies(sourceRepoPath: string, workspacePath: str
126143

127144
const relativeTarget = resolve(sourceNodeModules);
128145
await symlink(relativeTarget, workspaceNodeModules, "dir");
146+
await ignoreWorkspaceEntry(workspacePath, "node_modules");
129147
}
130148

131149
function safeWorkspaceName(workBranch: string): string {

0 commit comments

Comments
 (0)