Skip to content

Commit 91786d2

Browse files
authored
refactor(effect): use Git service in file and storage (#21803)
1 parent eca11ca commit 91786d2

5 files changed

Lines changed: 111 additions & 123 deletions

File tree

packages/opencode/src/file/index.ts

Lines changed: 90 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import path from "path"
1111
import z from "zod"
1212
import { Global } from "../global"
1313
import { Instance } from "../project/instance"
14-
import { Filesystem } from "../util/filesystem"
1514
import { Log } from "../util/log"
1615
import { Protected } from "./protected"
1716
import { Ripgrep } from "./ripgrep"
@@ -344,6 +343,7 @@ export namespace File {
344343
Service,
345344
Effect.gen(function* () {
346345
const appFs = yield* AppFileSystem.Service
346+
const git = yield* Git.Service
347347

348348
const state = yield* InstanceState.make<State>(
349349
Effect.fn("File.state")(() =>
@@ -410,107 +410,98 @@ export namespace File {
410410
cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void)))
411411
})
412412

413+
const gitText = Effect.fnUntraced(function* (args: string[]) {
414+
return (yield* git.run(args, { cwd: Instance.directory })).text()
415+
})
416+
413417
const init = Effect.fn("File.init")(function* () {
414418
yield* ensure()
415419
})
416420

417421
const status = Effect.fn("File.status")(function* () {
418422
if (Instance.project.vcs !== "git") return []
419423

420-
return yield* Effect.promise(async () => {
421-
const diffOutput = (
422-
await Git.run(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], {
423-
cwd: Instance.directory,
424+
const diffOutput = yield* gitText([
425+
"-c",
426+
"core.fsmonitor=false",
427+
"-c",
428+
"core.quotepath=false",
429+
"diff",
430+
"--numstat",
431+
"HEAD",
432+
])
433+
434+
const changed: File.Info[] = []
435+
436+
if (diffOutput.trim()) {
437+
for (const line of diffOutput.trim().split("\n")) {
438+
const [added, removed, file] = line.split("\t")
439+
changed.push({
440+
path: file,
441+
added: added === "-" ? 0 : parseInt(added, 10),
442+
removed: removed === "-" ? 0 : parseInt(removed, 10),
443+
status: "modified",
424444
})
425-
).text()
426-
427-
const changed: File.Info[] = []
428-
429-
if (diffOutput.trim()) {
430-
for (const line of diffOutput.trim().split("\n")) {
431-
const [added, removed, file] = line.split("\t")
432-
changed.push({
433-
path: file,
434-
added: added === "-" ? 0 : parseInt(added, 10),
435-
removed: removed === "-" ? 0 : parseInt(removed, 10),
436-
status: "modified",
437-
})
438-
}
439445
}
446+
}
440447

441-
const untrackedOutput = (
442-
await Git.run(
443-
[
444-
"-c",
445-
"core.fsmonitor=false",
446-
"-c",
447-
"core.quotepath=false",
448-
"ls-files",
449-
"--others",
450-
"--exclude-standard",
451-
],
452-
{
453-
cwd: Instance.directory,
454-
},
455-
)
456-
).text()
457-
458-
if (untrackedOutput.trim()) {
459-
for (const file of untrackedOutput.trim().split("\n")) {
460-
try {
461-
const content = await Filesystem.readText(path.join(Instance.directory, file))
462-
changed.push({
463-
path: file,
464-
added: content.split("\n").length,
465-
removed: 0,
466-
status: "added",
467-
})
468-
} catch {
469-
continue
470-
}
471-
}
448+
const untrackedOutput = yield* gitText([
449+
"-c",
450+
"core.fsmonitor=false",
451+
"-c",
452+
"core.quotepath=false",
453+
"ls-files",
454+
"--others",
455+
"--exclude-standard",
456+
])
457+
458+
if (untrackedOutput.trim()) {
459+
for (const file of untrackedOutput.trim().split("\n")) {
460+
const content = yield* appFs
461+
.readFileString(path.join(Instance.directory, file))
462+
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
463+
if (content === undefined) continue
464+
changed.push({
465+
path: file,
466+
added: content.split("\n").length,
467+
removed: 0,
468+
status: "added",
469+
})
472470
}
471+
}
473472

474-
const deletedOutput = (
475-
await Git.run(
476-
[
477-
"-c",
478-
"core.fsmonitor=false",
479-
"-c",
480-
"core.quotepath=false",
481-
"diff",
482-
"--name-only",
483-
"--diff-filter=D",
484-
"HEAD",
485-
],
486-
{
487-
cwd: Instance.directory,
488-
},
489-
)
490-
).text()
491-
492-
if (deletedOutput.trim()) {
493-
for (const file of deletedOutput.trim().split("\n")) {
494-
changed.push({
495-
path: file,
496-
added: 0,
497-
removed: 0,
498-
status: "deleted",
499-
})
500-
}
473+
const deletedOutput = yield* gitText([
474+
"-c",
475+
"core.fsmonitor=false",
476+
"-c",
477+
"core.quotepath=false",
478+
"diff",
479+
"--name-only",
480+
"--diff-filter=D",
481+
"HEAD",
482+
])
483+
484+
if (deletedOutput.trim()) {
485+
for (const file of deletedOutput.trim().split("\n")) {
486+
changed.push({
487+
path: file,
488+
added: 0,
489+
removed: 0,
490+
status: "deleted",
491+
})
501492
}
493+
}
502494

503-
return changed.map((item) => {
504-
const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path)
505-
return {
506-
...item,
507-
path: path.relative(Instance.directory, full),
508-
}
509-
})
495+
return changed.map((item) => {
496+
const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path)
497+
return {
498+
...item,
499+
path: path.relative(Instance.directory, full),
500+
}
510501
})
511502
})
512503

513-
const read = Effect.fn("File.read")(function* (file: string) {
504+
const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
514505
using _ = log.time("read", { file })
515506
const full = path.join(Instance.directory, file)
516507

@@ -558,27 +549,19 @@ export namespace File {
558549
)
559550

560551
if (Instance.project.vcs === "git") {
561-
return yield* Effect.promise(async (): Promise<File.Content> => {
562-
let diff = (
563-
await Git.run(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })
564-
).text()
565-
if (!diff.trim()) {
566-
diff = (
567-
await Git.run(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], {
568-
cwd: Instance.directory,
569-
})
570-
).text()
571-
}
572-
if (diff.trim()) {
573-
const original = (await Git.run(["show", `HEAD:${file}`], { cwd: Instance.directory })).text()
574-
const patch = structuredPatch(file, file, original, content, "old", "new", {
575-
context: Infinity,
576-
ignoreWhitespace: true,
577-
})
578-
return { type: "text", content, patch, diff: formatPatch(patch) }
579-
}
580-
return { type: "text", content }
581-
})
552+
let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
553+
if (!diff.trim()) {
554+
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
555+
}
556+
if (diff.trim()) {
557+
const original = yield* git.show(Instance.directory, "HEAD", file)
558+
const patch = structuredPatch(file, file, original, content, "old", "new", {
559+
context: Infinity,
560+
ignoreWhitespace: true,
561+
})
562+
return { type: "text" as const, content, patch, diff: formatPatch(patch) }
563+
}
564+
return { type: "text" as const, content }
582565
}
583566

584567
return { type: "text" as const, content }
@@ -660,7 +643,7 @@ export namespace File {
660643
}),
661644
)
662645

663-
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
646+
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
664647

665648
const { runPromise } = makeRuntime(Service, defaultLayer)
666649

packages/opencode/src/file/watcher.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export namespace FileWatcher {
7171
Service,
7272
Effect.gen(function* () {
7373
const config = yield* Config.Service
74+
const git = yield* Git.Service
7475

7576
const state = yield* InstanceState.make(
7677
Effect.fn("FileWatcher.state")(
@@ -131,11 +132,9 @@ export namespace FileWatcher {
131132
}
132133

133134
if (Instance.project.vcs === "git") {
134-
const result = yield* Effect.promise(() =>
135-
Git.run(["rev-parse", "--git-dir"], {
136-
cwd: Instance.project.worktree,
137-
}),
138-
)
135+
const result = yield* git.run(["rev-parse", "--git-dir"], {
136+
cwd: Instance.project.worktree,
137+
})
139138
const vcsDir =
140139
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
141140
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
@@ -161,7 +160,7 @@ export namespace FileWatcher {
161160
}),
162161
)
163162

164-
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
163+
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
165164

166165
const { runPromise } = makeRuntime(Service, defaultLayer)
167166

packages/opencode/src/storage/storage.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { Git } from "@/git"
1111
export namespace Storage {
1212
const log = Log.create({ service: "storage" })
1313

14-
type Migration = (dir: string, fs: AppFileSystem.Interface) => Effect.Effect<void, AppFileSystem.Error>
14+
type Migration = (
15+
dir: string,
16+
fs: AppFileSystem.Interface,
17+
git: Git.Interface,
18+
) => Effect.Effect<void, AppFileSystem.Error>
1519

1620
export const NotFoundError = NamedError.create(
1721
"NotFoundError",
@@ -83,7 +87,7 @@ export namespace Storage {
8387
}
8488

8589
const MIGRATIONS: Migration[] = [
86-
Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface) {
90+
Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface, git: Git.Interface) {
8791
const project = path.resolve(dir, "../project")
8892
if (!(yield* fs.isDir(project))) return
8993
const projectDirs = yield* fs.glob("*", {
@@ -110,11 +114,9 @@ export namespace Storage {
110114
}
111115
if (!worktree) continue
112116
if (!(yield* fs.isDir(worktree))) continue
113-
const result = yield* Effect.promise(() =>
114-
Git.run(["rev-list", "--max-parents=0", "--all"], {
115-
cwd: worktree,
116-
}),
117-
)
117+
const result = yield* git.run(["rev-list", "--max-parents=0", "--all"], {
118+
cwd: worktree,
119+
})
118120
const [id] = result
119121
.text()
120122
.split("\n")
@@ -220,6 +222,7 @@ export namespace Storage {
220222
Service,
221223
Effect.gen(function* () {
222224
const fs = yield* AppFileSystem.Service
225+
const git = yield* Git.Service
223226
const locks = yield* RcMap.make({
224227
lookup: () => TxReentrantLock.make(),
225228
idleTimeToLive: 0,
@@ -236,7 +239,7 @@ export namespace Storage {
236239
for (let i = migration; i < MIGRATIONS.length; i++) {
237240
log.info("running migration", { index: i })
238241
const step = MIGRATIONS[i]!
239-
const exit = yield* Effect.exit(step(dir, fs))
242+
const exit = yield* Effect.exit(step(dir, fs, git))
240243
if (Exit.isFailure(exit)) {
241244
log.error("failed to run migration", { index: i, cause: exit.cause })
242245
break
@@ -327,7 +330,7 @@ export namespace Storage {
327330
}),
328331
)
329332

330-
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
333+
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
331334

332335
const { runPromise } = makeRuntime(Service, defaultLayer)
333336

packages/opencode/test/file/watcher.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { tmpdir } from "../fixture/fixture"
77
import { Bus } from "../../src/bus"
88
import { Config } from "../../src/config/config"
99
import { FileWatcher } from "../../src/file/watcher"
10+
import { Git } from "../../src/git"
1011
import { Instance } from "../../src/project/instance"
1112

1213
// 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>) {
3233
fn: async () => {
3334
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
3435
Layer.provide(Config.defaultLayer),
36+
Layer.provide(Git.defaultLayer),
3537
Layer.provide(watcherConfigLayer),
3638
)
3739
const rt = ManagedRuntime.make(layer)

packages/opencode/test/storage/storage.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from "fs/promises"
33
import path from "path"
44
import { Effect, Layer, ManagedRuntime } from "effect"
55
import { AppFileSystem } from "../../src/filesystem"
6+
import { Git } from "../../src/git"
67
import { Global } from "../../src/global"
78
import { Storage } from "../../src/storage/storage"
89
import { tmpdir } from "../fixture/fixture"
@@ -47,7 +48,7 @@ async function withStorage<T>(
4748
root: string,
4849
fn: (run: <A, E>(body: Effect.Effect<A, E, Storage.Service>) => Promise<A>) => Promise<T>,
4950
) {
50-
const rt = ManagedRuntime.make(Storage.layer.pipe(Layer.provide(layer(root))))
51+
const rt = ManagedRuntime.make(Storage.layer.pipe(Layer.provide(layer(root)), Layer.provide(Git.defaultLayer)))
5152
try {
5253
return await fn((body) => rt.runPromise(body))
5354
} finally {

0 commit comments

Comments
 (0)