diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index b9731040c7d..225983131c3 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -31,6 +31,8 @@ export namespace FileIgnore { "mypy_cache", ".history", ".gradle", + ".kilocode", // kilocode_change — ignore legacy local storage (#8379) + ".opencode", // kilocode_change — ignore legacy local storage (#8379) ]) const FILES = [ diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 92b6317eeec..7764b874a0c 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -187,8 +187,31 @@ export namespace Snapshot { ref: "FileDiff", }) export type FileDiff = z.infer + + // kilocode_change start — cache diffFull results to prevent redundant git spawning (#8379) + const diffCache = new Map>() + const DIFF_CACHE_MAX = 100 + export async function diffFull(from: string, to: string): Promise { + if (from === to) return [] + const key = `${from}:${to}` + const cached = diffCache.get(key) + if (cached) return cached + if (diffCache.size >= DIFF_CACHE_MAX) { + const first = diffCache.keys().next().value + if (first) diffCache.delete(first) + } + const pending = diffFullUncached(from, to).catch((err) => { + diffCache.delete(key) + throw err + }) + diffCache.set(key, pending) + return pending + } + + async function diffFullUncached(from: string, to: string): Promise { const git = await KiloSnapshot.prepare() // kilocode_change + // kilocode_change end const result: FileDiff[] = [] const status = new Map() diff --git a/packages/opencode/test/kilocode/snapshot-cache.test.ts b/packages/opencode/test/kilocode/snapshot-cache.test.ts new file mode 100644 index 00000000000..5c0c05fb824 --- /dev/null +++ b/packages/opencode/test/kilocode/snapshot-cache.test.ts @@ -0,0 +1,81 @@ +import { test, expect } from "bun:test" +import { $ } from "bun" +import { Snapshot } from "../../src/snapshot" +import { Instance } from "../../src/project/instance" +import { Filesystem } from "../../src/util/filesystem" +import { tmpdir } from "../fixture/fixture" + +async function bootstrap() { + return tmpdir({ + git: true, + init: async (dir) => { + await Filesystem.write(`${dir}/a.txt`, "A") + await Filesystem.write(`${dir}/b.txt`, "B") + await $`git add .`.cwd(dir).quiet() + await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet() + }, + }) +} + +test("diffFull returns cached result for same hash pair", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await Filesystem.write(`${tmp.path}/a.txt`, "MODIFIED") + const after = await Snapshot.track() + expect(after).toBeTruthy() + expect(after).not.toBe(before) + + const first = await Snapshot.diffFull(before!, after!) + const second = await Snapshot.diffFull(before!, after!) + + // Should be the exact same array reference (cached) + expect(second).toBe(first) + expect(first.length).toBeGreaterThan(0) + }, + }) +}) + +test("diffFull returns empty array when from === to", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const hash = await Snapshot.track() + expect(hash).toBeTruthy() + + const result = await Snapshot.diffFull(hash!, hash!) + expect(result).toEqual([]) + }, + }) +}) + +test("diffFull concurrent calls for same pair share one result", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await Filesystem.write(`${tmp.path}/a.txt`, "CONCURRENT") + const after = await Snapshot.track() + expect(after).toBeTruthy() + + // Fire multiple concurrent calls — they should all resolve to the same object + const results = await Promise.all([ + Snapshot.diffFull(before!, after!), + Snapshot.diffFull(before!, after!), + Snapshot.diffFull(before!, after!), + ]) + + expect(results[0]).toBe(results[1]) + expect(results[1]).toBe(results[2]) + expect(results[0].length).toBeGreaterThan(0) + }, + }) +})