Skip to content

Commit 4abc590

Browse files
committed
fix: create worktrees from empty git repos
1 parent 1338d7b commit 4abc590

2 files changed

Lines changed: 37 additions & 3 deletions

File tree

packages/opencode/src/worktree/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ function failedRemoves(...chunks: string[]) {
128128
)
129129
}
130130

131+
function branchMissing(...chunks: string[]) {
132+
return chunks.some((chunk) => /branch ['"].+['"] not found/i.test(chunk))
133+
}
134+
131135
// ---------------------------------------------------------------------------
132136
// Effect service
133137
// ---------------------------------------------------------------------------
@@ -229,9 +233,11 @@ export const layer: Layer.Layer<
229233

230234
const setup = Effect.fnUntraced(function* (info: Info) {
231235
const ctx = yield* InstanceState.context
236+
const head = yield* git(["rev-parse", "--verify", "HEAD"], { cwd: ctx.worktree })
237+
const hasHead = head.code === 0
232238
const created = yield* git(
233239
info.branch
234-
? ["worktree", "add", "--no-checkout", "-b", info.branch, info.directory]
240+
? ["worktree", "add", ...(hasHead ? ["--no-checkout"] : []), "-b", info.branch, info.directory]
235241
: ["worktree", "add", "--no-checkout", "--detach", info.directory, "HEAD"],
236242
{ cwd: ctx.worktree },
237243
)
@@ -242,6 +248,7 @@ export const layer: Layer.Layer<
242248
}
243249

244250
yield* project.addSandbox(ctx.project.id, info.directory).pipe(Effect.catch(() => Effect.void))
251+
return undefined
245252
})
246253

247254
const boot = Effect.fnUntraced(function* (info: Info, startCommand?: string) {
@@ -454,7 +461,7 @@ export const layer: Layer.Layer<
454461
const branch = entry.branch?.replace(/^refs\/heads\//, "")
455462
if (branch) {
456463
const deleted = yield* git(["branch", "-D", branch], { cwd: ctx.worktree })
457-
if (deleted.code !== 0) {
464+
if (deleted.code !== 0 && !branchMissing(deleted.stderr, deleted.text)) {
458465
return yield* new RemoveFailedError({
459466
message: deleted.stderr || deleted.text || "Failed to delete worktree branch",
460467
})
@@ -478,7 +485,7 @@ export const layer: Layer.Layer<
478485
function* (directory: string, cmd: string) {
479486
const [shell, args] = process.platform === "win32" ? ["cmd", ["/c", cmd]] : ["bash", ["-lc", cmd]]
480487
const result = yield* appProcess.run(
481-
ChildProcess.make(shell, args as string[], { cwd: directory, extendEnv: true, stdin: "ignore" }),
488+
ChildProcess.make(shell, args, { cwd: directory, extendEnv: true, stdin: "ignore" }),
482489
)
483490
return { code: result.exitCode, stderr: result.stderr.toString("utf8") }
484491
},

packages/opencode/test/project/worktree.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterEach, describe, expect } from "bun:test"
2+
import { $ } from "bun"
23
import path from "path"
34
import { FSUtil } from "@opencode-ai/core/fs-util"
45
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -14,6 +15,15 @@ const it = testEffect(
1415
)
1516
const wintest = process.platform !== "win32" ? it.instance : it.instance.skip
1617

18+
const initEmptyGitRepo = (directory: string) =>
19+
Effect.promise(async () => {
20+
await $`git init`.cwd(directory).quiet()
21+
await $`git config core.fsmonitor false`.cwd(directory).quiet()
22+
await $`git config commit.gpgsign false`.cwd(directory).quiet()
23+
await $`git config user.email test@opencode.test`.cwd(directory).quiet()
24+
await $`git config user.name Test`.cwd(directory).quiet()
25+
})
26+
1727
function normalize(input: string) {
1828
return input.replace(/\\/g, "/").toLowerCase()
1929
}
@@ -41,6 +51,7 @@ const removeCreatedWorktree = (directory: string) =>
4151
const svc = yield* Worktree.Service
4252
const ok = yield* svc.remove({ directory })
4353
if (!ok) return yield* Effect.fail(new Error(`failed to remove worktree ${directory}`))
54+
return undefined
4455
})
4556

4657
const withCreatedWorktree = <A, E, R>(
@@ -174,6 +185,22 @@ describe("Worktree", () => {
174185
})
175186

176187
describe("create + remove lifecycle", () => {
188+
wintest(
189+
"creates git worktree from empty repository",
190+
() =>
191+
withCreatedWorktree({ name: "empty-repo" }, ({ info, ready }) =>
192+
Effect.gen(function* () {
193+
const test = yield* TestInstance
194+
const list = yield* git(test.directory, ["worktree", "list", "--porcelain"])
195+
196+
expect(normalize(list)).toContain(normalize(info.directory))
197+
expect(info.branch).toBe("opencode/empty-repo")
198+
expect(ready.branch).toBe(info.branch)
199+
}),
200+
),
201+
{ init: initEmptyGitRepo },
202+
)
203+
177204
it.instance(
178205
"create returns worktree info and remove cleans up",
179206
() =>

0 commit comments

Comments
 (0)