feat: add managed git worktrees (#6674)

This commit is contained in:
Adam
2026-01-02 20:17:02 -06:00
committed by GitHub
parent f6fe709f6e
commit 052de3c556
11 changed files with 692 additions and 116 deletions

View File

@@ -0,0 +1,215 @@
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Global } from "../global"
import { Instance } from "../project/instance"
import { Project } from "../project/project"
import { fn } from "../util/fn"
export namespace Worktree {
export const Info = z
.object({
name: z.string(),
branch: z.string(),
directory: z.string(),
})
.meta({
ref: "Worktree",
})
export type Info = z.infer<typeof Info>
export const CreateInput = z
.object({
name: z.string().optional(),
startCommand: z.string().optional(),
})
.meta({
ref: "WorktreeCreateInput",
})
export type CreateInput = z.infer<typeof CreateInput>
export const NotGitError = NamedError.create(
"WorktreeNotGitError",
z.object({
message: z.string(),
}),
)
export const NameGenerationFailedError = NamedError.create(
"WorktreeNameGenerationFailedError",
z.object({
message: z.string(),
}),
)
export const CreateFailedError = NamedError.create(
"WorktreeCreateFailedError",
z.object({
message: z.string(),
}),
)
export const StartCommandFailedError = NamedError.create(
"WorktreeStartCommandFailedError",
z.object({
message: z.string(),
}),
)
const ADJECTIVES = [
"brave",
"calm",
"clever",
"cosmic",
"crisp",
"curious",
"eager",
"gentle",
"glowing",
"happy",
"hidden",
"jolly",
"kind",
"lucky",
"mighty",
"misty",
"neon",
"nimble",
"playful",
"proud",
"quick",
"quiet",
"shiny",
"silent",
"stellar",
"sunny",
"swift",
"tidy",
"witty",
] as const
const NOUNS = [
"cabin",
"cactus",
"canyon",
"circuit",
"comet",
"eagle",
"engine",
"falcon",
"forest",
"garden",
"harbor",
"island",
"knight",
"lagoon",
"meadow",
"moon",
"mountain",
"nebula",
"orchid",
"otter",
"panda",
"pixel",
"planet",
"river",
"rocket",
"sailor",
"squid",
"star",
"tiger",
"wizard",
"wolf",
] as const
function pick<const T extends readonly string[]>(list: T) {
return list[Math.floor(Math.random() * list.length)]
}
function slug(input: string) {
return input
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
}
function randomName() {
return `${pick(ADJECTIVES)}-${pick(NOUNS)}`
}
async function exists(target: string) {
return fs
.stat(target)
.then(() => true)
.catch(() => false)
}
function outputText(input: Uint8Array | undefined) {
if (!input?.length) return ""
return new TextDecoder().decode(input).trim()
}
function errorText(result: { stdout?: Uint8Array; stderr?: Uint8Array }) {
return [outputText(result.stderr), outputText(result.stdout)].filter(Boolean).join("\n")
}
async function candidate(root: string, base?: string) {
for (const attempt of Array.from({ length: 26 }, (_, i) => i)) {
const name = base ? (attempt === 0 ? base : `${base}-${randomName()}`) : randomName()
const branch = `opencode/${name}`
const directory = path.join(root, name)
if (await exists(directory)) continue
const ref = `refs/heads/${branch}`
const branchCheck = await $`git show-ref --verify --quiet ${ref}`.quiet().nothrow().cwd(Instance.worktree)
if (branchCheck.exitCode === 0) continue
return Info.parse({ name, branch, directory })
}
throw new NameGenerationFailedError({ message: "Failed to generate a unique worktree name" })
}
async function runStartCommand(directory: string, cmd: string) {
if (process.platform === "win32") {
return $`cmd /c ${cmd}`.nothrow().cwd(directory)
}
return $`bash -lc ${cmd}`.nothrow().cwd(directory)
}
export const create = fn(CreateInput.optional(), async (input) => {
if (Instance.project.vcs !== "git") {
throw new NotGitError({ message: "Worktrees are only supported for git projects" })
}
const root = path.join(Global.Path.data, "worktree", Instance.project.id)
await fs.mkdir(root, { recursive: true })
const base = input?.name ? slug(input.name) : ""
const info = await candidate(root, base || undefined)
const created = await $`git worktree add -b ${info.branch} ${info.directory}`.nothrow().cwd(Instance.worktree)
if (created.exitCode !== 0) {
throw new CreateFailedError({ message: errorText(created) || "Failed to create git worktree" })
}
await Project.addSandbox(Instance.project.id, info.directory)
const cmd = input?.startCommand?.trim()
if (!cmd) return info
const ran = await runStartCommand(info.directory, cmd)
if (ran.exitCode !== 0) {
throw new StartCommandFailedError({ message: errorText(ran) || "Worktree start command failed" })
}
return info
})
}