diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index bc6cd871ca0..d1fefb1531f 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -1,7 +1,6 @@ export * as Git from "./git" import path from "path" -import { randomUUID } from "crypto" import { Context, Effect, Layer, Schema, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" import { AbsolutePath, RelativePath } from "./schema" @@ -175,17 +174,10 @@ export interface Interface { context?: number paths?: readonly RelativePath[] }) => Effect.Effect - readonly preview: (input: { - repository: Repository - current: TreeID - files: ReadonlyMap - context?: number - }) => Effect.Effect readonly restore: (input: { repository: Repository files: ReadonlyMap }) => Effect.Effect - readonly checkout: (input: { repository: Repository; tree: TreeID }) => Effect.Effect } } @@ -657,58 +649,6 @@ const layer = Layer.effect( return { mode: match[1], object: match[2] } }) - const preview = Effect.fn("Git.tree.preview")( - (input: { - repository: Repository - current: TreeID - files: ReadonlyMap - context?: number - }) => - locked( - input.repository, - Effect.gen(function* () { - const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`) - const env = { GIT_INDEX_FILE: index } - return yield* Effect.gen(function* () { - yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env }) - yield* Effect.forEach( - input.files, - ([file, tree]) => - Effect.gen(function* () { - const source = yield* entry(input.repository, tree, file) - if (!source) { - yield* repositoryOperation( - "diff", - input.repository, - ["update-index", "--force-remove", "--", file], - { env }, - ) - return - } - yield* repositoryOperation( - "diff", - input.repository, - ["update-index", "--add", "--cacheinfo", source.mode, source.object, file], - { env }, - ) - }), - { discard: true }, - ) - const target = TreeID.make( - (yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(), - ) - return yield* treeDiff({ - repository: input.repository, - from: input.current, - to: target, - context: input.context, - paths: Array.from(input.files.keys()), - }) - }).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void)))) - }), - ), - ) - const restore = Effect.fn("Git.tree.restore")( (input: { repository: Repository; files: ReadonlyMap }) => locked( @@ -738,16 +678,6 @@ const layer = Layer.effect( ), ) - const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) => - locked( - input.repository, - Effect.gen(function* () { - yield* repositoryOperation("restore", input.repository, ["read-tree", input.tree]) - yield* repositoryOperation("restore", input.repository, ["checkout-index", "--all", "--force"]) - }), - ), - ) - const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) { const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "." const tracked = yield* execute( @@ -957,9 +887,7 @@ const layer = Layer.effect( write: writeTree, files: treeFiles, diff: treeDiff, - preview, restore, - checkout: checkoutTree, }, }) }), diff --git a/packages/core/src/snapshot.ts b/packages/core/src/snapshot.ts index 5113b3f4c6f..29ebffc589e 100644 --- a/packages/core/src/snapshot.ts +++ b/packages/core/src/snapshot.ts @@ -16,7 +16,7 @@ import { Hash } from "@opencode-ai/util/hash" export { ID } export class Error extends Schema.TaggedErrorClass()("Snapshot.Error", { - operation: Schema.Literals(["capture", "files", "diff", "preview", "restore"]), + operation: Schema.Literals(["capture", "files", "diff", "restore"]), message: Schema.String, cause: Schema.optional(Schema.Defect()), }) {} @@ -36,10 +36,6 @@ export interface RestoreInput { readonly files: ReadonlyMap } -export interface PreviewInput extends RestoreInput { - readonly context?: number -} - export interface Interface { /** * Capture the current Location-scoped filesystem state as a content-addressed @@ -60,25 +56,11 @@ export interface Interface { */ readonly diff: (input: DiffInput) => Effect.Effect - /** - * Preview the filesystem result of a selective restore without modifying the - * worktree. Each project-relative path maps to the tree it would be restored - * from. - */ - readonly preview: (input: PreviewInput) => Effect.Effect - /** * Restore selected project-relative paths from their associated trees. A path * absent from its selected tree is removed; paths outside the map are untouched. - */ + */ readonly restore: (input: RestoreInput) => Effect.Effect - - /** - * Replace the snapshot index with a captured tree and check out all its entries. - * Files absent from the tree remain untouched. Prefer selective `restore` when - * only known paths should change. - */ - readonly checkout: (snapshot: ID) => Effect.Effect } export class Service extends Context.Service()("@opencode/Snapshot") {} @@ -176,59 +158,26 @@ const layer = Layer.effect( .pipe(Effect.mapError((cause) => failure("diff", cause))) }) - const plan = Effect.fnUntraced(function* ( - operation: "preview" | "restore", - worktree: AbsolutePath, - input: RestoreInput, - ) { + const plan = Effect.fnUntraced(function* (worktree: AbsolutePath, input: RestoreInput) { const files = new Map() for (const [file, snapshot] of input.files) { const absolute = path.resolve(worktree, file) if (!FSUtil.contains(worktree, absolute)) - return yield* new Error({ operation, message: `Path escapes the project: ${file}` }) + return yield* new Error({ operation: "restore", message: `Path escapes the project: ${file}` }) files.set(file, Git.TreeID.make(snapshot)) } return files }) - const preview = Effect.fn("Snapshot.preview")(function* (input: PreviewInput) { - if (!(yield* enabled())) return yield* new Error({ operation: "preview", message: "Snapshots are disabled" }) - const repo = yield* repository.pipe(Effect.mapError((cause) => failure("preview", cause))) - const files = yield* plan("preview", repo.worktree, input) - const current = yield* git.tree - .capture({ - repository: repo.snapshotRepository, - scopes: Array.from(files.keys()), - ignores: repo.source, - maximumUntrackedFileBytes: 2 * 1024 * 1024, - }) - .pipe(Effect.mapError((cause) => failure("preview", cause))) - return yield* git.tree - .preview({ - repository: repo.snapshotRepository, - current, - files, - context: input.context, - }) - .pipe(Effect.mapError((cause) => failure("preview", cause))) - }) - const restore = Effect.fn("Snapshot.restore")(function* (input: RestoreInput) { if (!(yield* enabled())) return yield* new Error({ operation: "restore", message: "Snapshots are disabled" }) const repo = yield* repository.pipe(Effect.mapError((cause) => failure("restore", cause))) yield* git.tree - .restore({ repository: repo.snapshotRepository, files: yield* plan("restore", repo.worktree, input) }) + .restore({ repository: repo.snapshotRepository, files: yield* plan(repo.worktree, input) }) .pipe(Effect.mapError((cause) => failure("restore", cause))) }) - const checkout = Effect.fn("Snapshot.checkout")(function* (snapshot: ID) { - const repo = yield* repository.pipe(Effect.mapError((cause) => failure("restore", cause))) - yield* git.tree - .checkout({ repository: repo.snapshotRepository, tree: Git.TreeID.make(snapshot) }) - .pipe(Effect.mapError((cause) => failure("restore", cause))) - }) - - return Service.of({ capture, files, diff, preview, restore, checkout }) + return Service.of({ capture, files, diff, restore }) }).pipe(Effect.withSpan("Snapshot.boot")), ) @@ -244,9 +193,7 @@ export const noopLayer = Layer.succeed( capture: () => Effect.succeed(undefined), files: () => Effect.succeed([]), diff: () => Effect.succeed([]), - preview: () => Effect.succeed([]), restore: () => Effect.void, - checkout: () => Effect.void, }), ) diff --git a/packages/core/test/git.test.ts b/packages/core/test/git.test.ts index 055a6b76407..26aface5730 100644 --- a/packages/core/test/git.test.ts +++ b/packages/core/test/git.test.ts @@ -185,9 +185,6 @@ describe("Git trees", () => { ]) const files = new Map([[RelativePath.make("scope/tracked.txt"), before]]) - const preview = yield* git.tree.preview({ repository, current: after, files, context: 1 }) - expect(preview).toHaveLength(1) - expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt")) yield* git.tree.restore({ repository, files }) expect(yield* read(path.join(root.path, "scope", "tracked.txt"))).toBe("one\n") expect(yield* read(path.join(root.path, "scope", "added.txt"))).toBe("added\n") diff --git a/packages/core/test/snapshot.test.ts b/packages/core/test/snapshot.test.ts index eba95e66267..c36d2d447a6 100644 --- a/packages/core/test/snapshot.test.ts +++ b/packages/core/test/snapshot.test.ts @@ -117,9 +117,6 @@ describe("Snapshot", () => { RelativePath.make("scope/tracked.txt"), ]) const plan = new Map([[RelativePath.make("scope/tracked.txt"), before]]) - const preview = yield* snapshot.preview({ files: plan, context: 1 }) - expect(preview).toHaveLength(1) - expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt")) yield* snapshot.restore({ files: plan }) expect(yield* read(path.join(location, "tracked.txt"))).toBe("one\n") expect(yield* read(path.join(location, "added.txt"))).toBe("added\n") @@ -185,36 +182,6 @@ describe("Snapshot", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ), ) - - testEffect(Layer.empty).live("checks out a legacy revert snapshot without removing unrelated files", () => - Effect.acquireUseRelease( - Effect.promise(() => tmpdir()), - (tmp) => - Effect.gen(function* () { - const project = path.join(tmp.path, "project") - yield* Effect.promise(async () => { - await fs.mkdir(project) - await fs.writeFile(path.join(project, "tracked.txt"), "one\n") - await initGit(project) - }) - - yield* Effect.gen(function* () { - const snapshot = yield* Snapshot.Service - const before = yield* snapshot.capture() - expect(before).toBeDefined() - if (!before) return - yield* Effect.promise(async () => { - await fs.writeFile(path.join(project, "tracked.txt"), "two\n") - await fs.writeFile(path.join(project, "unrelated.txt"), "keep\n") - }) - yield* snapshot.checkout(before) - expect(yield* read(path.join(project, "tracked.txt"))).toBe("one\n") - expect(yield* read(path.join(project, "unrelated.txt"))).toBe("keep\n") - }).pipe(Effect.provide(snapshotLayer(tmp.path, project))) - }), - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ), - ) }) function snapshotLayer(data: string, directory: string) {