summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-09 22:49:36 -0400
committerGitHub <[email protected]>2026-04-09 22:49:36 -0400
commit91786d2fc18c09a4b08846396e4d7f21b03e0c5c (patch)
tree7ee163608b5964ab9b4da9003196dfbe1b6c0456
parenteca11ca71ab34d5818a18754981c97bc03b62bc1 (diff)
downloadopencode-91786d2fc18c09a4b08846396e4d7f21b03e0c5c.tar.gz
opencode-91786d2fc18c09a4b08846396e4d7f21b03e0c5c.zip
refactor(effect): use Git service in file and storage (#21803)
-rw-r--r--packages/opencode/src/file/index.ts197
-rw-r--r--packages/opencode/src/file/watcher.ts11
-rw-r--r--packages/opencode/src/storage/storage.ts21
-rw-r--r--packages/opencode/test/file/watcher.test.ts2
-rw-r--r--packages/opencode/test/storage/storage.test.ts3
5 files changed, 111 insertions, 123 deletions
diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts
index cdcf80a99..47d15fbb0 100644
--- a/packages/opencode/src/file/index.ts
+++ b/packages/opencode/src/file/index.ts
@@ -11,7 +11,6 @@ import path from "path"
import z from "zod"
import { Global } from "../global"
import { Instance } from "../project/instance"
-import { Filesystem } from "../util/filesystem"
import { Log } from "../util/log"
import { Protected } from "./protected"
import { Ripgrep } from "./ripgrep"
@@ -344,6 +343,7 @@ export namespace File {
Service,
Effect.gen(function* () {
const appFs = yield* AppFileSystem.Service
+ const git = yield* Git.Service
const state = yield* InstanceState.make<State>(
Effect.fn("File.state")(() =>
@@ -410,6 +410,10 @@ export namespace File {
cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void)))
})
+ const gitText = Effect.fnUntraced(function* (args: string[]) {
+ return (yield* git.run(args, { cwd: Instance.directory })).text()
+ })
+
const init = Effect.fn("File.init")(function* () {
yield* ensure()
})
@@ -417,100 +421,87 @@ export namespace File {
const status = Effect.fn("File.status")(function* () {
if (Instance.project.vcs !== "git") return []
- return yield* Effect.promise(async () => {
- const diffOutput = (
- await Git.run(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], {
- cwd: Instance.directory,
+ const diffOutput = yield* gitText([
+ "-c",
+ "core.fsmonitor=false",
+ "-c",
+ "core.quotepath=false",
+ "diff",
+ "--numstat",
+ "HEAD",
+ ])
+
+ const changed: File.Info[] = []
+
+ if (diffOutput.trim()) {
+ for (const line of diffOutput.trim().split("\n")) {
+ const [added, removed, file] = line.split("\t")
+ changed.push({
+ path: file,
+ added: added === "-" ? 0 : parseInt(added, 10),
+ removed: removed === "-" ? 0 : parseInt(removed, 10),
+ status: "modified",
})
- ).text()
-
- const changed: File.Info[] = []
-
- if (diffOutput.trim()) {
- for (const line of diffOutput.trim().split("\n")) {
- const [added, removed, file] = line.split("\t")
- changed.push({
- path: file,
- added: added === "-" ? 0 : parseInt(added, 10),
- removed: removed === "-" ? 0 : parseInt(removed, 10),
- status: "modified",
- })
- }
}
+ }
- const untrackedOutput = (
- await Git.run(
- [
- "-c",
- "core.fsmonitor=false",
- "-c",
- "core.quotepath=false",
- "ls-files",
- "--others",
- "--exclude-standard",
- ],
- {
- cwd: Instance.directory,
- },
- )
- ).text()
-
- if (untrackedOutput.trim()) {
- for (const file of untrackedOutput.trim().split("\n")) {
- try {
- const content = await Filesystem.readText(path.join(Instance.directory, file))
- changed.push({
- path: file,
- added: content.split("\n").length,
- removed: 0,
- status: "added",
- })
- } catch {
- continue
- }
- }
+ const untrackedOutput = yield* gitText([
+ "-c",
+ "core.fsmonitor=false",
+ "-c",
+ "core.quotepath=false",
+ "ls-files",
+ "--others",
+ "--exclude-standard",
+ ])
+
+ if (untrackedOutput.trim()) {
+ for (const file of untrackedOutput.trim().split("\n")) {
+ const content = yield* appFs
+ .readFileString(path.join(Instance.directory, file))
+ .pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
+ if (content === undefined) continue
+ changed.push({
+ path: file,
+ added: content.split("\n").length,
+ removed: 0,
+ status: "added",
+ })
}
+ }
- const deletedOutput = (
- await Git.run(
- [
- "-c",
- "core.fsmonitor=false",
- "-c",
- "core.quotepath=false",
- "diff",
- "--name-only",
- "--diff-filter=D",
- "HEAD",
- ],
- {
- cwd: Instance.directory,
- },
- )
- ).text()
-
- if (deletedOutput.trim()) {
- for (const file of deletedOutput.trim().split("\n")) {
- changed.push({
- path: file,
- added: 0,
- removed: 0,
- status: "deleted",
- })
- }
+ const deletedOutput = yield* gitText([
+ "-c",
+ "core.fsmonitor=false",
+ "-c",
+ "core.quotepath=false",
+ "diff",
+ "--name-only",
+ "--diff-filter=D",
+ "HEAD",
+ ])
+
+ if (deletedOutput.trim()) {
+ for (const file of deletedOutput.trim().split("\n")) {
+ changed.push({
+ path: file,
+ added: 0,
+ removed: 0,
+ status: "deleted",
+ })
}
+ }
- return changed.map((item) => {
- const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path)
- return {
- ...item,
- path: path.relative(Instance.directory, full),
- }
- })
+ return changed.map((item) => {
+ const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path)
+ return {
+ ...item,
+ path: path.relative(Instance.directory, full),
+ }
})
})
- const read = Effect.fn("File.read")(function* (file: string) {
+ const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
using _ = log.time("read", { file })
const full = path.join(Instance.directory, file)
@@ -558,27 +549,19 @@ export namespace File {
)
if (Instance.project.vcs === "git") {
- return yield* Effect.promise(async (): Promise<File.Content> => {
- let diff = (
- await Git.run(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })
- ).text()
- if (!diff.trim()) {
- diff = (
- await Git.run(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], {
- cwd: Instance.directory,
- })
- ).text()
- }
- if (diff.trim()) {
- const original = (await Git.run(["show", `HEAD:${file}`], { cwd: Instance.directory })).text()
- const patch = structuredPatch(file, file, original, content, "old", "new", {
- context: Infinity,
- ignoreWhitespace: true,
- })
- return { type: "text", content, patch, diff: formatPatch(patch) }
- }
- return { type: "text", content }
- })
+ let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
+ if (!diff.trim()) {
+ diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
+ }
+ if (diff.trim()) {
+ const original = yield* git.show(Instance.directory, "HEAD", file)
+ const patch = structuredPatch(file, file, original, content, "old", "new", {
+ context: Infinity,
+ ignoreWhitespace: true,
+ })
+ return { type: "text" as const, content, patch, diff: formatPatch(patch) }
+ }
+ return { type: "text" as const, content }
}
return { type: "text" as const, content }
@@ -660,7 +643,7 @@ export namespace File {
}),
)
- export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
+ export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts
index b78b3a33a..dd8b5798c 100644
--- a/packages/opencode/src/file/watcher.ts
+++ b/packages/opencode/src/file/watcher.ts
@@ -71,6 +71,7 @@ export namespace FileWatcher {
Service,
Effect.gen(function* () {
const config = yield* Config.Service
+ const git = yield* Git.Service
const state = yield* InstanceState.make(
Effect.fn("FileWatcher.state")(
@@ -131,11 +132,9 @@ export namespace FileWatcher {
}
if (Instance.project.vcs === "git") {
- const result = yield* Effect.promise(() =>
- Git.run(["rev-parse", "--git-dir"], {
- cwd: Instance.project.worktree,
- }),
- )
+ const result = yield* git.run(["rev-parse", "--git-dir"], {
+ cwd: Instance.project.worktree,
+ })
const vcsDir =
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
@@ -161,7 +160,7 @@ export namespace FileWatcher {
}),
)
- export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
+ export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts
index 0d0dce726..c30089a18 100644
--- a/packages/opencode/src/storage/storage.ts
+++ b/packages/opencode/src/storage/storage.ts
@@ -11,7 +11,11 @@ import { Git } from "@/git"
export namespace Storage {
const log = Log.create({ service: "storage" })
- type Migration = (dir: string, fs: AppFileSystem.Interface) => Effect.Effect<void, AppFileSystem.Error>
+ type Migration = (
+ dir: string,
+ fs: AppFileSystem.Interface,
+ git: Git.Interface,
+ ) => Effect.Effect<void, AppFileSystem.Error>
export const NotFoundError = NamedError.create(
"NotFoundError",
@@ -83,7 +87,7 @@ export namespace Storage {
}
const MIGRATIONS: Migration[] = [
- Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface) {
+ Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface, git: Git.Interface) {
const project = path.resolve(dir, "../project")
if (!(yield* fs.isDir(project))) return
const projectDirs = yield* fs.glob("*", {
@@ -110,11 +114,9 @@ export namespace Storage {
}
if (!worktree) continue
if (!(yield* fs.isDir(worktree))) continue
- const result = yield* Effect.promise(() =>
- Git.run(["rev-list", "--max-parents=0", "--all"], {
- cwd: worktree,
- }),
- )
+ const result = yield* git.run(["rev-list", "--max-parents=0", "--all"], {
+ cwd: worktree,
+ })
const [id] = result
.text()
.split("\n")
@@ -220,6 +222,7 @@ export namespace Storage {
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
+ const git = yield* Git.Service
const locks = yield* RcMap.make({
lookup: () => TxReentrantLock.make(),
idleTimeToLive: 0,
@@ -236,7 +239,7 @@ export namespace Storage {
for (let i = migration; i < MIGRATIONS.length; i++) {
log.info("running migration", { index: i })
const step = MIGRATIONS[i]!
- const exit = yield* Effect.exit(step(dir, fs))
+ const exit = yield* Effect.exit(step(dir, fs, git))
if (Exit.isFailure(exit)) {
log.error("failed to run migration", { index: i, cause: exit.cause })
break
@@ -327,7 +330,7 @@ export namespace Storage {
}),
)
- export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
+ export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts
index 2224a80e6..0c8968d94 100644
--- a/packages/opencode/test/file/watcher.test.ts
+++ b/packages/opencode/test/file/watcher.test.ts
@@ -7,6 +7,7 @@ import { tmpdir } from "../fixture/fixture"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config/config"
import { FileWatcher } from "../../src/file/watcher"
+import { Git } from "../../src/git"
import { Instance } from "../../src/project/instance"
// Native @parcel/watcher bindings aren't reliably available in CI (missing on Linux, flaky on Windows)
@@ -32,6 +33,7 @@ function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
fn: async () => {
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
Layer.provide(Config.defaultLayer),
+ Layer.provide(Git.defaultLayer),
Layer.provide(watcherConfigLayer),
)
const rt = ManagedRuntime.make(layer)
diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts
index e5a04c082..1ff40b4b9 100644
--- a/packages/opencode/test/storage/storage.test.ts
+++ b/packages/opencode/test/storage/storage.test.ts
@@ -3,6 +3,7 @@ import fs from "fs/promises"
import path from "path"
import { Effect, Layer, ManagedRuntime } from "effect"
import { AppFileSystem } from "../../src/filesystem"
+import { Git } from "../../src/git"
import { Global } from "../../src/global"
import { Storage } from "../../src/storage/storage"
import { tmpdir } from "../fixture/fixture"
@@ -47,7 +48,7 @@ async function withStorage<T>(
root: string,
fn: (run: <A, E>(body: Effect.Effect<A, E, Storage.Service>) => Promise<A>) => Promise<T>,
) {
- const rt = ManagedRuntime.make(Storage.layer.pipe(Layer.provide(layer(root))))
+ const rt = ManagedRuntime.make(Storage.layer.pipe(Layer.provide(layer(root)), Layer.provide(Git.defaultLayer)))
try {
return await fn((body) => rt.runPromise(body))
} finally {