From 79eb6c738fd34f750e74d2da738aa5f0b7bf5c5e Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 18 Apr 2026 14:11:24 +0900 Subject: [PATCH] fix(shared/project-discovery-dirs): memoize detectWorktreePath per process --- src/shared/project-discovery-dirs.test.ts | 73 +++++++++-------------- src/shared/project-discovery-dirs.ts | 21 ++++++- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/shared/project-discovery-dirs.test.ts b/src/shared/project-discovery-dirs.test.ts index 2c9f127b5..d2904bc72 100644 --- a/src/shared/project-discovery-dirs.test.ts +++ b/src/shared/project-discovery-dirs.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os" import { join } from "node:path" const TEST_DIR = join(tmpdir(), `project-discovery-dirs-${Date.now()}`) +let worktreeSpawnCount = 0 function canonicalPath(path: string): string { return realpathSync(path) @@ -18,6 +19,34 @@ describe("project-discovery-dirs", () => { rmSync(TEST_DIR, { recursive: true, force: true }) }) + it("#given repeated worktree detection #when detecting twice #then reuses the cached result", async () => { + // given + worktreeSpawnCount = 0 + + mock.module("node:child_process", () => ({ + execFileSync: () => { + worktreeSpawnCount += 1 + return TEST_DIR + }, + })) + + const { clearWorktreeCache, detectWorktreePath } = await import("./project-discovery-dirs") + + clearWorktreeCache() + + // when + const firstPath = detectWorktreePath("/some/dir") + const secondPath = detectWorktreePath("/some/dir") + clearWorktreeCache() + const thirdPath = detectWorktreePath("/some/dir") + + // then + expect(firstPath).toBe(TEST_DIR) + expect(secondPath).toBe(TEST_DIR) + expect(thirdPath).toBe(TEST_DIR) + expect(worktreeSpawnCount).toBe(2) + }) + it("#given nested .opencode skill directories #when finding project opencode skill dirs #then returns nearest-first with aliases", async () => { // given const projectDir = join(TEST_DIR, "project") @@ -92,48 +121,4 @@ describe("project-discovery-dirs", () => { expect(directories).toEqual([canonicalPath(join(projectDir, ".opencode", "skills"))]) }) - it("#given repeated worktree detection #when detecting twice #then reuses the cached result", async () => { - // given - let callCount = 0 - mock.module("node:child_process", () => ({ - execFileSync: () => { - callCount += 1 - return TEST_DIR - }, - })) - - const { clearWorktreeCache, detectWorktreePath } = await import("./project-discovery-dirs") - - clearWorktreeCache() - - // when - const firstPath = detectWorktreePath("/some/dir") - const secondPath = detectWorktreePath("/some/dir") - - // then - expect(firstPath).toBe(TEST_DIR) - expect(secondPath).toBe(TEST_DIR) - expect(callCount).toBe(1) - }) - - it("#given a cleared worktree cache #when detecting again #then spawns git again", async () => { - // given - let callCount = 0 - mock.module("node:child_process", () => ({ - execFileSync: () => { - callCount += 1 - return TEST_DIR - }, - })) - - const { clearWorktreeCache, detectWorktreePath } = await import("./project-discovery-dirs") - - // when - detectWorktreePath("/some/dir") - clearWorktreeCache() - detectWorktreePath("/some/dir") - - // then - expect(execFileSync).toHaveBeenCalledTimes(2) - }) }) diff --git a/src/shared/project-discovery-dirs.ts b/src/shared/project-discovery-dirs.ts index 4e22b66f6..5e243df5a 100644 --- a/src/shared/project-discovery-dirs.ts +++ b/src/shared/project-discovery-dirs.ts @@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process" import { existsSync, realpathSync } from "node:fs" import { dirname, join, resolve } from "node:path" +const worktreePathCache = new Map() + function normalizePath(path: string): string { const resolvedPath = resolve(path) if (!existsSync(resolvedPath)) { @@ -49,15 +51,28 @@ function findAncestorDirectories( } } -function detectWorktreePath(directory: string): string | undefined { +export function clearWorktreeCache(): void { + worktreePathCache.clear() +} + +export function detectWorktreePath(directory: string): string | undefined { + const resolvedDirectory = resolve(directory) + if (worktreePathCache.has(resolvedDirectory)) { + return worktreePathCache.get(resolvedDirectory) + } + try { - return execFileSync("git", ["rev-parse", "--show-toplevel"], { - cwd: directory, + const worktreePath = execFileSync("git", ["rev-parse", "--show-toplevel"], { + cwd: resolvedDirectory, encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"], }).trim() + + worktreePathCache.set(resolvedDirectory, worktreePath) + return worktreePath } catch { + worktreePathCache.set(resolvedDirectory, undefined) return undefined } }