mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 05:10:58 +08:00
refactor(worktree): remove async facade exports (#22369)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import z from "zod"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
|
||||
|
||||
@@ -12,7 +13,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
name: "Worktree",
|
||||
description: "Create a git worktree",
|
||||
async configure(info) {
|
||||
const worktree = await Worktree.makeWorktreeInfo(undefined)
|
||||
const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.makeWorktreeInfo()))
|
||||
return {
|
||||
...info,
|
||||
name: worktree.name,
|
||||
@@ -22,15 +23,19 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
},
|
||||
async create(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
await Worktree.createFromInfo({
|
||||
name: config.name,
|
||||
directory: config.directory,
|
||||
branch: config.branch,
|
||||
})
|
||||
await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) =>
|
||||
svc.createFromInfo({
|
||||
name: config.name,
|
||||
directory: config.directory,
|
||||
branch: config.branch,
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
async remove(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
await Worktree.remove({ directory: config.directory })
|
||||
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })))
|
||||
},
|
||||
target(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
|
||||
@@ -254,7 +254,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
validator("json", Worktree.CreateInput.optional()),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const worktree = await Worktree.create(body)
|
||||
const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.create(body)))
|
||||
return c.json(worktree)
|
||||
},
|
||||
)
|
||||
@@ -301,7 +301,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
validator("json", Worktree.RemoveInput),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Worktree.remove(body)
|
||||
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove(body)))
|
||||
await Project.removeSandbox(Instance.project.id, body.directory)
|
||||
return c.json(true)
|
||||
},
|
||||
@@ -327,7 +327,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
validator("json", Worktree.ResetInput),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Worktree.reset(body)
|
||||
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.reset(body)))
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
|
||||
@@ -598,25 +597,4 @@ export namespace Worktree {
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(NodePath.layer),
|
||||
)
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function makeWorktreeInfo(name?: string) {
|
||||
return runPromise((svc) => svc.makeWorktreeInfo(name))
|
||||
}
|
||||
|
||||
export async function createFromInfo(info: Info, startCommand?: string) {
|
||||
return runPromise((svc) => svc.createFromInfo(info, startCommand))
|
||||
}
|
||||
|
||||
export async function create(input?: CreateInput) {
|
||||
return runPromise((svc) => svc.create(input))
|
||||
}
|
||||
|
||||
export async function remove(input: RemoveInput) {
|
||||
return runPromise((svc) => svc.remove(input))
|
||||
}
|
||||
|
||||
export async function reset(input: ResetInput) {
|
||||
return runPromise((svc) => svc.reset(input))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,126 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import { describe, expect } from "bun:test"
|
||||
import * as fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const wintest = process.platform === "win32" ? test : test.skip
|
||||
const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
const wintest = process.platform === "win32" ? it.live : it.live.skip
|
||||
|
||||
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)
|
||||
it.live("continues when git remove exits non-zero after detaching", () =>
|
||||
provideTmpdirInstance(
|
||||
(root) =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
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()
|
||||
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
|
||||
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
|
||||
|
||||
const real = (await $`which git`.quiet().text()).trim()
|
||||
expect(real).toBeTruthy()
|
||||
const real = (yield* Effect.promise(() => $`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 bin = path.join(root, "bin")
|
||||
const shim = path.join(bin, "git")
|
||||
yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
|
||||
yield* Effect.promise(() =>
|
||||
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"),
|
||||
),
|
||||
)
|
||||
yield* Effect.promise(() => fs.chmod(shim, 0o755))
|
||||
|
||||
const prev = process.env.PATH ?? ""
|
||||
process.env.PATH = `${bin}${path.delimiter}${prev}`
|
||||
const prev = yield* Effect.acquireRelease(
|
||||
Effect.sync(() => {
|
||||
const prev = process.env.PATH ?? ""
|
||||
process.env.PATH = `${bin}${path.delimiter}${prev}`
|
||||
return prev
|
||||
}),
|
||||
(prev) =>
|
||||
Effect.sync(() => {
|
||||
process.env.PATH = prev
|
||||
}),
|
||||
)
|
||||
void prev
|
||||
|
||||
const ok = await (async () => {
|
||||
try {
|
||||
return await Instance.provide({
|
||||
directory: root,
|
||||
fn: () => Worktree.remove({ directory: dir }),
|
||||
})
|
||||
} finally {
|
||||
process.env.PATH = prev
|
||||
}
|
||||
})()
|
||||
const ok = yield* svc.remove({ directory: dir })
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(await Filesystem.exists(dir)).toBe(false)
|
||||
expect(ok).toBe(true)
|
||||
expect(
|
||||
yield* Effect.promise(() =>
|
||||
fs
|
||||
.stat(dir)
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
),
|
||||
).toBe(false)
|
||||
|
||||
const list = await $`git worktree list --porcelain`.cwd(root).quiet().text()
|
||||
expect(list).not.toContain(`worktree ${dir}`)
|
||||
const list = yield* Effect.promise(() => $`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)
|
||||
})
|
||||
const ref = yield* Effect.promise(() =>
|
||||
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
|
||||
)
|
||||
expect(ref.exitCode).not.toBe(0)
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
wintest("stops fsmonitor before removing a worktree", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const root = tmp.path
|
||||
const name = `remove-fsmonitor-${Date.now().toString(36)}`
|
||||
const branch = `opencode/${name}`
|
||||
const dir = path.join(root, "..", name)
|
||||
wintest("stops fsmonitor before removing a worktree", () =>
|
||||
provideTmpdirInstance(
|
||||
(root) =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const name = `remove-fsmonitor-${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()
|
||||
await $`git config core.fsmonitor true`.cwd(dir).quiet()
|
||||
await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
|
||||
await Bun.write(path.join(dir, "tracked.txt"), "next\n")
|
||||
await $`git diff`.cwd(dir).quiet()
|
||||
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
|
||||
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
|
||||
yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
|
||||
yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
|
||||
yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
|
||||
yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
|
||||
|
||||
const before = await $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow()
|
||||
expect(before.exitCode).toBe(0)
|
||||
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
|
||||
expect(before.exitCode).toBe(0)
|
||||
|
||||
const ok = await Instance.provide({
|
||||
directory: root,
|
||||
fn: () => Worktree.remove({ directory: dir }),
|
||||
})
|
||||
const ok = yield* svc.remove({ directory: dir })
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(await Filesystem.exists(dir)).toBe(false)
|
||||
expect(ok).toBe(true)
|
||||
expect(
|
||||
yield* Effect.promise(() =>
|
||||
fs
|
||||
.stat(dir)
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
),
|
||||
).toBe(false)
|
||||
|
||||
const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
|
||||
expect(ref.exitCode).not.toBe(0)
|
||||
})
|
||||
const ref = yield* Effect.promise(() =>
|
||||
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
|
||||
)
|
||||
expect(ref.exitCode).not.toBe(0)
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { $ } from "bun"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
|
||||
const wintest = process.platform !== "win32" ? test : test.skip
|
||||
import fs from "fs/promises"
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import * as fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { provideInstance, provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
function withInstance(directory: string, fn: () => Promise<any>) {
|
||||
return Instance.provide({ directory, fn })
|
||||
}
|
||||
const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
const wintest = process.platform !== "win32" ? it.live : it.live.skip
|
||||
|
||||
function normalize(input: string) {
|
||||
return input.replace(/\\/g, "/").toLowerCase()
|
||||
@@ -40,134 +40,175 @@ describe("Worktree", () => {
|
||||
afterEach(() => Instance.disposeAll())
|
||||
|
||||
describe("makeWorktreeInfo", () => {
|
||||
test("returns info with name, branch, and directory", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
it.live("returns info with name, branch, and directory", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const info = yield* svc.makeWorktreeInfo()
|
||||
|
||||
const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo())
|
||||
expect(info.name).toBeDefined()
|
||||
expect(typeof info.name).toBe("string")
|
||||
expect(info.branch).toBe(`opencode/${info.name}`)
|
||||
expect(info.directory).toContain(info.name)
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
expect(info.name).toBeDefined()
|
||||
expect(typeof info.name).toBe("string")
|
||||
expect(info.branch).toBe(`opencode/${info.name}`)
|
||||
expect(info.directory).toContain(info.name)
|
||||
})
|
||||
it.live("uses provided name as base", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const info = yield* svc.makeWorktreeInfo("my-feature")
|
||||
|
||||
test("uses provided name as base", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
expect(info.name).toBe("my-feature")
|
||||
expect(info.branch).toBe("opencode/my-feature")
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo("my-feature"))
|
||||
it.live("slugifies the provided name", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const info = yield* svc.makeWorktreeInfo("My Feature Branch!")
|
||||
|
||||
expect(info.name).toBe("my-feature")
|
||||
expect(info.branch).toBe("opencode/my-feature")
|
||||
})
|
||||
expect(info.name).toBe("my-feature-branch")
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
test("slugifies the provided name", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
it.live("throws NotGitError for non-git directories", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const exit = yield* Effect.exit(svc.makeWorktreeInfo())
|
||||
|
||||
const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo("My Feature Branch!"))
|
||||
|
||||
expect(info.name).toBe("my-feature-branch")
|
||||
})
|
||||
|
||||
test("throws NotGitError for non-git directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await expect(withInstance(tmp.path, () => Worktree.makeWorktreeInfo())).rejects.toThrow("WorktreeNotGitError")
|
||||
})
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
describe("create + remove lifecycle", () => {
|
||||
test("create returns worktree info and remove cleans up", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
it.live("create returns worktree info and remove cleans up", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const info = yield* svc.create()
|
||||
|
||||
const info = await withInstance(tmp.path, () => Worktree.create())
|
||||
expect(info.name).toBeDefined()
|
||||
expect(info.branch).toStartWith("opencode/")
|
||||
expect(info.directory).toBeDefined()
|
||||
|
||||
expect(info.name).toBeDefined()
|
||||
expect(info.branch).toStartWith("opencode/")
|
||||
expect(info.directory).toBeDefined()
|
||||
yield* Effect.promise(() => Bun.sleep(1000))
|
||||
|
||||
// Wait for bootstrap to complete
|
||||
await Bun.sleep(1000)
|
||||
const ok = yield* svc.remove({ directory: info.directory })
|
||||
expect(ok).toBe(true)
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
const ok = await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
|
||||
expect(ok).toBe(true)
|
||||
})
|
||||
it.live("create returns after setup and fires Event.Ready after bootstrap", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const ready = waitReady()
|
||||
const info = yield* svc.create()
|
||||
|
||||
test("create returns after setup and fires Event.Ready after bootstrap", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const ready = waitReady()
|
||||
expect(info.name).toBeDefined()
|
||||
expect(info.branch).toStartWith("opencode/")
|
||||
|
||||
const info = await withInstance(tmp.path, () => Worktree.create())
|
||||
const text = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(dir).quiet().text())
|
||||
const next = yield* Effect.promise(() => fs.realpath(info.directory).catch(() => info.directory))
|
||||
expect(normalize(text)).toContain(normalize(next))
|
||||
|
||||
// create returns before bootstrap completes, but the worktree already exists
|
||||
expect(info.name).toBeDefined()
|
||||
expect(info.branch).toStartWith("opencode/")
|
||||
const props = yield* Effect.promise(() => ready)
|
||||
expect(props.name).toBe(info.name)
|
||||
expect(props.branch).toBe(info.branch)
|
||||
|
||||
const text = await $`git worktree list --porcelain`.cwd(tmp.path).quiet().text()
|
||||
const dir = await fs.realpath(info.directory).catch(() => info.directory)
|
||||
expect(normalize(text)).toContain(normalize(dir))
|
||||
yield* Effect.promise(() => Instance.dispose()).pipe(provideInstance(info.directory))
|
||||
yield* Effect.promise(() => Bun.sleep(100))
|
||||
yield* svc.remove({ directory: info.directory })
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
// Event.Ready fires after bootstrap finishes in the background
|
||||
const props = await ready
|
||||
expect(props.name).toBe(info.name)
|
||||
expect(props.branch).toBe(info.branch)
|
||||
it.live("create with custom name", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const ready = waitReady()
|
||||
const info = yield* svc.create({ name: "test-workspace" })
|
||||
|
||||
// Cleanup
|
||||
await withInstance(info.directory, () => Instance.dispose())
|
||||
await Bun.sleep(100)
|
||||
await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
|
||||
})
|
||||
expect(info.name).toBe("test-workspace")
|
||||
expect(info.branch).toBe("opencode/test-workspace")
|
||||
|
||||
test("create with custom name", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const ready = waitReady()
|
||||
|
||||
const info = await withInstance(tmp.path, () => Worktree.create({ name: "test-workspace" }))
|
||||
|
||||
expect(info.name).toBe("test-workspace")
|
||||
expect(info.branch).toBe("opencode/test-workspace")
|
||||
|
||||
// Cleanup
|
||||
await ready
|
||||
await withInstance(info.directory, () => Instance.dispose())
|
||||
await Bun.sleep(100)
|
||||
await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
|
||||
})
|
||||
yield* Effect.promise(() => ready)
|
||||
yield* Effect.promise(() => Instance.dispose()).pipe(provideInstance(info.directory))
|
||||
yield* Effect.promise(() => Bun.sleep(100))
|
||||
yield* svc.remove({ directory: info.directory })
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
describe("createFromInfo", () => {
|
||||
wintest("creates and bootstraps git worktree", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
wintest("creates and bootstraps git worktree", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const info = yield* svc.makeWorktreeInfo("from-info-test")
|
||||
yield* svc.createFromInfo(info)
|
||||
|
||||
const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo("from-info-test"))
|
||||
await withInstance(tmp.path, () => Worktree.createFromInfo(info))
|
||||
const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(dir).quiet().text())
|
||||
const normalizedList = list.replace(/\\/g, "/")
|
||||
const normalizedDir = info.directory.replace(/\\/g, "/")
|
||||
expect(normalizedList).toContain(normalizedDir)
|
||||
|
||||
// Worktree should exist in git (normalize slashes for Windows)
|
||||
const list = await $`git worktree list --porcelain`.cwd(tmp.path).quiet().text()
|
||||
const normalizedList = list.replace(/\\/g, "/")
|
||||
const normalizedDir = info.directory.replace(/\\/g, "/")
|
||||
expect(normalizedList).toContain(normalizedDir)
|
||||
|
||||
// Cleanup
|
||||
await withInstance(tmp.path, () => Worktree.remove({ directory: info.directory }))
|
||||
})
|
||||
yield* svc.remove({ directory: info.directory })
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
describe("remove edge cases", () => {
|
||||
test("remove non-existent directory succeeds silently", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
it.live("remove non-existent directory succeeds silently", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const ok = yield* svc.remove({ directory: path.join(dir, "does-not-exist") })
|
||||
expect(ok).toBe(true)
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
const ok = await withInstance(tmp.path, () =>
|
||||
Worktree.remove({ directory: path.join(tmp.path, "does-not-exist") }),
|
||||
)
|
||||
expect(ok).toBe(true)
|
||||
})
|
||||
it.live("throws NotGitError for non-git directories", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Worktree.Service
|
||||
const exit = yield* Effect.exit(svc.remove({ directory: "/tmp/fake" }))
|
||||
|
||||
test("throws NotGitError for non-git directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await expect(withInstance(tmp.path, () => Worktree.remove({ directory: "/tmp/fake" }))).rejects.toThrow(
|
||||
"WorktreeNotGitError",
|
||||
)
|
||||
})
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user