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

This commit is contained in:
Kit Langton
2026-04-09 22:49:36 -04:00
committed by GitHub
parent eca11ca71a
commit 91786d2fc1
5 changed files with 109 additions and 121 deletions
+29 -46
View File
@@ -11,7 +11,6 @@ import path from "path"
import z from "zod" import z from "zod"
import { Global } from "../global" import { Global } from "../global"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Protected } from "./protected" import { Protected } from "./protected"
import { Ripgrep } from "./ripgrep" import { Ripgrep } from "./ripgrep"
@@ -344,6 +343,7 @@ export namespace File {
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const appFs = yield* AppFileSystem.Service const appFs = yield* AppFileSystem.Service
const git = yield* Git.Service
const state = yield* InstanceState.make<State>( const state = yield* InstanceState.make<State>(
Effect.fn("File.state")(() => Effect.fn("File.state")(() =>
@@ -410,6 +410,10 @@ export namespace File {
cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void))) 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* () { const init = Effect.fn("File.init")(function* () {
yield* ensure() yield* ensure()
}) })
@@ -417,12 +421,15 @@ export namespace File {
const status = Effect.fn("File.status")(function* () { const status = Effect.fn("File.status")(function* () {
if (Instance.project.vcs !== "git") return [] if (Instance.project.vcs !== "git") return []
return yield* Effect.promise(async () => { const diffOutput = yield* gitText([
const diffOutput = ( "-c",
await Git.run(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], { "core.fsmonitor=false",
cwd: Instance.directory, "-c",
}) "core.quotepath=false",
).text() "diff",
"--numstat",
"HEAD",
])
const changed: File.Info[] = [] const changed: File.Info[] = []
@@ -438,9 +445,7 @@ export namespace File {
} }
} }
const untrackedOutput = ( const untrackedOutput = yield* gitText([
await Git.run(
[
"-c", "-c",
"core.fsmonitor=false", "core.fsmonitor=false",
"-c", "-c",
@@ -448,32 +453,24 @@ export namespace File {
"ls-files", "ls-files",
"--others", "--others",
"--exclude-standard", "--exclude-standard",
], ])
{
cwd: Instance.directory,
},
)
).text()
if (untrackedOutput.trim()) { if (untrackedOutput.trim()) {
for (const file of untrackedOutput.trim().split("\n")) { for (const file of untrackedOutput.trim().split("\n")) {
try { const content = yield* appFs
const content = await Filesystem.readText(path.join(Instance.directory, file)) .readFileString(path.join(Instance.directory, file))
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
if (content === undefined) continue
changed.push({ changed.push({
path: file, path: file,
added: content.split("\n").length, added: content.split("\n").length,
removed: 0, removed: 0,
status: "added", status: "added",
}) })
} catch {
continue
}
} }
} }
const deletedOutput = ( const deletedOutput = yield* gitText([
await Git.run(
[
"-c", "-c",
"core.fsmonitor=false", "core.fsmonitor=false",
"-c", "-c",
@@ -482,12 +479,7 @@ export namespace File {
"--name-only", "--name-only",
"--diff-filter=D", "--diff-filter=D",
"HEAD", "HEAD",
], ])
{
cwd: Instance.directory,
},
)
).text()
if (deletedOutput.trim()) { if (deletedOutput.trim()) {
for (const file of deletedOutput.trim().split("\n")) { for (const file of deletedOutput.trim().split("\n")) {
@@ -508,9 +500,8 @@ export namespace File {
} }
}) })
}) })
})
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 }) using _ = log.time("read", { file })
const full = path.join(Instance.directory, file) const full = path.join(Instance.directory, file)
@@ -558,27 +549,19 @@ export namespace File {
) )
if (Instance.project.vcs === "git") { if (Instance.project.vcs === "git") {
return yield* Effect.promise(async (): Promise<File.Content> => { let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
let diff = (
await Git.run(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })
).text()
if (!diff.trim()) { if (!diff.trim()) {
diff = ( diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
await Git.run(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], {
cwd: Instance.directory,
})
).text()
} }
if (diff.trim()) { if (diff.trim()) {
const original = (await Git.run(["show", `HEAD:${file}`], { cwd: Instance.directory })).text() const original = yield* git.show(Instance.directory, "HEAD", file)
const patch = structuredPatch(file, file, original, content, "old", "new", { const patch = structuredPatch(file, file, original, content, "old", "new", {
context: Infinity, context: Infinity,
ignoreWhitespace: true, ignoreWhitespace: true,
}) })
return { type: "text", content, patch, diff: formatPatch(patch) } return { type: "text" as const, content, patch, diff: formatPatch(patch) }
} }
return { type: "text", content } return { type: "text" as const, content }
})
} }
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) const { runPromise } = makeRuntime(Service, defaultLayer)
+4 -5
View File
@@ -71,6 +71,7 @@ export namespace FileWatcher {
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service const config = yield* Config.Service
const git = yield* Git.Service
const state = yield* InstanceState.make( const state = yield* InstanceState.make(
Effect.fn("FileWatcher.state")( Effect.fn("FileWatcher.state")(
@@ -131,11 +132,9 @@ export namespace FileWatcher {
} }
if (Instance.project.vcs === "git") { if (Instance.project.vcs === "git") {
const result = yield* Effect.promise(() => const result = yield* git.run(["rev-parse", "--git-dir"], {
Git.run(["rev-parse", "--git-dir"], {
cwd: Instance.project.worktree, cwd: Instance.project.worktree,
}), })
)
const vcsDir = const vcsDir =
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) { 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) const { runPromise } = makeRuntime(Service, defaultLayer)
+11 -8
View File
@@ -11,7 +11,11 @@ import { Git } from "@/git"
export namespace Storage { export namespace Storage {
const log = Log.create({ service: "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( export const NotFoundError = NamedError.create(
"NotFoundError", "NotFoundError",
@@ -83,7 +87,7 @@ export namespace Storage {
} }
const MIGRATIONS: Migration[] = [ 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") const project = path.resolve(dir, "../project")
if (!(yield* fs.isDir(project))) return if (!(yield* fs.isDir(project))) return
const projectDirs = yield* fs.glob("*", { const projectDirs = yield* fs.glob("*", {
@@ -110,11 +114,9 @@ export namespace Storage {
} }
if (!worktree) continue if (!worktree) continue
if (!(yield* fs.isDir(worktree))) continue if (!(yield* fs.isDir(worktree))) continue
const result = yield* Effect.promise(() => const result = yield* git.run(["rev-list", "--max-parents=0", "--all"], {
Git.run(["rev-list", "--max-parents=0", "--all"], {
cwd: worktree, cwd: worktree,
}), })
)
const [id] = result const [id] = result
.text() .text()
.split("\n") .split("\n")
@@ -220,6 +222,7 @@ export namespace Storage {
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* AppFileSystem.Service const fs = yield* AppFileSystem.Service
const git = yield* Git.Service
const locks = yield* RcMap.make({ const locks = yield* RcMap.make({
lookup: () => TxReentrantLock.make(), lookup: () => TxReentrantLock.make(),
idleTimeToLive: 0, idleTimeToLive: 0,
@@ -236,7 +239,7 @@ export namespace Storage {
for (let i = migration; i < MIGRATIONS.length; i++) { for (let i = migration; i < MIGRATIONS.length; i++) {
log.info("running migration", { index: i }) log.info("running migration", { index: i })
const step = MIGRATIONS[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)) { if (Exit.isFailure(exit)) {
log.error("failed to run migration", { index: i, cause: exit.cause }) log.error("failed to run migration", { index: i, cause: exit.cause })
break 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) const { runPromise } = makeRuntime(Service, defaultLayer)
@@ -7,6 +7,7 @@ import { tmpdir } from "../fixture/fixture"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Config } from "../../src/config/config" import { Config } from "../../src/config/config"
import { FileWatcher } from "../../src/file/watcher" import { FileWatcher } from "../../src/file/watcher"
import { Git } from "../../src/git"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
// Native @parcel/watcher bindings aren't reliably available in CI (missing on Linux, flaky on Windows) // 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 () => { fn: async () => {
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe( const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
Layer.provide(Config.defaultLayer), Layer.provide(Config.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(watcherConfigLayer), Layer.provide(watcherConfigLayer),
) )
const rt = ManagedRuntime.make(layer) const rt = ManagedRuntime.make(layer)
@@ -3,6 +3,7 @@ import fs from "fs/promises"
import path from "path" import path from "path"
import { Effect, Layer, ManagedRuntime } from "effect" import { Effect, Layer, ManagedRuntime } from "effect"
import { AppFileSystem } from "../../src/filesystem" import { AppFileSystem } from "../../src/filesystem"
import { Git } from "../../src/git"
import { Global } from "../../src/global" import { Global } from "../../src/global"
import { Storage } from "../../src/storage/storage" import { Storage } from "../../src/storage/storage"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
@@ -47,7 +48,7 @@ async function withStorage<T>(
root: string, root: string,
fn: (run: <A, E>(body: Effect.Effect<A, E, Storage.Service>) => Promise<A>) => Promise<T>, 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 { try {
return await fn((body) => rt.runPromise(body)) return await fn((body) => rt.runPromise(body))
} finally { } finally {