diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 7068545d265c..ec07173c8328 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -34,6 +34,7 @@ export namespace Snapshot { const log = Log.create({ service: "snapshot" }) const prune = "7.days" + const limit = 2 * 1024 * 1024 const core = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"] const cfg = ["-c", "core.autocrlf=false", ...core] const quote = [...cfg, "-c", "core.quotepath=false"] @@ -123,20 +124,69 @@ export namespace Snapshot { return file }) - const sync = Effect.fnUntraced(function* () { + const sync = Effect.fnUntraced(function* (list: string[] = []) { const file = yield* excludes() const target = path.join(state.gitdir, "info", "exclude") + const text = [ + file ? (yield* read(file)).trimEnd() : "", + ...list.map((item) => `/${item.replaceAll("\\", "/")}`), + ] + .filter(Boolean) + .join("\n") yield* fs.ensureDir(path.join(state.gitdir, "info")).pipe(Effect.orDie) - if (!file) { - yield* fs.writeFileString(target, "").pipe(Effect.orDie) - return - } - yield* fs.writeFileString(target, yield* read(file)).pipe(Effect.orDie) + yield* fs.writeFileString(target, text ? `${text}\n` : "").pipe(Effect.orDie) }) const add = Effect.fnUntraced(function* () { yield* sync() - yield* git([...cfg, ...args(["add", "."])], { cwd: state.directory }) + const [diff, other] = yield* Effect.all( + [ + git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], { + cwd: state.directory, + }), + git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], { + cwd: state.directory, + }), + ], + { concurrency: 2 }, + ) + if (diff.code !== 0 || other.code !== 0) { + log.warn("failed to list snapshot files", { + diffCode: diff.code, + diffStderr: diff.stderr, + otherCode: other.code, + otherStderr: other.stderr, + }) + return + } + + const tracked = diff.text.split("\0").filter(Boolean) + const all = Array.from(new Set([...tracked, ...other.text.split("\0").filter(Boolean)])) + if (!all.length) return + + const large = (yield* Effect.all( + all.map((item) => + fs + .stat(path.join(state.directory, item)) + .pipe(Effect.catch(() => Effect.void)) + .pipe( + Effect.map((stat) => { + if (!stat || stat.type !== "File") return + const size = typeof stat.size === "bigint" ? Number(stat.size) : stat.size + return size > limit ? item : undefined + }), + ), + ), + { concurrency: 8 }, + )).filter((item): item is string => Boolean(item)) + yield* sync(large) + const result = yield* git([...cfg, ...args(["add", "--sparse", "."])], { cwd: state.directory }) + if (result.code !== 0) { + log.warn("failed to add snapshot files", { + exitCode: result.code, + stderr: result.stderr, + }) + } }) const cleanup = Effect.fnUntraced(function* () { @@ -177,7 +227,7 @@ export namespace Snapshot { const patch = Effect.fnUntraced(function* (hash: string) { yield* add() const result = yield* git( - [...quote, ...args(["diff", "--no-ext-diff", "--name-only", hash, "--", "."])], + [...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])], { cwd: state.directory, }, @@ -245,7 +295,7 @@ export namespace Snapshot { const diff = Effect.fnUntraced(function* (hash: string) { yield* add() - const result = yield* git([...quote, ...args(["diff", "--no-ext-diff", hash, "--", "."])], { + const result = yield* git([...quote, ...args(["diff", "--cached", "--no-ext-diff", hash, "--", "."])], { cwd: state.worktree, }) if (result.code !== 0) { diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index bf54feb4723d..f42cec4fc73f 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -181,7 +181,7 @@ test("symlink handling", async () => { }) }) -test("large file handling", async () => { +test("file under size limit handling", async () => { await using tmp = await bootstrap() await Instance.provide({ directory: tmp.path, @@ -196,6 +196,23 @@ test("large file handling", async () => { }) }) +test("large added files are skipped", 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}/huge.txt`, new Uint8Array(2 * 1024 * 1024 + 1)) + + expect((await Snapshot.patch(before!)).files).toEqual([]) + expect(await Snapshot.diff(before!)).toBe("") + expect(await Snapshot.track()).toBe(before) + }, + }) +}) + test("nested directory revert", async () => { await using tmp = await bootstrap() await Instance.provide({