mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
fix(app): worktree delete
This commit is contained in:
parent
0303c29e3f
commit
8da5fd0a66
@ -420,49 +420,78 @@ export namespace Worktree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directory = await canonical(input.directory)
|
const directory = await canonical(input.directory)
|
||||||
|
const locate = async (stdout: Uint8Array | undefined) => {
|
||||||
|
const lines = outputText(stdout)
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
|
||||||
|
if (!line) return acc
|
||||||
|
if (line.startsWith("worktree ")) {
|
||||||
|
acc.push({ path: line.slice("worktree ".length).trim() })
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
const current = acc[acc.length - 1]
|
||||||
|
if (!current) return acc
|
||||||
|
if (line.startsWith("branch ")) {
|
||||||
|
current.branch = line.slice("branch ".length).trim()
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (async () => {
|
||||||
|
for (const item of entries) {
|
||||||
|
if (!item.path) continue
|
||||||
|
const key = await canonical(item.path)
|
||||||
|
if (key === directory) return item
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clean = (target: string) =>
|
||||||
|
fs
|
||||||
|
.rm(target, {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
maxRetries: 5,
|
||||||
|
retryDelay: 100,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
|
||||||
|
})
|
||||||
|
|
||||||
const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
|
const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
|
||||||
if (list.exitCode !== 0) {
|
if (list.exitCode !== 0) {
|
||||||
throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" })
|
throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = outputText(list.stdout)
|
const entry = await locate(list.stdout)
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.trim())
|
|
||||||
const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
|
|
||||||
if (!line) return acc
|
|
||||||
if (line.startsWith("worktree ")) {
|
|
||||||
acc.push({ path: line.slice("worktree ".length).trim() })
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
const current = acc[acc.length - 1]
|
|
||||||
if (!current) return acc
|
|
||||||
if (line.startsWith("branch ")) {
|
|
||||||
current.branch = line.slice("branch ".length).trim()
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const entry = await (async () => {
|
|
||||||
for (const item of entries) {
|
|
||||||
if (!item.path) continue
|
|
||||||
const key = await canonical(item.path)
|
|
||||||
if (key === directory) return item
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (!entry?.path) {
|
if (!entry?.path) {
|
||||||
const directoryExists = await exists(directory)
|
const directoryExists = await exists(directory)
|
||||||
if (directoryExists) {
|
if (directoryExists) {
|
||||||
await fs.rm(directory, { recursive: true, force: true })
|
await clean(directory)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree)
|
const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree)
|
||||||
if (removed.exitCode !== 0) {
|
if (removed.exitCode !== 0) {
|
||||||
throw new RemoveFailedError({ message: errorText(removed) || "Failed to remove git worktree" })
|
const next = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
|
||||||
|
if (next.exitCode !== 0) {
|
||||||
|
throw new RemoveFailedError({
|
||||||
|
message: errorText(removed) || errorText(next) || "Failed to remove git worktree",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const stale = await locate(next.stdout)
|
||||||
|
if (stale?.path) {
|
||||||
|
throw new RemoveFailedError({ message: errorText(removed) || "Failed to remove git worktree" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await clean(entry.path)
|
||||||
|
|
||||||
const branch = entry.branch?.replace(/^refs\/heads\//, "")
|
const branch = entry.branch?.replace(/^refs\/heads\//, "")
|
||||||
if (branch) {
|
if (branch) {
|
||||||
const deleted = await $`git branch -D ${branch}`.quiet().nothrow().cwd(Instance.worktree)
|
const deleted = await $`git branch -D ${branch}`.quiet().nothrow().cwd(Instance.worktree)
|
||||||
|
|||||||
64
packages/opencode/test/project/worktree-remove.test.ts
Normal file
64
packages/opencode/test/project/worktree-remove.test.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { $ } from "bun"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import path from "path"
|
||||||
|
import { Instance } from "../../src/project/instance"
|
||||||
|
import { Worktree } from "../../src/worktree"
|
||||||
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
|
describe("Worktree.remove", () => {
|
||||||
|
test("continues when git remove exits non-zero after detaching", async () => {
|
||||||
|
await using tmp = await tmpdir({ git: true })
|
||||||
|
const root = tmp.path
|
||||||
|
const name = `remove-regression-${Date.now().toString(36)}`
|
||||||
|
const branch = `opencode/${name}`
|
||||||
|
const dir = path.join(root, "..", name)
|
||||||
|
|
||||||
|
await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet()
|
||||||
|
await $`git reset --hard`.cwd(dir).quiet()
|
||||||
|
|
||||||
|
const real = (await $`which git`.quiet().text()).trim()
|
||||||
|
expect(real).toBeTruthy()
|
||||||
|
|
||||||
|
const bin = path.join(root, "bin")
|
||||||
|
const shim = path.join(bin, "git")
|
||||||
|
await fs.mkdir(bin, { recursive: true })
|
||||||
|
await Bun.write(
|
||||||
|
shim,
|
||||||
|
[
|
||||||
|
"#!/bin/bash",
|
||||||
|
`REAL_GIT=${JSON.stringify(real)}`,
|
||||||
|
'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
|
||||||
|
' "$REAL_GIT" "$@" >/dev/null 2>&1',
|
||||||
|
' echo "fatal: failed to remove worktree: Directory not empty" >&2',
|
||||||
|
" exit 1",
|
||||||
|
"fi",
|
||||||
|
'exec "$REAL_GIT" "$@"',
|
||||||
|
].join("\n"),
|
||||||
|
)
|
||||||
|
await fs.chmod(shim, 0o755)
|
||||||
|
|
||||||
|
const prev = process.env.PATH ?? ""
|
||||||
|
process.env.PATH = `${bin}${path.delimiter}${prev}`
|
||||||
|
|
||||||
|
const ok = await (async () => {
|
||||||
|
try {
|
||||||
|
return await Instance.provide({
|
||||||
|
directory: root,
|
||||||
|
fn: () => Worktree.remove({ directory: dir }),
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
process.env.PATH = prev
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
expect(ok).toBe(true)
|
||||||
|
expect(await Bun.file(dir).exists()).toBe(false)
|
||||||
|
|
||||||
|
const list = await $`git worktree list --porcelain`.cwd(root).quiet().text()
|
||||||
|
expect(list).not.toContain(`worktree ${dir}`)
|
||||||
|
|
||||||
|
const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
|
||||||
|
expect(ref.exitCode).not.toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user