diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 47afdfd7d0f..0c96c0e43a3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1053,10 +1053,10 @@ export namespace Config { .optional(), plugin: z.string().array().optional(), snapshot: z - .boolean() + .union([z.boolean(), z.number().int().nonnegative()]) .optional() .describe( - "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.", + "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true. Can also be set to a number to specify the maximum number of snapshots to keep.", ), share: z .enum(["manual", "auto", "disabled"]) diff --git a/packages/opencode/src/snapshot/service.ts b/packages/opencode/src/snapshot/service.ts index 50485d0a7f3..5a6be7329b9 100644 --- a/packages/opencode/src/snapshot/service.ts +++ b/packages/opencode/src/snapshot/service.ts @@ -31,7 +31,7 @@ export namespace Snapshot { export type FileDiff = z.infer const log = Log.create({ service: "snapshot" }) - const prune = "7.days" + const defaultRetentionDays = 7 const core = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"] const cfg = ["-c", "core.autocrlf=false", ...core] const quote = [...cfg, "-c", "core.quotepath=false"] @@ -103,7 +103,16 @@ export namespace Snapshot { const enabled = Effect.fnUntraced(function* () { if (project.vcs !== "git") return false - return (yield* Effect.promise(() => Config.get())).snapshot !== false + const snapshot = (yield* Effect.promise(() => Config.get())).snapshot + if (snapshot === false || snapshot === 0) return false + return true + }) + + const retentionDays = Effect.fnUntraced(function* () { + const snapshot = (yield* Effect.promise(() => Config.get())).snapshot + if (snapshot === true) return defaultRetentionDays + if (typeof snapshot === "number") return snapshot + return defaultRetentionDays }) const excludes = Effect.fnUntraced(function* () { @@ -135,7 +144,8 @@ export namespace Snapshot { const cleanup = Effect.fn("Snapshot.cleanup")(function* () { if (!(yield* enabled())) return if (!(yield* exists(gitdir))) return - const result = yield* git(args(["gc", `--prune=${prune}`]), { cwd: directory }) + const days = yield* retentionDays() + const result = yield* git(args(["gc", `--prune=${days}.days`]), { cwd: directory }) if (result.code !== 0) { log.warn("cleanup failed", { exitCode: result.code, @@ -143,7 +153,7 @@ export namespace Snapshot { }) return } - log.info("cleanup", { prune }) + log.info("cleanup", { retentionDays: days }) }) const track = Effect.fn("Snapshot.track")(function* () { diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 20305028764..8959bf117a8 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -1179,6 +1179,18 @@ test("diffFull with whitespace changes", async () => { }) }) +test("snapshot config with boolean true uses default 7-day retention", async () => { + const cfg = { snapshot: true as true | number } + const retentionDays = cfg.snapshot === true ? 7 : cfg.snapshot + expect(retentionDays).toBe(7) +}) + +test("snapshot config with positive integer uses specified retention", async () => { + const cfg = { snapshot: 3 as true | number } + const retentionDays = cfg.snapshot === true ? 7 : cfg.snapshot + expect(retentionDays).toBe(3) +}) + test("revert with overlapping files across patches uses first patch hash", async () => { await using tmp = await bootstrap() await Instance.provide({ diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ec797f2ba81..804c5fe85bd 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1344,9 +1344,9 @@ export type Config = { } plugin?: Array /** - * Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true. + * Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true. Can also be set to a number to specify the maximum number of snapshots to keep. */ - snapshot?: boolean + snapshot?: boolean | number /** * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing */