Skip to content
Open
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
87 changes: 87 additions & 0 deletions packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,93 @@ export namespace Project {
log.info("fromDirectory", { directory })

const { id, sandbox, worktree, vcs } = await iife(async () => {
const envGitDir = process.env.GIT_DIR
const envWorkTree = process.env.GIT_WORK_TREE

if (envGitDir && (await fs.stat(envGitDir).catch(() => undefined))) {
log.info("using GIT_DIR from environment", { envGitDir, envWorkTree })
const gitBinary = Bun.which("git")
if (!gitBinary) {
return {
id: "global",
worktree: envWorkTree ?? directory,
sandbox: envWorkTree ?? directory,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}

const git = envGitDir
let sandbox = envWorkTree ?? directory

let id = await Bun.file(path.join(git, "opencode"))
.text()
.then((x) => x.trim())
.catch(() => undefined)

if (!id) {
const roots = await $`git rev-list --max-parents=0 --all`
.env({ ...process.env, GIT_DIR: git })
.quiet()
.nothrow()
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
.catch(() => undefined)

id = roots?.[0]
if (id) {
void Bun.file(path.join(git, "opencode"))
.write(id)
.catch(() => undefined)
}
}

if (!id) {
return {
id: "global",
worktree: sandbox,
sandbox,
vcs: "git",
}
}

if (!envWorkTree) {
const top = await $`git rev-parse --show-toplevel`
.env({ ...process.env, GIT_DIR: git })
.quiet()
.nothrow()
.text()
.then((x) => x.trim())
.catch(() => undefined)

if (top) sandbox = top
}

const worktree = await $`git rev-parse --git-common-dir`
.env({ ...process.env, GIT_DIR: git })
.quiet()
.nothrow()
.text()
.then((x) => {
const dirname = path.dirname(x.trim())
if (dirname === ".") return sandbox
return dirname
})
.catch(() => sandbox)

return {
id,
sandbox,
worktree,
vcs: "git",
}
}

const matches = Filesystem.up({ targets: [".git"], start: directory })
const git = await matches.next().then((x) => x.value)
await matches.return()
Expand Down
78 changes: 78 additions & 0 deletions packages/opencode/test/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,84 @@ describe("Project.fromDirectory with worktrees", () => {
})
})

describe("Project.fromDirectory with GIT_DIR/GIT_WORK_TREE env vars", () => {
test("should respect GIT_DIR and GIT_WORK_TREE when both are set", async () => {
await using tmp = await tmpdir({ git: true })

// Create a separate directory that is NOT a git repo
const separateDir = path.join(tmp.path, "..", "separate-dir")
await $`mkdir -p ${separateDir}`.quiet()

// Set env vars to point to the git repo
const originalGitDir = process.env.GIT_DIR
const originalWorkTree = process.env.GIT_WORK_TREE
try {
process.env.GIT_DIR = path.join(tmp.path, ".git")
process.env.GIT_WORK_TREE = tmp.path

// Call fromDirectory with the separate (non-git) directory
const { project } = await Project.fromDirectory(separateDir)

// Should detect the git repo from env vars, not filesystem walk
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
} finally {
if (originalGitDir === undefined) delete process.env.GIT_DIR
else process.env.GIT_DIR = originalGitDir
if (originalWorkTree === undefined) delete process.env.GIT_WORK_TREE
else process.env.GIT_WORK_TREE = originalWorkTree
await $`rm -rf ${separateDir}`.quiet().nothrow()
}
})

test("should respect only GIT_DIR and derive worktree", async () => {
await using tmp = await tmpdir({ git: true })

const separateDir = path.join(tmp.path, "..", "separate-dir-2")
await $`mkdir -p ${separateDir}`.quiet()

const originalGitDir = process.env.GIT_DIR
const originalWorkTree = process.env.GIT_WORK_TREE
try {
process.env.GIT_DIR = path.join(tmp.path, ".git")
delete process.env.GIT_WORK_TREE

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

expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
} finally {
if (originalGitDir === undefined) delete process.env.GIT_DIR
else process.env.GIT_DIR = originalGitDir
if (originalWorkTree === undefined) delete process.env.GIT_WORK_TREE
else process.env.GIT_WORK_TREE = originalWorkTree
await $`rm -rf ${separateDir}`.quiet().nothrow()
}
})

test("should fall back to filesystem walk when GIT_DIR is invalid", async () => {
await using tmp = await tmpdir({ git: true })

const originalGitDir = process.env.GIT_DIR
const originalWorkTree = process.env.GIT_WORK_TREE
try {
process.env.GIT_DIR = "/nonexistent/path/.git"
process.env.GIT_WORK_TREE = "/nonexistent/path"

// Should fall back to filesystem walk and find the actual git repo
const { project } = await Project.fromDirectory(tmp.path)

expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
} finally {
if (originalGitDir === undefined) delete process.env.GIT_DIR
else process.env.GIT_DIR = originalGitDir
if (originalWorkTree === undefined) delete process.env.GIT_WORK_TREE
else process.env.GIT_WORK_TREE = originalWorkTree
}
})
})

describe("Project.discover", () => {
test("should discover favicon.png in root", async () => {
await using tmp = await tmpdir({ git: true })
Expand Down