mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-07 22:23:24 +00:00
fix(core): bound protected home searches (#40518)
This commit is contained in:
parent
74e3155ef0
commit
45d58717a4
6 changed files with 100 additions and 14 deletions
|
|
@ -3,7 +3,6 @@ export * as LocationWatcher from "./location-watcher"
|
|||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Config } from "../config"
|
||||
import { Bus } from "../bus"
|
||||
|
|
@ -44,7 +43,7 @@ const layer = Layer.effect(
|
|||
const config = (yield* configService.entries())
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.flatMap((item) => item.info.watcher?.ignore ?? [])
|
||||
const home = path.resolve(location.directory) === path.resolve(os.homedir())
|
||||
const home = Protected.isHome(location.directory)
|
||||
|
||||
if (!home && location.vcs) {
|
||||
const updates = yield* watcher.subscribe({
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import path from "path"
|
|||
|
||||
const home = os.homedir()
|
||||
|
||||
export function isHome(directory: string) {
|
||||
return path.resolve(directory) === path.resolve(home)
|
||||
}
|
||||
|
||||
const DARWIN_HOME = [
|
||||
"Music",
|
||||
"Pictures",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
|
|||
import { Location } from "../location"
|
||||
import { Ripgrep } from "../ripgrep"
|
||||
import { RelativePath } from "../schema"
|
||||
import { Protected } from "./protected"
|
||||
|
||||
export interface Interface {
|
||||
readonly find: (input: FileSystem.FindInput) => Effect.Effect<FileSystem.Entry[]>
|
||||
|
|
@ -33,11 +34,13 @@ export const ripgrepLayer = Layer.effect(
|
|||
const scope = yield* Scope.Scope
|
||||
const files: string[] = []
|
||||
const directories = new Set<string>()
|
||||
const home = Protected.isHome(location.directory)
|
||||
yield* ripgrep
|
||||
.find({
|
||||
cwd: location.directory,
|
||||
pattern: "*",
|
||||
limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000,
|
||||
limit: location.vcs && !home ? Number.MAX_SAFE_INTEGER : 100_000,
|
||||
exclude: home ? [...Protected.names()].map((name) => `${name}/**`) : undefined,
|
||||
onEntry: (entry) =>
|
||||
Effect.sync(() => {
|
||||
files.push(entry.path)
|
||||
|
|
@ -232,15 +235,16 @@ export const fffLayer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
export const layer = (options?: Options) => Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
if (options?.fff === false || (options?.fff === undefined && process.platform === "win32") || !Fff.available())
|
||||
return ripgrepLayer
|
||||
const location = yield* Location.Service
|
||||
// Non-VCS locations can contain many repositories, so avoid eagerly content-indexing the entire aggregate tree.
|
||||
return location.vcs ? fffLayer : ripgrepLayer
|
||||
}),
|
||||
)
|
||||
export const layer = (options?: Options) =>
|
||||
Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
if (options?.fff === false || (options?.fff === undefined && process.platform === "win32") || !Fff.available())
|
||||
return ripgrepLayer
|
||||
const location = yield* Location.Service
|
||||
// Non-VCS locations can contain many repositories, so avoid eagerly content-indexing the entire aggregate tree.
|
||||
return location.vcs && !Protected.isHome(location.directory) ? fffLayer : ripgrepLayer
|
||||
}),
|
||||
)
|
||||
|
||||
export function configured(options?: Options) {
|
||||
return makeLocationNode({ service: Service, layer: layer(options), deps: [FSUtil.node, Location.node, Ripgrep.node] })
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export interface FindInput {
|
|||
readonly cwd: string
|
||||
readonly pattern: string
|
||||
readonly limit: number
|
||||
readonly exclude?: readonly string[]
|
||||
readonly hidden?: boolean
|
||||
readonly follow?: boolean
|
||||
readonly signal?: AbortSignal
|
||||
|
|
@ -195,6 +196,7 @@ const layer = Layer.effect(
|
|||
...(input.hidden ? ["--hidden"] : []),
|
||||
...(input.follow ? ["--follow"] : []),
|
||||
...(input.pattern === "*" ? [] : [`--glob=${input.pattern}`]),
|
||||
...(input.exclude ?? []).map((pattern) => `--glob=!${pattern}`),
|
||||
"--glob=!**/.git/**",
|
||||
".",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Protected } from "@opencode-ai/core/filesystem/protected"
|
||||
import { FileSystemSearch } from "@opencode-ai/core/filesystem/search"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
|
|
@ -42,3 +49,50 @@ describe("Ripgrep", () => {
|
|||
),
|
||||
)
|
||||
})
|
||||
|
||||
describe("FileSystemSearch", () => {
|
||||
test("bounds a home scan even when home is detected as a repository", async () => {
|
||||
let observed: Ripgrep.FindInput | undefined
|
||||
const home = AbsolutePath.make(os.homedir())
|
||||
const layer = AppNodeBuilder.build(FileSystemSearch.node, [
|
||||
[
|
||||
Location.node,
|
||||
Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
location({ directory: home }, { vcs: { type: "git", store: AbsolutePath.make(path.join(home, ".git")) } }),
|
||||
),
|
||||
),
|
||||
],
|
||||
[
|
||||
Ripgrep.node,
|
||||
Layer.succeed(
|
||||
Ripgrep.Service,
|
||||
Ripgrep.Service.of({
|
||||
find: (input) =>
|
||||
Effect.gen(function* () {
|
||||
observed = input
|
||||
if (input.onEntry)
|
||||
yield* input.onEntry(FileSystem.Entry.make({ path: RelativePath.make("src/index.ts"), type: "file" }))
|
||||
return []
|
||||
}),
|
||||
glob: () => Effect.succeed([]),
|
||||
grep: () => Effect.succeed([]),
|
||||
}),
|
||||
),
|
||||
],
|
||||
])
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const search = yield* FileSystemSearch.Service
|
||||
yield* Effect.sleep("10 millis")
|
||||
expect(observed?.limit).toBe(100_000)
|
||||
expect(observed?.exclude).toEqual([...Protected.names()].map((name) => `${name}/**`))
|
||||
expect((yield* search.find({ query: "src", type: "directory" }))[0]?.path).toBe(
|
||||
RelativePath.make(`src${path.sep}`),
|
||||
)
|
||||
}).pipe(Effect.provide(layer), Effect.scoped),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -63,6 +63,29 @@ describe("Ripgrep", () => {
|
|||
),
|
||||
)
|
||||
|
||||
it.live("excludes protected directory trees from catch-all find results", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "Pictures")))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "Pictures", "private.jpg"), "private\n"))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "visible.txt"), "visible\n"))
|
||||
|
||||
const files = yield* (yield* Ripgrep.Service).find({
|
||||
cwd: tmp.path,
|
||||
pattern: "*",
|
||||
limit: 10,
|
||||
exclude: ["Pictures/**"],
|
||||
})
|
||||
|
||||
expect(files.map((item) => item.path)).toContain(RelativePath.make("visible.txt"))
|
||||
expect(files.map((item) => item.path)).not.toContain(RelativePath.make("Pictures/private.jpg"))
|
||||
}),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("returns a bounded preview for matches on oversized lines", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue