diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 2e773f71e256..4a21525f438b 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -49,9 +49,11 @@ export const Info = Schema.Struct({ description: "@deprecated Use 'references' field instead. Named git or local directory references", }), watcher: Schema.optional(Schema.Struct({ ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))) })), - snapshot: Schema.optional(Schema.Boolean).annotate({ + snapshot: Schema.optional( + Schema.Union([Schema.Boolean, NonNegativeInt]) + ).annotate({ description: - "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 how many days snapshots should be retained for.", }), plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPluginV1.Spec))), share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({ diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 4da9bc3ca864..ed6e867e1cb5 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -20,7 +20,7 @@ export type Patch = typeof Patch.Type export const FileDiff = Info export type FileDiff = typeof FileDiff.Type -const prune = "7.days" +const defaultRetentionDays = 7 const limit = 2 * 1024 * 1024 const core = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"] const cfg = ["-c", "core.autocrlf=false", ...core] @@ -166,7 +166,14 @@ const layer: Layer.Layer Effect.void)) + } + } + + // Prune loose objects older than retention period + const result = yield* git(args(["prune", `--expire=${days}.days`])) if (result.code !== 0) { - yield* Effect.logWarning("cleanup failed", { + yield* Effect.logWarning("prune encountered errors (continuing cleanup)", { exitCode: result.code, stderr: result.stderr, }) - return } - yield* Effect.logInfo("cleanup", { prune }) + + // Remove empty object directories + const objectsDir = path.join(state.gitdir, "objects") + const entries = yield* fs.readDirectoryEntries(objectsDir).pipe(Effect.orDie) + for (const entry of entries) { + if (entry.type === "directory" && entry.name !== "pack" && entry.name !== "info") { + const dirPath = path.join(objectsDir, entry.name) + const dirEntries = yield* fs.readDirectoryEntries(dirPath).pipe(Effect.orDie) + if (dirEntries.length === 0) { + yield* fs.remove(dirPath).pipe(Effect.catch(() => Effect.void)) + } + } + } + + yield* Effect.logInfo("cleanup", { retentionDays: days }) }), ) }) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index a39624087c63..f69b8f95dd5d 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -1,4 +1,4 @@ -import { afterEach, expect } from "bun:test" +import { afterEach, expect, test } from "bun:test" import { $ } from "bun" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { LayerNode } from "@opencode-ai/core/effect/layer-node" @@ -1216,3 +1216,15 @@ it.instance( }), { git: true }, ) + +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) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5e067f3afb23..6a7094f2638f 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1907,7 +1907,10 @@ export type Config = { watcher?: { ignore?: Array } - snapshot?: boolean + /** + * 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 how many days snapshots should be retained for. + */ + snapshot?: boolean | number plugin?: Array< | string | [