diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts index f5ccabf8c5..1a7cd8cec2 100644 --- a/packages/core/src/location.ts +++ b/packages/core/src/location.ts @@ -1,6 +1,7 @@ import { Context, Effect, Layer } from "effect" import { Info, Ref, response } from "@opencode-ai/schema/location" import { Project } from "./project" +import { Git } from "./git" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { makeLocationNode, tags } from "@opencode-ai/util/effect/app-node" @@ -10,6 +11,7 @@ export { Info, Ref, response } export interface Interface extends Info { readonly vcs?: Project.Vcs + readonly repository?: Git.Repository } export class Service extends Context.Service()("@opencode/Location") {} @@ -27,6 +29,7 @@ const layer = (ref: Ref) => workspaceID: ref.workspaceID, project: { id: resolved.id, directory: resolved.directory }, vcs: resolved.vcs, + repository: resolved.repository, }) }), ) diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 30ea465407..53ac69023c 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -42,6 +42,7 @@ export interface Resolved { readonly id: ID readonly directory: AbsolutePath readonly vcs?: Vcs + readonly repository?: Git.Repository } // Keep this filesystem-only; permission checks use it and should not execute VCS commands. @@ -217,6 +218,7 @@ const layer = Layer.effect( id: id ?? ID.global, directory: repo.worktree, vcs: { type: "git" as const, store: repo.commonDirectory }, + repository: repo, } } diff --git a/packages/core/src/snapshot.ts b/packages/core/src/snapshot.ts index 72982fbd90..b13a994fef 100644 --- a/packages/core/src/snapshot.ts +++ b/packages/core/src/snapshot.ts @@ -95,7 +95,7 @@ const layer = Layer.effect( // Cache a scope-owned fiber so caller cancellation stops waiting without poisoning shared initialization. const repositoryFiber = yield* Effect.cached( Effect.gen(function* () { - const source = yield* git.repo.discover(location.project.directory) + const source = location.repository ?? (yield* git.repo.discover(location.project.directory)) if (!source) return yield* new Error({ operation: "capture", message: "Project is not a Git repository" }) const worktree = AbsolutePath.make(yield* fs.realPath(source.worktree).pipe(Effect.orDie)) const gitDirectory = AbsolutePath.make( diff --git a/packages/core/test/location.test.ts b/packages/core/test/location.test.ts index d012a0463f..884f198b59 100644 --- a/packages/core/test/location.test.ts +++ b/packages/core/test/location.test.ts @@ -3,12 +3,18 @@ import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" +import { Git } from "@opencode-ai/core/git" import { AbsolutePath } from "@opencode-ai/core/schema" import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { testEffect } from "./lib/effect" const workspaceID = WorkspaceV2.ID.make("wrk_test") const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID } +const repository = new Git.Repository({ + worktree: AbsolutePath.make("/repo"), + gitDirectory: AbsolutePath.make("/repo/.git"), + commonDirectory: AbsolutePath.make("/repo/.git"), +}) const projectLayer = Layer.succeed( Project.Service, Project.Service.of({ @@ -19,6 +25,7 @@ const projectLayer = Layer.succeed( id: Project.ID.make("project"), directory: AbsolutePath.make("/repo"), vcs: { type: "git", store: AbsolutePath.make("/repo/.git") }, + repository, }), commit: () => Effect.void, }), @@ -38,6 +45,7 @@ describe("Location", () => { type: "git", store: AbsolutePath.make("/repo/.git"), }) + expect(location.repository).toBe(repository) }), ) }) diff --git a/packages/core/test/snapshot.test.ts b/packages/core/test/snapshot.test.ts index eba95e6626..3df0f3f7c6 100644 --- a/packages/core/test/snapshot.test.ts +++ b/packages/core/test/snapshot.test.ts @@ -14,7 +14,7 @@ import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" describe("Snapshot", () => { - testEffect(Layer.empty).live("keeps lazy repository discovery after the first caller is interrupted", () => + testEffect(Layer.empty).live("reuses the Location repository after the first caller is interrupted", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => @@ -65,16 +65,16 @@ describe("Snapshot", () => { const interrupted = yield* snapshot.capture().pipe(Effect.forkChild) yield* Deferred.await(started) - expect(discoveries).toBe(1) + expect(discoveries).toBe(0) expect(creations).toBe(1) yield* Fiber.interrupt(interrupted) const capture = yield* snapshot.capture().pipe(Effect.forkChild) - expect(discoveries).toBe(1) + expect(discoveries).toBe(0) expect(creations).toBe(1) yield* Deferred.succeed(release, undefined) expect(yield* Fiber.join(capture)).toBeDefined() - expect(discoveries).toBe(1) + expect(discoveries).toBe(0) expect(creations).toBe(1) }).pipe(Effect.provide(layer)) }),