Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/core/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export const layer = Layer.effect(
if (!origin) return undefined
const normalized = url(origin)
if (!normalized) return undefined
return ID.make(Hash.fast(`git-remote:${normalized}`))
const storeHash = Hash.short(repo.store)
return ID.make(Hash.fast(`git-remote:${normalized}|${storeHash}`))
})

function url(input: string) {
Expand Down Expand Up @@ -100,7 +101,9 @@ export const layer = Layer.effect(

const root = Effect.fnUntraced(function* (repo: Git.Repo) {
const root = (yield* git.roots(repo))[0]
return root ? ID.make(root) : undefined
if (!root) return undefined
const storeHash = Hash.short(repo.store)
return ID.make(Hash.fast(`git-root:${root}|${storeHash}`))
})

const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/util/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ export namespace Hash {
export function fast(input: string | Buffer): string {
return createHash("sha1").update(input).digest("hex")
}

/** The first 14 characters of the SHA1 hash are used as a short identifier to distinguish different git stores. */
export function short(input: string): string {
return fast(input).substring(0, 14)
}
}
42 changes: 30 additions & 12 deletions packages/core/test/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ import { testEffect } from "./lib/effect"

const it = testEffect(ProjectV2.defaultLayer)

function remoteID(remote: string) {
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))
function remoteID(remote: string, store: string) {
const storeHash = Hash.short(store)
return Project.ID.make(Hash.fast(`git-remote:${remote}|${storeHash}`))
}

function rootID(root: string, store: string) {
const storeHash = Hash.short(store)
return Project.ID.make(Hash.fast(`git-root:${root}|${storeHash}`))
}

function abs(value: string) {
Expand Down Expand Up @@ -84,7 +90,8 @@ describe("ProjectV2.resolve", () => {

const result = yield* project.resolve(abs(tmp.path))

expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
const store = path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), ".git")
expect(result.id).toBe(rootID(yield* Effect.promise(() => rootCommit(tmp.path)), store))
expect(result.directory).toBe(yield* real(tmp.path))
expect(result.previous).toBeUndefined()
expect(result.vcs?.type).toBe("git")
Expand All @@ -102,9 +109,11 @@ 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(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
const store = path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), ".git")
expect(result.id).toBe(remoteID("github.com/Acme/App", store))
expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
expect(result.directory).toBe(yield* real(tmp.path))
expect(result.previous).toBeUndefined()
expect(result.vcs?.type).toBe("git")
}),
)
Expand All @@ -126,8 +135,14 @@ describe("ProjectV2.resolve", () => {
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)
// 两个独立 clone 的 store 不同,所以 ID 不同
Comment thread
FQXCS marked this conversation as resolved.
const storeSsh = path.join(yield* Effect.promise(() => fs.realpath(ssh.path)), ".git")
const storeHttps = path.join(yield* Effect.promise(() => fs.realpath(https.path)), ".git")
expect(a.id).not.toBe(Project.ID.global)
expect(b.id).not.toBe(Project.ID.global)
expect(a.id).toBe(remoteID("github.com/owner/repo", storeSsh))
expect(b.id).toBe(remoteID("github.com/owner/repo", storeHttps))
expect(a.id).not.toBe(b.id)
}),
)

Expand All @@ -142,7 +157,8 @@ describe("ProjectV2.resolve", () => {

const result = yield* project.resolve(abs(tmp.path))

expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
const store = path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), ".git")
expect(result.id).toBe(rootID(yield* Effect.promise(() => rootCommit(tmp.path)), store))
}),
)

Expand All @@ -158,8 +174,9 @@ describe("ProjectV2.resolve", () => {

const result = yield* project.resolve(abs(tmp.path))

expect(result.previous).toBe(ProjectV2.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo"))
const store = path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), ".git")
expect(result.previous).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo", store))
}),
)

Expand Down Expand Up @@ -211,9 +228,10 @@ describe("ProjectV2.resolve", () => {

const result = yield* project.resolve(abs(worktree))

const store = path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), ".git")
expect(result.directory).toBe(yield* real(worktree))
expect(result.previous).toBe(ProjectV2.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo"))
expect(result.previous).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo", store))
expect(result.vcs?.type).toBe("git")
}),
)
Expand Down
37 changes: 25 additions & 12 deletions packages/opencode/test/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,20 @@ const encoder = new TextEncoder()
const layer = Layer.mergeAll(Project.defaultLayer, Database.defaultLayer, CrossSpawnSpawner.defaultLayer)
const it = testEffect(layer)

function remoteProjectID(remote: string) {
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))
function run<A, E>(fn: (svc: Project.Interface) => Effect.Effect<A, E>) {
return Effect.gen(function* () {
const svc = yield* Project.Service
return yield* fn(svc)
})
}

function remoteProjectID(remote: string, store: string) {
const storeHash = Hash.short(store)
return ProjectID.make(Hash.fast(`git-remote:${remote}|${storeHash}`))
Comment thread
FQXCS marked this conversation as resolved.
}

function storePath(tmp: string) {
return path.join(tmp, ".git")
}

/**
Expand Down Expand Up @@ -173,7 +185,7 @@ describe("Project.fromDirectory", () => {

const result = yield* project.fromDirectory(tmp)

expect(result.project.id).toBe(remoteProjectID("github.com/Test-Org/Test-Repo"))
expect(project.id).toBe(remoteProjectID("github.com/Test-Org/Test-Repo", storePath(tmp)))
Comment thread
FQXCS marked this conversation as resolved.
}),
)

Expand All @@ -188,8 +200,10 @@ describe("Project.fromDirectory", () => {
const result = yield* project.fromDirectory(ssh)
const next = yield* project.fromDirectory(https)

expect(result.project.id).toBe(remoteProjectID("github.com/owner/repo"))
expect(next.project.id).toBe(result.project.id)
// 不同 store → 不同 ID
Comment thread
FQXCS marked this conversation as resolved.
expect(a.id).toBe(remoteProjectID("github.com/owner/repo", storePath(ssh)))
expect(b.id).toBe(remoteProjectID("github.com/owner/repo", storePath(https)))
expect(a.id).not.toBe(b.id)
Comment thread
FQXCS marked this conversation as resolved.
}),
)

Expand All @@ -198,9 +212,8 @@ describe("Project.fromDirectory", () => {
const { db } = yield* Database.Service
const tmp = yield* tmpdirScoped({ git: true })
const projects = yield* Project.Service
const rootResult = yield* projects.fromDirectory(tmp)
const rootProject = rootResult.project
const remoteID = remoteProjectID("github.com/acme/app")
const { project: rootProject } = yield* projects.fromDirectory(tmp)
const remoteID = remoteProjectID("github.com/acme/app", storePath(tmp))
const sessionID = crypto.randomUUID() as SessionID
const workspaceID = WorkspaceV2.ID.ascending()

Expand Down Expand Up @@ -348,7 +361,7 @@ describe("Project.fromDirectory with worktrees", () => {
}),
)

it.live("separate clones of the same repo should share project ID", () =>
it.live("separate clones of the same repo should have different project IDs", () =>
Effect.gen(function* () {
const project = yield* Project.Service
const tmp = yield* tmpdirScoped({ git: true })
Expand All @@ -362,10 +375,10 @@ describe("Project.fromDirectory with worktrees", () => {
yield* Effect.promise(() => $`git clone --bare ${tmp} ${bare}`.quiet())
yield* Effect.promise(() => $`git clone ${bare} ${clone}`.quiet())

const result = yield* project.fromDirectory(tmp)
const next = yield* project.fromDirectory(clone)
const { project: a } = yield* run((svc) => svc.fromDirectory(tmp))
const { project: b } = yield* run((svc) => svc.fromDirectory(clone))

expect(next.project.id).toBe(result.project.id)
expect(b.id).not.toBe(a.id)
}),
)

Expand Down
Loading