refactor: use InstanceState context in File service (#23015)

This commit is contained in:
Kit Langton
2026-04-17 22:08:57 -04:00
committed by GitHub
parent daaa1c7e26
commit 5fa1673341
2 changed files with 29 additions and 21 deletions

View File

@@ -356,8 +356,9 @@ export const layer = Layer.effect(
) )
const scan = Effect.fn("File.scan")(function* () { const scan = Effect.fn("File.scan")(function* () {
if (Instance.directory === path.parse(Instance.directory).root) return const ctx = yield* InstanceState.context
const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global" if (ctx.directory === path.parse(ctx.directory).root) return
const isGlobalHome = ctx.directory === Global.Path.home && ctx.project.id === "global"
const next: Entry = { files: [], dirs: [] } const next: Entry = { files: [], dirs: [] }
if (isGlobalHome) { if (isGlobalHome) {
@@ -366,14 +367,14 @@ export const layer = Layer.effect(
const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"]) const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name) const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name) const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name)
const top = yield* appFs.readDirectoryEntries(Instance.directory).pipe(Effect.orElseSucceed(() => [])) const top = yield* appFs.readDirectoryEntries(ctx.directory).pipe(Effect.orElseSucceed(() => []))
for (const entry of top) { for (const entry of top) {
if (entry.type !== "directory") continue if (entry.type !== "directory") continue
if (shouldIgnoreName(entry.name)) continue if (shouldIgnoreName(entry.name)) continue
dirs.add(entry.name + "/") dirs.add(entry.name + "/")
const base = path.join(Instance.directory, entry.name) const base = path.join(ctx.directory, entry.name)
const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => [])) const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => []))
for (const child of children) { for (const child of children) {
if (child.type !== "directory") continue if (child.type !== "directory") continue
@@ -384,7 +385,7 @@ export const layer = Layer.effect(
next.dirs = Array.from(dirs).toSorted() next.dirs = Array.from(dirs).toSorted()
} else { } else {
const files = yield* rg.files({ cwd: Instance.directory }).pipe( const files = yield* rg.files({ cwd: ctx.directory }).pipe(
Stream.runCollect, Stream.runCollect,
Effect.map((chunk) => [...chunk]), Effect.map((chunk) => [...chunk]),
) )
@@ -416,7 +417,7 @@ export const layer = Layer.effect(
}) })
const gitText = Effect.fnUntraced(function* (args: string[]) { const gitText = Effect.fnUntraced(function* (args: string[]) {
return (yield* git.run(args, { cwd: Instance.directory })).text() return (yield* git.run(args, { cwd: (yield* InstanceState.context).directory })).text()
}) })
const init = Effect.fn("File.init")(function* () { const init = Effect.fn("File.init")(function* () {
@@ -424,7 +425,8 @@ export const layer = Layer.effect(
}) })
const status = Effect.fn("File.status")(function* () { const status = Effect.fn("File.status")(function* () {
if (Instance.project.vcs !== "git") return [] const ctx = yield* InstanceState.context
if (ctx.project.vcs !== "git") return []
const diffOutput = yield* gitText([ const diffOutput = yield* gitText([
"-c", "-c",
@@ -463,7 +465,7 @@ export const layer = Layer.effect(
if (untrackedOutput.trim()) { if (untrackedOutput.trim()) {
for (const file of untrackedOutput.trim().split("\n")) { for (const file of untrackedOutput.trim().split("\n")) {
const content = yield* appFs const content = yield* appFs
.readFileString(path.join(Instance.directory, file)) .readFileString(path.join(ctx.directory, file))
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined))) .pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
if (content === undefined) continue if (content === undefined) continue
changed.push({ changed.push({
@@ -498,19 +500,22 @@ export const layer = Layer.effect(
} }
return changed.map((item) => { return changed.map((item) => {
const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path) const full = path.isAbsolute(item.path) ? item.path : path.join(ctx.directory, item.path)
return { return {
...item, ...item,
path: path.relative(Instance.directory, full), path: path.relative(ctx.directory, full),
} }
}) })
}) })
const read: Interface["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 ctx = yield* InstanceState.context
const full = path.join(ctx.directory, file)
if (!Instance.containsPath(full)) throw new Error("Access denied: path escapes project directory") if (!Instance.containsPath(full, ctx)) {
throw new Error("Access denied: path escapes project directory")
}
if (isImageByExtension(file)) { if (isImageByExtension(file)) {
const exists = yield* appFs.existsSafe(full) const exists = yield* appFs.existsSafe(full)
@@ -553,13 +558,13 @@ export const layer = Layer.effect(
Effect.catch(() => Effect.succeed("")), Effect.catch(() => Effect.succeed("")),
) )
if (Instance.project.vcs === "git") { if (ctx.project.vcs === "git") {
let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file]) let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
if (!diff.trim()) { if (!diff.trim()) {
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file]) diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
} }
if (diff.trim()) { if (diff.trim()) {
const original = yield* git.show(Instance.directory, "HEAD", file) const original = yield* git.show(ctx.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,
@@ -573,21 +578,24 @@ export const layer = Layer.effect(
}) })
const list = Effect.fn("File.list")(function* (dir?: string) { const list = Effect.fn("File.list")(function* (dir?: string) {
const ctx = yield* InstanceState.context
const exclude = [".git", ".DS_Store"] const exclude = [".git", ".DS_Store"]
let ignored = (_: string) => false let ignored = (_: string) => false
if (Instance.project.vcs === "git") { if (ctx.project.vcs === "git") {
const ig = ignore() const ig = ignore()
const gitignore = path.join(Instance.project.worktree, ".gitignore") const gitignore = path.join(ctx.worktree, ".gitignore")
const gitignoreText = yield* appFs.readFileString(gitignore).pipe(Effect.catch(() => Effect.succeed(""))) const gitignoreText = yield* appFs.readFileString(gitignore).pipe(Effect.catch(() => Effect.succeed("")))
if (gitignoreText) ig.add(gitignoreText) if (gitignoreText) ig.add(gitignoreText)
const ignoreFile = path.join(Instance.project.worktree, ".ignore") const ignoreFile = path.join(ctx.worktree, ".ignore")
const ignoreText = yield* appFs.readFileString(ignoreFile).pipe(Effect.catch(() => Effect.succeed(""))) const ignoreText = yield* appFs.readFileString(ignoreFile).pipe(Effect.catch(() => Effect.succeed("")))
if (ignoreText) ig.add(ignoreText) if (ignoreText) ig.add(ignoreText)
ignored = ig.ignores.bind(ig) ignored = ig.ignores.bind(ig)
} }
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory const resolved = dir ? path.join(ctx.directory, dir) : ctx.directory
if (!Instance.containsPath(resolved)) throw new Error("Access denied: path escapes project directory") if (!Instance.containsPath(resolved, ctx)) {
throw new Error("Access denied: path escapes project directory")
}
const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => [])) const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => []))
@@ -595,7 +603,7 @@ export const layer = Layer.effect(
for (const entry of entries) { for (const entry of entries) {
if (exclude.includes(entry.name)) continue if (exclude.includes(entry.name)) continue
const absolute = path.join(resolved, entry.name) const absolute = path.join(resolved, entry.name)
const file = path.relative(Instance.directory, absolute) const file = path.relative(ctx.directory, absolute)
const type = entry.type === "directory" ? "directory" : "file" const type = entry.type === "directory" ? "directory" : "file"
nodes.push({ nodes.push({
name: entry.name, name: entry.name,

View File

@@ -96,7 +96,7 @@ export const Instance = {
if (AppFileSystem.contains(instance.directory, filepath)) return true if (AppFileSystem.contains(instance.directory, filepath)) return true
// Non-git projects set worktree to "/" which would match ANY absolute path. // Non-git projects set worktree to "/" which would match ANY absolute path.
// Skip worktree check in this case to preserve external_directory permissions. // Skip worktree check in this case to preserve external_directory permissions.
if (Instance.worktree === "/") return false if (instance.worktree === "/") return false
return AppFileSystem.contains(instance.worktree, filepath) return AppFileSystem.contains(instance.worktree, filepath)
}, },
/** /**