Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 90 additions & 107 deletions packages/opencode/src/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")(() =>
Expand Down Expand Up @@ -410,107 +410,98 @@ 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()
})

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)

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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)

Expand Down
11 changes: 5 additions & 6 deletions packages/opencode/src/file/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")(
Expand Down Expand Up @@ -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)) {
Expand All @@ -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)

Expand Down
21 changes: 12 additions & 9 deletions packages/opencode/src/storage/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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("*", {
Expand All @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/test/file/watcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/test/storage/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
Loading