diff options
| author | Dax Raad <[email protected]> | 2025-10-01 05:05:50 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-10-01 05:06:37 -0400 |
| commit | 6a7eeb39c362cff4707079f15bbbe3ec5a556a37 (patch) | |
| tree | 17541dcb092fe480d957ed441aab2839bc84073a | |
| parent | 35a608cd53fa253e5962921fd5fe1a3c70e23274 (diff) | |
| download | opencode-6a7eeb39c362cff4707079f15bbbe3ec5a556a37.tar.gz opencode-6a7eeb39c362cff4707079f15bbbe3ec5a556a37.zip | |
core: prevent file deletion when reverting changes to existing files
| -rw-r--r-- | packages/opencode/src/snapshot/index.ts | 13 | ||||
| -rw-r--r-- | packages/opencode/test/snapshot/snapshot.test.ts | 52 |
2 files changed, 63 insertions, 2 deletions
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index f301c81f3..fb49ae739 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -105,8 +105,17 @@ export namespace Snapshot { .cwd(Instance.worktree) .nothrow() if (result.exitCode !== 0) { - log.info("file not found in history, deleting", { file }) - await fs.unlink(file).catch(() => {}) + const relativePath = path.relative(Instance.worktree, file) + const checkTree = await $`git --git-dir=${git} ls-tree ${item.hash} -- ${relativePath}` + .quiet() + .cwd(Instance.worktree) + .nothrow() + if (checkTree.exitCode === 0 && checkTree.text().trim()) { + log.info("file existed in snapshot but checkout failed, keeping", { file }) + } else { + log.info("file did not exist in snapshot, deleting", { file }) + await fs.unlink(file).catch(() => {}) + } } files.add(file) } diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index bafe6d6e3..1398162e3 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -532,3 +532,55 @@ test("restore function", async () => { }, }) }) + +test("revert should not delete files that existed but were deleted in snapshot", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const snapshot1 = await Snapshot.track() + expect(snapshot1).toBeTruthy() + + await $`rm ${tmp.path}/a.txt`.quiet() + + const snapshot2 = await Snapshot.track() + expect(snapshot2).toBeTruthy() + + await Bun.write(`${tmp.path}/a.txt`, "recreated content") + + const patch = await Snapshot.patch(snapshot2!) + expect(patch.files).toContain(`${tmp.path}/a.txt`) + + await Snapshot.revert([patch]) + + expect(await Bun.file(`${tmp.path}/a.txt`).exists()).toBe(false) + }, + }) +}) + +test("revert preserves file that existed in snapshot when deleted then recreated", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await Bun.write(`${tmp.path}/existing.txt`, "original content") + + const snapshot = await Snapshot.track() + expect(snapshot).toBeTruthy() + + await $`rm ${tmp.path}/existing.txt`.quiet() + await Bun.write(`${tmp.path}/existing.txt`, "recreated") + await Bun.write(`${tmp.path}/newfile.txt`, "new") + + const patch = await Snapshot.patch(snapshot!) + expect(patch.files).toContain(`${tmp.path}/existing.txt`) + expect(patch.files).toContain(`${tmp.path}/newfile.txt`) + + await Snapshot.revert([patch]) + + expect(await Bun.file(`${tmp.path}/newfile.txt`).exists()).toBe(false) + expect(await Bun.file(`${tmp.path}/existing.txt`).exists()).toBe(true) + expect(await Bun.file(`${tmp.path}/existing.txt`).text()).toBe("original content") + }, + }) +}) |
