Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
28 changes: 20 additions & 8 deletions packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,25 @@ export namespace Project {
}
}

const worktree = await git(["rev-parse", "--git-common-dir"], {
const common = await git(["rev-parse", "--git-common-dir"], {
cwd: sandbox,
})
.then(async (result) => {
const common = gitpath(sandbox, await result.text())
const resolved = gitpath(sandbox, await result.text())
// Avoid going to parent of sandbox when git-common-dir is empty.
return common === sandbox ? sandbox : path.dirname(common)
return resolved === sandbox ? sandbox : resolved
})
.catch(() => undefined)

if (!worktree) {
// worktree is the repo root: parent of .git for normal repos,
// but for bare repos (e.g. opencode.git) path.dirname gives
// the parent directory which may be shared across repos.
// If common resolves to sandbox (empty git-common-dir output),
// preserve sandbox to keep previous fallback behavior.
// Only used for the returned project info, not for cache paths.
const worktree = common ? (common === sandbox ? sandbox : path.dirname(common)) : undefined

if (!worktree || !common) {
return {
id: id ?? ProjectID.global,
worktree: sandbox,
Expand All @@ -140,9 +148,11 @@ export namespace Project {

// In the case of a git worktree, it can't cache the id
// because `.git` is not a folder, but it always needs the
// same project id as the common dir, so we resolve it now
// same project id as the common dir, so we resolve it now.
// Use `common` directly (the git dir) instead of reconstructing
// worktree + "/.git" which breaks for bare repos (e.g. repo.git).
if (id == null) {
id = await readCachedId(path.join(worktree, ".git"))
id = await readCachedId(common)
}

// generate id from root commit
Expand Down Expand Up @@ -170,8 +180,10 @@ export namespace Project {

id = roots[0] ? ProjectID.make(roots[0]) : undefined
if (id) {
// Write to common dir so the cache is shared across worktrees.
await Filesystem.write(path.join(worktree, ".git", "opencode"), id).catch(() => undefined)
// Write cache inside the git common dir itself, not worktree/.git.
// For bare repos (e.g. opencode.git), worktree/.git would resolve
// to the parent org directory's .git, corrupting other repos.
await Filesystem.write(path.join(common, "opencode"), id).catch(() => undefined)
}
}

Expand Down
22 changes: 22 additions & 0 deletions packages/opencode/test/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,28 @@ describe("Project.fromDirectory with worktrees", () => {
}
})

test("different bare repos under same parent should not share project ID", async () => {
const p = await loadProject()
await using parent = await tmpdir()
await using repoA = await tmpdir({ git: true })
await using repoB = await tmpdir({ git: true })

const bareA = path.join(parent.path, "repo-a.git")
const bareB = path.join(parent.path, "repo-b.git")
const wtA = path.join(parent.path, "wt-a")
const wtB = path.join(parent.path, "wt-b")

await $`git clone --bare ${repoA.path} ${bareA}`.quiet()
await $`git clone --bare ${repoB.path} ${bareB}`.quiet()
await $`git --git-dir=${bareA} worktree add ${wtA} -b wt-a-${Date.now()} HEAD`.quiet()
await $`git --git-dir=${bareB} worktree add ${wtB} -b wt-b-${Date.now()} HEAD`.quiet()

const { project: a } = await p.fromDirectory(wtA)
const { project: b } = await p.fromDirectory(wtB)

expect(a.id).not.toBe(b.id)
})

test("should accumulate multiple worktrees in sandboxes", async () => {
const p = await loadProject()
await using tmp = await tmpdir({ git: true })
Expand Down
Loading