Browse Source

fix(project): preserve cached project identity

Dax Raad 2 months ago
parent
commit
00a6f228f2

+ 1 - 1
packages/core/src/project.ts

@@ -108,7 +108,7 @@ export const layer = Layer.effect(
       if (!repo) return { id: ID.global, directory: input, vcs: undefined }
 
       const previous = yield* cached(repo.store)
-      const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo))
+      const id = previous ?? (yield* root(repo))
 
       return {
         previous,

+ 6 - 34
packages/core/test/project.test.ts

@@ -5,16 +5,11 @@ import path from "path"
 import { Effect } from "effect"
 import { Project } from "@opencode-ai/core/project"
 import { AbsolutePath } from "@opencode-ai/core/schema"
-import { Hash } from "@opencode-ai/core/util/hash"
 import { tmpdir } from "./fixture/tmpdir"
 import { testEffect } from "./lib/effect"
 
 const it = testEffect(Project.defaultLayer)
 
-function remoteID(remote: string) {
-  return Project.ID.make(Hash.fast(`git-remote:${remote}`))
-}
-
 function abs(value: string) {
   return AbsolutePath.make(value)
 }
@@ -91,7 +86,7 @@ describe("ProjectV2.resolve", () => {
     }),
   )
 
-  it.live("prefers normalized origin over root commit", () =>
+  it.live("uses root commit when origin exists", () =>
     Effect.gen(function* () {
       const tmp = yield* Effect.acquireRelease(
         Effect.promise(() => tmpdir()),
@@ -102,36 +97,13 @@ describe("ProjectV2.resolve", () => {
 
       const result = yield* project.resolve(abs(tmp.path))
 
-      expect(result.id).toBe(remoteID("github.com/Acme/App"))
-      expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
+      expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
       expect(result.directory).toBe(yield* real(tmp.path))
       expect(result.vcs?.type).toBe("git")
     }),
   )
 
-  it.live("normalizes ssh and https remotes to the same id", () =>
-    Effect.gen(function* () {
-      const ssh = yield* Effect.acquireRelease(
-        Effect.promise(() => tmpdir()),
-        (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
-      )
-      const https = yield* Effect.acquireRelease(
-        Effect.promise(() => tmpdir()),
-        (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
-      )
-      yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
-      yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" }))
-      const project = yield* Project.Service
-
-      const a = yield* project.resolve(abs(ssh.path))
-      const b = yield* project.resolve(abs(https.path))
-
-      expect(a.id).toBe(remoteID("github.com/owner/repo"))
-      expect(b.id).toBe(a.id)
-    }),
-  )
-
-  it.live("ignores file remotes and falls back to root commit", () =>
+  it.live("uses root commit when local remote exists", () =>
     Effect.gen(function* () {
       const tmp = yield* Effect.acquireRelease(
         Effect.promise(() => tmpdir()),
@@ -146,7 +118,7 @@ describe("ProjectV2.resolve", () => {
     }),
   )
 
-  it.live("returns previous cached id from common dir", () =>
+  it.live("prefers previous cached id over origin", () =>
     Effect.gen(function* () {
       const tmp = yield* Effect.acquireRelease(
         Effect.promise(() => tmpdir()),
@@ -159,7 +131,7 @@ describe("ProjectV2.resolve", () => {
       const result = yield* project.resolve(abs(tmp.path))
 
       expect(result.previous).toBe(Project.ID.make("old-id"))
-      expect(result.id).toBe(remoteID("github.com/owner/repo"))
+      expect(result.id).toBe(Project.ID.make("old-id"))
     }),
   )
 
@@ -213,7 +185,7 @@ describe("ProjectV2.resolve", () => {
 
       expect(result.directory).toBe(yield* real(worktree))
       expect(result.previous).toBe(Project.ID.make("old-id"))
-      expect(result.id).toBe(remoteID("github.com/owner/repo"))
+      expect(result.id).toBe(Project.ID.make("old-id"))
       expect(result.vcs?.type).toBe("git")
     }),
   )

+ 5 - 81
packages/opencode/test/project/project.test.ts

@@ -7,15 +7,6 @@ import path from "path"
 import { tmpdirScoped } from "../fixture/fixture"
 import { GlobalBus } from "../../src/bus/global"
 import { ProjectID } from "../../src/project/schema"
-import { Database } from "@/storage/db"
-import { ProjectTable } from "@/project/project.sql"
-import { SessionTable } from "@/session/session.sql"
-import { PermissionTable } from "@/session/session.sql"
-import { WorkspaceTable } from "@/control-plane/workspace.sql"
-import { eq } from "drizzle-orm"
-import { Hash } from "@opencode-ai/core/util/hash"
-import { SessionID } from "@/session/schema"
-import { WorkspaceID } from "@/control-plane/schema"
 import { Cause, Effect, Exit, Layer, Stream } from "effect"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import { NodePath } from "@effect/platform-node"
@@ -40,10 +31,6 @@ function run<A, E>(fn: (svc: Project.Interface) => Effect.Effect<A, E>) {
   })
 }
 
-function remoteProjectID(remote: string) {
-  return ProjectID.make(Hash.fast(`git-remote:${remote}`))
-}
-
 /**
  * Creates a mock ChildProcessSpawner layer that intercepts git subcommands
  * matching `failArg` and returns exit code 128, while delegating everything
@@ -167,91 +154,28 @@ describe("Project.fromDirectory", () => {
     }),
   )
 
-  it.live("prefers normalized origin remote over root commit", () =>
+  it.live("keeps root commit identity when origin exists", () =>
     Effect.gen(function* () {
       const tmp = yield* tmpdirScoped({ git: true })
       yield* Effect.promise(() => $`git remote add origin git@github.com:Test-Org/Test-Repo.git`.cwd(tmp).quiet())
 
       const { project } = yield* run((svc) => svc.fromDirectory(tmp))
+      const root = (yield* Effect.promise(() => $`git rev-list --max-parents=0 HEAD`.cwd(tmp).text())).trim()
 
-      expect(project.id).toBe(remoteProjectID("github.com/Test-Org/Test-Repo"))
-    }),
-  )
-
-  it.live("normalizes equivalent origin URL forms to the same project ID", () =>
-    Effect.gen(function* () {
-      const ssh = yield* tmpdirScoped({ git: true })
-      const https = yield* tmpdirScoped({ git: true })
-      yield* Effect.promise(() => $`git remote add origin git@github.com:owner/repo.git`.cwd(ssh).quiet())
-      yield* Effect.promise(() => $`git remote add origin https://github.com/owner/repo.git`.cwd(https).quiet())
-
-      const { project: a } = yield* run((svc) => svc.fromDirectory(ssh))
-      const { project: b } = yield* run((svc) => svc.fromDirectory(https))
-
-      expect(a.id).toBe(remoteProjectID("github.com/owner/repo"))
-      expect(b.id).toBe(a.id)
+      expect(project.id).toBe(ProjectID.make(root))
     }),
   )
 
-  it.live("migrates cached root project data when origin becomes available", () =>
+  it.live("keeps cached project identity when origin becomes available", () =>
     Effect.gen(function* () {
       const tmp = yield* tmpdirScoped({ git: true })
       const projects = yield* Project.Service
       const { project: rootProject } = yield* projects.fromDirectory(tmp)
-      const remoteID = remoteProjectID("github.com/acme/app")
-      const sessionID = crypto.randomUUID() as SessionID
-      const workspaceID = WorkspaceID.ascending()
-
-      yield* Effect.sync(() => {
-        Database.use((db) => {
-          db.insert(SessionTable)
-            .values({
-              id: sessionID,
-              project_id: rootProject.id,
-              slug: sessionID,
-              directory: tmp,
-              title: "test",
-              version: "0.0.0-test",
-              time_created: Date.now(),
-              time_updated: Date.now(),
-            })
-            .run()
-          db.insert(PermissionTable)
-            .values({
-              project_id: rootProject.id,
-              data: [{ permission: "edit", pattern: "*", action: "allow" }],
-              time_created: Date.now(),
-              time_updated: Date.now(),
-            })
-            .run()
-          db.insert(WorkspaceTable)
-            .values({
-              id: workspaceID,
-              type: "local",
-              name: "test",
-              project_id: rootProject.id,
-            })
-            .run()
-        })
-      })
       yield* Effect.promise(() => $`git remote add origin git@github.com:acme/app.git`.cwd(tmp).quiet())
 
       const { project } = yield* projects.fromDirectory(tmp)
 
-      expect(project.id).toBe(remoteID)
-      expect(
-        Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get()),
-      ).toBeUndefined()
-      expect(
-        Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get())?.project_id,
-      ).toBe(remoteID)
-      expect(
-        Database.use((db) => db.select().from(PermissionTable).where(eq(PermissionTable.project_id, remoteID)).get()),
-      ).toBeDefined()
-      expect(
-        Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get())
-          ?.project_id,
-      ).toBe(remoteID)
+      expect(project.id).toBe(rootProject.id)
     }),
   )
 })