Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/opencode/src/file/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
23 changes: 23 additions & 0 deletions packages/opencode/src/snapshot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,31 @@ export namespace Snapshot {
ref: "FileDiff",
})
export type FileDiff = z.infer<typeof FileDiff>

// kilocode_change start — cache diffFull results to prevent redundant git spawning (#8379)
const diffCache = new Map<string, Promise<FileDiff[]>>()
const DIFF_CACHE_MAX = 100

export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
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<FileDiff[]> {
const git = await KiloSnapshot.prepare() // kilocode_change
// kilocode_change end
const result: FileDiff[] = []
const status = new Map<string, "added" | "deleted" | "modified">()

Expand Down
81 changes: 81 additions & 0 deletions packages/opencode/test/kilocode/snapshot-cache.test.ts
Original file line number Diff line number Diff line change
@@ -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)
},
})
})
Loading