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
17 changes: 16 additions & 1 deletion packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,25 @@ export namespace Project {

const { id, sandbox, worktree, vcs } = await iife(async () => {
const matches = Filesystem.up({ targets: [".git"], start: directory })
const git = await matches.next().then((x) => x.value)
let git = await matches.next().then((x) => x.value)
await matches.return()

if (git) {
let sandbox = path.dirname(git)

const gitFile = Bun.file(git);
if (await gitFile.exists()) {
const content = await gitFile.text();
const gitDir = content.match(/^gitdir: (.*)/)?.at(1);
if (gitDir) {
const linkedDir = path.resolve(path.dirname(git), gitDir);
if (existsSync(linkedDir)) {
log.info("followGitFile", { linkedDir })
git = linkedDir;
}
}
}

const gitBinary = Bun.which("git")

// cached id calculation
Expand Down Expand Up @@ -168,6 +182,7 @@ export namespace Project {
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
})
log.info('resolvedProject', {id, sandbox, worktree, vcs})

let existing = await Storage.read<Info>(["project", id]).catch(() => undefined)
if (!existing) {
Expand Down
57 changes: 57 additions & 0 deletions packages/opencode/test/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,63 @@ describe("Project.fromDirectory", () => {
})
})

describe("Project.fromDirectory with .git file resolution", () => {
test("should follow .git file with gitdir: link", async () => {
await using tmp = await tmpdir({ git: true })

// Create a fake worktree by manually creating a .git file with gitdir: content
const fakeWorktreePath = path.join(tmp.path, "..", "fake-worktree")
await $`mkdir -p ${fakeWorktreePath}`.quiet()

// Create the .git file that points to the real git directory
const gitFilePath = path.join(fakeWorktreePath, ".git")
const gitDirPath = path.join(tmp.path, ".git")
await Bun.write(gitFilePath, `gitdir: ${gitDirPath}\n`)

const { project, sandbox } = await Project.fromDirectory(fakeWorktreePath)

expect(project).toBeDefined()
expect(project.id).not.toBe("global")
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
// sandbox is the directory where .git file was found
expect(sandbox).toBe(fakeWorktreePath)

await $`rm -rf ${fakeWorktreePath}`.quiet()
})

test("should handle .git file with relative gitdir path", async () => {
await using tmp = await tmpdir({ git: true })

// Create a fake worktree structure with relative path
const fakeWorktreePath = path.join(tmp.path, "..", "relative-worktree")
await $`mkdir -p ${fakeWorktreePath}`.quiet()

// Use a relative path in the gitdir: line
const gitFilePath = path.join(fakeWorktreePath, ".git")
await Bun.write(gitFilePath, `gitdir: ../opencode-test-${path.basename(tmp.path).split('-').pop()}/.git\n`)

const { project } = await Project.fromDirectory(fakeWorktreePath)

expect(project).toBeDefined()
expect(project.worktree).toBe(tmp.path)

await $`rm -rf ${fakeWorktreePath}`.quiet()
})

test("should handle .git file pointing to non-existent directory", async () => {
await using tmp = await tmpdir()

const gitFilePath = path.join(tmp.path, ".git")
await Bun.write(gitFilePath, `gitdir: /nonexistent/path/.git\n`)

const { project } = await Project.fromDirectory(tmp.path)

// Should fall back to global since the linked git dir doesn't exist
expect(project.id).toBe("global")
})
})

describe("Project.fromDirectory with worktrees", () => {
test("should set worktree to root when called from root", async () => {
await using tmp = await tmpdir({ git: true })
Expand Down