From e6834280e8fe18faa6d2e948b151ab86b35e8572 Mon Sep 17 00:00:00 2001 From: Michael Dwan Date: Tue, 10 Mar 2026 11:34:12 -0600 Subject: [PATCH 1/2] fix(opencode): stable project ID across worktrees and orphan branches The project ID cache was written to the .git path, which is a file (not a directory) in worktrees, so writes silently failed. Write to the git common dir instead so all worktrees share the cached ID. Switch rev-list from --all to HEAD so orphan branches (gh-pages, stash roots, subtree merges) cannot shift the sorted root set and produce a different project ID on restart. --- packages/opencode/src/project/project.ts | 7 ++- packages/opencode/test/fixture/fixture.ts | 2 + .../opencode/test/project/project.test.ts | 48 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 68cece0a52ad..6b88cb3ef89c 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -146,7 +146,7 @@ export namespace Project { // generate id from root commit if (!id) { - const roots = await git(["rev-list", "--max-parents=0", "--all"], { + const roots = await git(["rev-list", "--max-parents=0", "HEAD"], { cwd: sandbox, }) .then(async (result) => @@ -169,7 +169,10 @@ export namespace Project { id = roots[0] if (id) { - await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) + // Write to common dir so the cache is shared across worktrees. + // Falls back to dotgit for non-worktree repos (worktree === sandbox). + const cache = worktree !== sandbox ? path.join(worktree, ".git", "opencode") : path.join(dotgit, "opencode") + await Filesystem.write(cache, id).catch(() => undefined) } } diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index 63f93bcafe95..f2f864e8b19f 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -42,6 +42,8 @@ export async function tmpdir(options?: TmpDirOptions) { if (options?.git) { await $`git init`.cwd(dirpath).quiet() await $`git config core.fsmonitor false`.cwd(dirpath).quiet() + await $`git config user.email "test@opencode.test"`.cwd(dirpath).quiet() + await $`git config user.name "Test"`.cwd(dirpath).quiet() await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet() } if (options?.config) { diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index fef9e4190e26..202a3725fabf 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -22,7 +22,7 @@ mock.module("../../src/util/git", () => ({ mode === "rev-list-fail" && cmd.includes("git rev-list") && cmd.includes("--max-parents=0") && - cmd.includes("--all") + cmd.includes("HEAD") ) { return Promise.resolve({ exitCode: 128, @@ -171,6 +171,52 @@ describe("Project.fromDirectory with worktrees", () => { } }) + test("worktree should share project ID with main repo", async () => { + const p = await loadProject() + await using tmp = await tmpdir({ git: true }) + + const { project: main } = await p.fromDirectory(tmp.path) + + const worktreePath = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt-shared") + try { + await $`git worktree add ${worktreePath} -b shared-${Date.now()}`.cwd(tmp.path).quiet() + + const { project: wt } = await p.fromDirectory(worktreePath) + + expect(wt.id).toBe(main.id) + + // Cache should live in the common .git dir, not the worktree's .git file + const cache = path.join(tmp.path, ".git", "opencode") + const exists = await Filesystem.exists(cache) + expect(exists).toBe(true) + } finally { + await $`git worktree remove ${worktreePath}` + .cwd(tmp.path) + .quiet() + .catch(() => {}) + } + }) + + test("separate clones of the same repo should share project ID", async () => { + const p = await loadProject() + await using tmp = await tmpdir({ git: true }) + + // Create a bare remote, push, then clone into a second directory + const bare = tmp.path + "-bare" + const clone = tmp.path + "-clone" + try { + await $`git clone --bare ${tmp.path} ${bare}`.quiet() + await $`git clone ${bare} ${clone}`.quiet() + + const { project: a } = await p.fromDirectory(tmp.path) + const { project: b } = await p.fromDirectory(clone) + + expect(b.id).toBe(a.id) + } finally { + await $`rm -rf ${bare} ${clone}`.quiet().nothrow() + } + }) + test("should accumulate multiple worktrees in sandboxes", async () => { const p = await loadProject() await using tmp = await tmpdir({ git: true }) From 3d5cea8a2f07d4e4bd353079885584bc34367130 Mon Sep 17 00:00:00 2001 From: Michael Dwan Date: Wed, 11 Mar 2026 21:51:47 -0600 Subject: [PATCH 2/2] simplify worktree cache path per review feedback --- packages/opencode/src/project/project.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 6b88cb3ef89c..827cc6471a64 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -170,9 +170,7 @@ export namespace Project { id = roots[0] if (id) { // Write to common dir so the cache is shared across worktrees. - // Falls back to dotgit for non-worktree repos (worktree === sandbox). - const cache = worktree !== sandbox ? path.join(worktree, ".git", "opencode") : path.join(dotgit, "opencode") - await Filesystem.write(cache, id).catch(() => undefined) + await Filesystem.write(path.join(worktree, ".git", "opencode"), id).catch(() => undefined) } }