fix(shared/project-discovery-dirs): memoize detectWorktreePath per process
This commit is contained in:
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os"
|
|||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
|
||||||
const TEST_DIR = join(tmpdir(), `project-discovery-dirs-${Date.now()}`)
|
const TEST_DIR = join(tmpdir(), `project-discovery-dirs-${Date.now()}`)
|
||||||
|
let worktreeSpawnCount = 0
|
||||||
|
|
||||||
function canonicalPath(path: string): string {
|
function canonicalPath(path: string): string {
|
||||||
return realpathSync(path)
|
return realpathSync(path)
|
||||||
@@ -18,6 +19,34 @@ describe("project-discovery-dirs", () => {
|
|||||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
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 () => {
|
it("#given nested .opencode skill directories #when finding project opencode skill dirs #then returns nearest-first with aliases", async () => {
|
||||||
// given
|
// given
|
||||||
const projectDir = join(TEST_DIR, "project")
|
const projectDir = join(TEST_DIR, "project")
|
||||||
@@ -92,48 +121,4 @@ describe("project-discovery-dirs", () => {
|
|||||||
expect(directories).toEqual([canonicalPath(join(projectDir, ".opencode", "skills"))])
|
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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process"
|
|||||||
import { existsSync, realpathSync } from "node:fs"
|
import { existsSync, realpathSync } from "node:fs"
|
||||||
import { dirname, join, resolve } from "node:path"
|
import { dirname, join, resolve } from "node:path"
|
||||||
|
|
||||||
|
const worktreePathCache = new Map<string, string | undefined>()
|
||||||
|
|
||||||
function normalizePath(path: string): string {
|
function normalizePath(path: string): string {
|
||||||
const resolvedPath = resolve(path)
|
const resolvedPath = resolve(path)
|
||||||
if (!existsSync(resolvedPath)) {
|
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 {
|
try {
|
||||||
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
const worktreePath = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||||
cwd: directory,
|
cwd: resolvedDirectory,
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
}).trim()
|
}).trim()
|
||||||
|
|
||||||
|
worktreePathCache.set(resolvedDirectory, worktreePath)
|
||||||
|
return worktreePath
|
||||||
} catch {
|
} catch {
|
||||||
|
worktreePathCache.set(resolvedDirectory, undefined)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user