fix(core): stop repository discovery at nearest marker (#39714)

This commit is contained in:
Kit Langton 2026-07-30 13:28:29 -04:00 committed by GitHub
commit 9d55b223bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 48 additions and 21 deletions

View file

@ -201,7 +201,7 @@ const layer = Layer.effect(
locks.withLock(repository.gitDirectory)(effect)
const discover = Effect.fn("Git.repo.discover")(function* (input: AbsolutePath) {
const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe(
const dotgit = yield* fs.up({ targets: [".git"], start: input, mode: "first" }).pipe(
Effect.map((matches) => matches[0]),
Effect.catch(() => Effect.succeed(undefined)),
)

View file

@ -49,7 +49,7 @@ export const root = Effect.fn("Project.root")(function* (
fs: FSUtil.Interface,
input: AbsolutePath,
) {
return yield* fs.up({ targets: [".git", ".hg"], start: input }).pipe(
return yield* fs.up({ targets: [".git", ".hg"], start: input, mode: "first" }).pipe(
Effect.map((matches) => matches[0] ? AbsolutePath.make(path.dirname(matches[0])) : undefined),
Effect.catch(() => Effect.succeed(undefined)),
)
@ -224,7 +224,7 @@ const layer = Layer.effect(
})
const hgDiscover = Effect.fnUntraced(function* (input: AbsolutePath) {
const dotHg = yield* fs.up({ targets: [".hg"], start: input }).pipe(
const dotHg = yield* fs.up({ targets: [".hg"], start: input, mode: "first" }).pipe(
Effect.map((matches) => matches[0]),
Effect.catch(() => Effect.succeed(undefined)),
)

View file

@ -1,5 +1,5 @@
import { describe, test, expect } from "bun:test"
import { Effect, FileSystem } from "effect"
import { Effect, FileSystem, Layer } from "effect"
import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { FSUtil } from "@opencode-ai/util/fs-util"
@ -267,6 +267,33 @@ describe("FSUtil", () => {
expect(result).toContain(path.join(tmp, "b.txt"))
}),
)
it(
"stops at the first match when requested",
Effect.gen(function* () {
const filesys = yield* FileSystem.FileSystem
const tmp = yield* filesys.makeTempDirectoryScoped()
yield* filesys.writeFileString(path.join(tmp, "marker"), "root")
const child = path.join(tmp, "sub")
yield* filesys.makeDirectory(child)
yield* filesys.writeFileString(path.join(child, "marker"), "child")
const checked: string[] = []
const instrumented = FileSystem.FileSystem.of({
...filesys,
exists: (target) => Effect.sync(() => checked.push(target)).pipe(Effect.andThen(filesys.exists(target))),
})
const search = yield* FSUtil.Service.pipe(
Effect.provide(
FSUtil.layer.pipe(Layer.fresh, Layer.provide(Layer.succeed(FileSystem.FileSystem, instrumented))),
),
)
expect(yield* search.up({ targets: ["marker", "other"], start: child, mode: "first" })).toEqual([
path.join(child, "marker"),
])
expect(checked).toEqual([path.join(child, "marker")])
}),
)
})
describe("glob", () => {

View file

@ -28,6 +28,13 @@ export namespace FSUtil {
readonly type: "file" | "directory" | "symlink" | "other"
}
export interface UpOptions {
readonly targets: string[]
readonly start: string
readonly stop?: string
readonly mode?: "all" | "first"
}
export interface Interface extends FileSystem.FileSystem {
readonly isDir: (path: string) => Effect.Effect<boolean>
readonly isFile: (path: string) => Effect.Effect<boolean>
@ -40,7 +47,7 @@ export namespace FSUtil {
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
readonly resolve: (path: string) => Effect.Effect<string>
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
readonly up: (options: UpOptions) => Effect.Effect<string[], Error>
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
readonly scan: (pattern: string, options?: Glob.Options) => Effect.Effect<string[], Error>
readonly globMatch: (pattern: string, filepath: string) => boolean
@ -153,27 +160,16 @@ export namespace FSUtil {
})
})
const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) {
const result: string[] = []
let current = start
while (true) {
const search = join(current, target)
if (yield* fs.exists(search)) result.push(search)
if (stop === current) break
const parent = dirname(current)
if (parent === current) break
current = parent
}
return result
})
const up = Effect.fn("FileSystem.up")(function* (options: { targets: string[]; start: string; stop?: string }) {
const up = Effect.fn("FileSystem.up")(function* (options: UpOptions) {
const result: string[] = []
let current = options.start
while (true) {
for (const target of options.targets) {
const search = join(current, target)
if (yield* fs.exists(search)) result.push(search)
if (yield* fs.exists(search)) {
result.push(search)
if (options.mode === "first") return result
}
}
if (options.stop === current) break
const parent = dirname(current)
@ -183,6 +179,10 @@ export namespace FSUtil {
return result
})
const findUp = Effect.fn("FileSystem.findUp")((target: string, start: string, stop?: string) =>
up({ targets: [target], start, stop }),
)
const globUp = Effect.fn("FileSystem.globUp")(function* (pattern: string, start: string, stop?: string) {
const result: string[] = []
let current = start