diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index cdcf80a99e50..47d15fbb0019 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( 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(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 => { - 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 b78b3a33a086..dd8b5798ca92 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 0d0dce7264cf..c30089a18c9a 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 + type Migration = ( + dir: string, + fs: AppFileSystem.Interface, + git: Git.Interface, + ) => Effect.Effect 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 2224a80e680e..0c8968d94b05 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(directory: string, body: Effect.Effect) { fn: async () => { const layer: Layer.Layer = 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 e5a04c082dc5..1ff40b4b99ef 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( root: string, fn: (run: (body: Effect.Effect) => Promise) => Promise, ) { - 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 {