Skip to content

Commit 36745ca

Browse files
authored
refactor(worktree): remove async facade exports (anomalyco#22369)
1 parent c2403d0 commit 36745ca

5 files changed

Lines changed: 303 additions & 249 deletions

File tree

packages/opencode/src/control-plane/adaptors/worktree.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import z from "zod"
2+
import { AppRuntime } from "@/effect/app-runtime"
23
import { Worktree } from "@/worktree"
34
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
45

@@ -12,7 +13,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
1213
name: "Worktree",
1314
description: "Create a git worktree",
1415
async configure(info) {
15-
const worktree = await Worktree.makeWorktreeInfo(undefined)
16+
const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.makeWorktreeInfo()))
1617
return {
1718
...info,
1819
name: worktree.name,
@@ -22,15 +23,19 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
2223
},
2324
async create(info) {
2425
const config = WorktreeConfig.parse(info)
25-
await Worktree.createFromInfo({
26-
name: config.name,
27-
directory: config.directory,
28-
branch: config.branch,
29-
})
26+
await AppRuntime.runPromise(
27+
Worktree.Service.use((svc) =>
28+
svc.createFromInfo({
29+
name: config.name,
30+
directory: config.directory,
31+
branch: config.branch,
32+
}),
33+
),
34+
)
3035
},
3136
async remove(info) {
3237
const config = WorktreeConfig.parse(info)
33-
await Worktree.remove({ directory: config.directory })
38+
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })))
3439
},
3540
target(info) {
3641
const config = WorktreeConfig.parse(info)

packages/opencode/src/server/instance/experimental.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ export const ExperimentalRoutes = lazy(() =>
254254
validator("json", Worktree.CreateInput.optional()),
255255
async (c) => {
256256
const body = c.req.valid("json")
257-
const worktree = await Worktree.create(body)
257+
const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.create(body)))
258258
return c.json(worktree)
259259
},
260260
)
@@ -301,7 +301,7 @@ export const ExperimentalRoutes = lazy(() =>
301301
validator("json", Worktree.RemoveInput),
302302
async (c) => {
303303
const body = c.req.valid("json")
304-
await Worktree.remove(body)
304+
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove(body)))
305305
await Project.removeSandbox(Instance.project.id, body.directory)
306306
return c.json(true)
307307
},
@@ -327,7 +327,7 @@ export const ExperimentalRoutes = lazy(() =>
327327
validator("json", Worktree.ResetInput),
328328
async (c) => {
329329
const body = c.req.valid("json")
330-
await Worktree.reset(body)
330+
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.reset(body)))
331331
return c.json(true)
332332
},
333333
)

packages/opencode/src/worktree/index.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
1818
import { NodePath } from "@effect/platform-node"
1919
import { AppFileSystem } from "@/filesystem"
2020
import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
21-
import { makeRuntime } from "@/effect/run-service"
2221
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
2322
import { InstanceState } from "@/effect/instance-state"
2423

@@ -598,25 +597,4 @@ export namespace Worktree {
598597
Layer.provide(AppFileSystem.defaultLayer),
599598
Layer.provide(NodePath.layer),
600599
)
601-
const { runPromise } = makeRuntime(Service, defaultLayer)
602-
603-
export async function makeWorktreeInfo(name?: string) {
604-
return runPromise((svc) => svc.makeWorktreeInfo(name))
605-
}
606-
607-
export async function createFromInfo(info: Info, startCommand?: string) {
608-
return runPromise((svc) => svc.createFromInfo(info, startCommand))
609-
}
610-
611-
export async function create(input?: CreateInput) {
612-
return runPromise((svc) => svc.create(input))
613-
}
614-
615-
export async function remove(input: RemoveInput) {
616-
return runPromise((svc) => svc.remove(input))
617-
}
618-
619-
export async function reset(input: ResetInput) {
620-
return runPromise((svc) => svc.reset(input))
621-
}
622600
}
Lines changed: 119 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,126 @@
1-
import { describe, expect, test } from "bun:test"
21
import { $ } from "bun"
3-
import fs from "fs/promises"
2+
import { describe, expect } from "bun:test"
3+
import * as fs from "fs/promises"
44
import path from "path"
5-
import { Instance } from "../../src/project/instance"
5+
import { Effect, Layer } from "effect"
6+
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
67
import { Worktree } from "../../src/worktree"
7-
import { Filesystem } from "../../src/util/filesystem"
8-
import { tmpdir } from "../fixture/fixture"
8+
import { provideTmpdirInstance } from "../fixture/fixture"
9+
import { testEffect } from "../lib/effect"
910

10-
const wintest = process.platform === "win32" ? test : test.skip
11+
const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
12+
const wintest = process.platform === "win32" ? it.live : it.live.skip
1113

1214
describe("Worktree.remove", () => {
13-
test("continues when git remove exits non-zero after detaching", async () => {
14-
await using tmp = await tmpdir({ git: true })
15-
const root = tmp.path
16-
const name = `remove-regression-${Date.now().toString(36)}`
17-
const branch = `opencode/${name}`
18-
const dir = path.join(root, "..", name)
19-
20-
await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet()
21-
await $`git reset --hard`.cwd(dir).quiet()
22-
23-
const real = (await $`which git`.quiet().text()).trim()
24-
expect(real).toBeTruthy()
25-
26-
const bin = path.join(root, "bin")
27-
const shim = path.join(bin, "git")
28-
await fs.mkdir(bin, { recursive: true })
29-
await Bun.write(
30-
shim,
31-
[
32-
"#!/bin/bash",
33-
`REAL_GIT=${JSON.stringify(real)}`,
34-
'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
35-
' "$REAL_GIT" "$@" >/dev/null 2>&1',
36-
' echo "fatal: failed to remove worktree: Directory not empty" >&2',
37-
" exit 1",
38-
"fi",
39-
'exec "$REAL_GIT" "$@"',
40-
].join("\n"),
41-
)
42-
await fs.chmod(shim, 0o755)
43-
44-
const prev = process.env.PATH ?? ""
45-
process.env.PATH = `${bin}${path.delimiter}${prev}`
46-
47-
const ok = await (async () => {
48-
try {
49-
return await Instance.provide({
50-
directory: root,
51-
fn: () => Worktree.remove({ directory: dir }),
52-
})
53-
} finally {
54-
process.env.PATH = prev
55-
}
56-
})()
57-
58-
expect(ok).toBe(true)
59-
expect(await Filesystem.exists(dir)).toBe(false)
60-
61-
const list = await $`git worktree list --porcelain`.cwd(root).quiet().text()
62-
expect(list).not.toContain(`worktree ${dir}`)
63-
64-
const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
65-
expect(ref.exitCode).not.toBe(0)
66-
})
67-
68-
wintest("stops fsmonitor before removing a worktree", async () => {
69-
await using tmp = await tmpdir({ git: true })
70-
const root = tmp.path
71-
const name = `remove-fsmonitor-${Date.now().toString(36)}`
72-
const branch = `opencode/${name}`
73-
const dir = path.join(root, "..", name)
74-
75-
await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet()
76-
await $`git reset --hard`.cwd(dir).quiet()
77-
await $`git config core.fsmonitor true`.cwd(dir).quiet()
78-
await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
79-
await Bun.write(path.join(dir, "tracked.txt"), "next\n")
80-
await $`git diff`.cwd(dir).quiet()
81-
82-
const before = await $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow()
83-
expect(before.exitCode).toBe(0)
84-
85-
const ok = await Instance.provide({
86-
directory: root,
87-
fn: () => Worktree.remove({ directory: dir }),
88-
})
89-
90-
expect(ok).toBe(true)
91-
expect(await Filesystem.exists(dir)).toBe(false)
92-
93-
const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
94-
expect(ref.exitCode).not.toBe(0)
95-
})
15+
it.live("continues when git remove exits non-zero after detaching", () =>
16+
provideTmpdirInstance(
17+
(root) =>
18+
Effect.gen(function* () {
19+
const svc = yield* Worktree.Service
20+
const name = `remove-regression-${Date.now().toString(36)}`
21+
const branch = `opencode/${name}`
22+
const dir = path.join(root, "..", name)
23+
24+
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
25+
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
26+
27+
const real = (yield* Effect.promise(() => $`which git`.quiet().text())).trim()
28+
expect(real).toBeTruthy()
29+
30+
const bin = path.join(root, "bin")
31+
const shim = path.join(bin, "git")
32+
yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
33+
yield* Effect.promise(() =>
34+
Bun.write(
35+
shim,
36+
[
37+
"#!/bin/bash",
38+
`REAL_GIT=${JSON.stringify(real)}`,
39+
'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
40+
' "$REAL_GIT" "$@" >/dev/null 2>&1',
41+
' echo "fatal: failed to remove worktree: Directory not empty" >&2',
42+
" exit 1",
43+
"fi",
44+
'exec "$REAL_GIT" "$@"',
45+
].join("\n"),
46+
),
47+
)
48+
yield* Effect.promise(() => fs.chmod(shim, 0o755))
49+
50+
const prev = yield* Effect.acquireRelease(
51+
Effect.sync(() => {
52+
const prev = process.env.PATH ?? ""
53+
process.env.PATH = `${bin}${path.delimiter}${prev}`
54+
return prev
55+
}),
56+
(prev) =>
57+
Effect.sync(() => {
58+
process.env.PATH = prev
59+
}),
60+
)
61+
void prev
62+
63+
const ok = yield* svc.remove({ directory: dir })
64+
65+
expect(ok).toBe(true)
66+
expect(
67+
yield* Effect.promise(() =>
68+
fs
69+
.stat(dir)
70+
.then(() => true)
71+
.catch(() => false),
72+
),
73+
).toBe(false)
74+
75+
const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(root).quiet().text())
76+
expect(list).not.toContain(`worktree ${dir}`)
77+
78+
const ref = yield* Effect.promise(() =>
79+
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
80+
)
81+
expect(ref.exitCode).not.toBe(0)
82+
}),
83+
{ git: true },
84+
),
85+
)
86+
87+
wintest("stops fsmonitor before removing a worktree", () =>
88+
provideTmpdirInstance(
89+
(root) =>
90+
Effect.gen(function* () {
91+
const svc = yield* Worktree.Service
92+
const name = `remove-fsmonitor-${Date.now().toString(36)}`
93+
const branch = `opencode/${name}`
94+
const dir = path.join(root, "..", name)
95+
96+
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
97+
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
98+
yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
99+
yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
100+
yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
101+
yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
102+
103+
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
104+
expect(before.exitCode).toBe(0)
105+
106+
const ok = yield* svc.remove({ directory: dir })
107+
108+
expect(ok).toBe(true)
109+
expect(
110+
yield* Effect.promise(() =>
111+
fs
112+
.stat(dir)
113+
.then(() => true)
114+
.catch(() => false),
115+
),
116+
).toBe(false)
117+
118+
const ref = yield* Effect.promise(() =>
119+
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
120+
)
121+
expect(ref.exitCode).not.toBe(0)
122+
}),
123+
{ git: true },
124+
),
125+
)
96126
})

0 commit comments

Comments
 (0)