From fcca3f95e0f4499e8a1d95968ad0f2ad0c6c4f27 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 13 Jun 2026 09:18:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(executor):=20node=5Fmodules=20=E8=BD=AF?= =?UTF-8?q?=E9=93=BE=E5=86=99=E5=85=A5=E4=BB=93=E5=BA=93=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=20exclude,=E6=9D=9C=E7=BB=9D=E8=A2=AB=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E8=BF=9B=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版软链修复有回归:.gitignore 的 `node_modules/`(带斜杠)只匹配目录不匹配软链, 导致 agent 的 `git add -A` 把 `node_modules -> /abs/path` 软链提交进分支,泄露本机绝对路径 (code review + 安全审计已正确 reject)。改为建软链前先把 node_modules 写进共享 /.git/info/exclude(git 的 info/exclude 取 common dir,worktree 也读它;untracked、幂等、 不动 tracked 的 .gitignore)。测试加回归断言:git add -A 后软链不出现在 status。 Co-Authored-By: Claude Opus 4.8 --- src/executor/worktree.ts | 31 +++++++++++++++++++++++++++++-- test/worktree.test.ts | 5 +++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/executor/worktree.ts b/src/executor/worktree.ts index c3b3f30..b401a35 100644 --- a/src/executor/worktree.ts +++ b/src/executor/worktree.ts @@ -1,7 +1,7 @@ import { execFile } from 'node:child_process'; import { homedir } from 'node:os'; -import { basename, dirname, join } from 'node:path'; -import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from 'node:fs'; +import { basename, dirname, isAbsolute, join, resolve } from 'node:path'; +import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; const GIT_TIMEOUT_MS = 60_000; const MAX_BUFFER = 16 * 1024 * 1024; @@ -75,6 +75,10 @@ function linkNodeModules(repoPath: string, dir: string): void { if (!existsSync(src)) return; const dest = join(dir, 'node_modules'); if (existsSync(dest) || isBrokenSymlink(dest)) return; // 项目自己提交了 node_modules / 残留软链,别覆盖 + // 关键:先把 node_modules 写进仓库本地 exclude,确保软链永不被 `git add -A` 收录 + // (.gitignore 的 `node_modules/` 带斜杠只匹配目录、不匹配软链,否则会把本机绝对路径提交进历史)。 + // git 的 info/exclude 取的是共享 common dir(worktree 也读它),故写到 /.git/info/exclude。 + excludeNodeModules(repoPath); try { symlinkSync(src, dest, 'dir'); } catch { @@ -82,6 +86,29 @@ function linkNodeModules(repoPath: string, dir: string): void { } } +/** 把 node_modules 追加进仓库本地 exclude(/info/exclude,untracked、不动 tracked 的 .gitignore,幂等) */ +function excludeNodeModules(repoPath: string): void { + try { + const gitPath = join(repoPath, '.git'); + // 正常仓库 .git 是目录即 common dir;若 repoPath 本身是 worktree(.git 为文件)则解析其 gitdir + let commonDir = gitPath; + if (!lstatSync(gitPath).isDirectory()) { + const m = readFileSync(gitPath, 'utf8').match(/^gitdir:\s*(.+?)\s*$/m); + if (!m) return; + const gd = isAbsolute(m[1]) ? m[1] : resolve(repoPath, m[1]); + commonDir = resolve(gd, '..', '..'); // .git/worktrees/ → .git + } + const infoDir = join(commonDir, 'info'); + mkdirSync(infoDir, { recursive: true }); + const exclPath = join(infoDir, 'exclude'); + const prev = existsSync(exclPath) ? readFileSync(exclPath, 'utf8') : ''; + if (prev.split('\n').some((l) => l.trim() === 'node_modules')) return; // 幂等 + writeFileSync(exclPath, (prev && !prev.endsWith('\n') ? prev + '\n' : prev) + 'node_modules\n'); + } catch { + // 写 exclude 失败不阻断;最坏情况软链可能被 add,但已尽力规避 + } +} + /** 目标是「指向已失效路径的软链」——existsSync 对断链返回 false,需 lstat 兜底判断 */ function isBrokenSymlink(p: string): boolean { try { diff --git a/test/worktree.test.ts b/test/worktree.test.ts index 2de2413..822bf24 100644 --- a/test/worktree.test.ts +++ b/test/worktree.test.ts @@ -100,6 +100,11 @@ test('worktree:主仓有 node_modules 时软链进 worktree(让 .bin/tsc 等 assert.equal(readlinkSync(link), join(repo, 'node_modules'), '软链应指向主仓 node_modules'); assert.ok(existsSync(join(link, '.bin', 'tsc')), '经软链可解析到 .bin/tsc'); + // 关键回归:软链绝不能被 git 收录(否则把本机绝对路径提交进历史) + gitSync(wt.dir, ['add', '-A']); + const staged = gitSync(wt.dir, ['status', '--porcelain']); + assert.ok(!staged.includes('node_modules'), `node_modules 软链不应被 git add 收录:${staged}`); + // remove 不应误删主仓 node_modules(软链被移除,目标保留) await removeWorktree(repo, wt.dir); assert.ok(existsSync(join(repo, 'node_modules', '.bin', 'tsc')), '主仓 node_modules 不受 remove 影响');