Просмотр исходного кода

refactor(effect): use Git service in file and storage (#21803)

Kit Langton 4 месяцев назад
Родитель
Сommit
91786d2fc1

+ 90 - 107
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)
 

+ 5 - 6
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)
 

+ 12 - 9
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)
 

+ 2 - 0
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)

+ 2 - 1
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 {