fix(watcher): filter ignored paths before publishing events, handle worktree git dirs

The parcel/watcher callback was shared and published all filesystem events
without re-filtering through FileIgnore. Build artifacts and lock files in
ignored directories (out/, .turbo/, etc.) fired watcher events that drove
repeated refreshVcs() calls and made the review panel flicker.

Also fixes the frontend listener to handle absolute .git/worktrees/... paths
produced by git worktrees, which bypassed the old startsWith('.git/') check.

Fixes #21452
pull/21453/head
Richard Cool 2026-04-07 22:04:49 -07:00
parent ae614d919f
commit 036a100f2a
2 changed files with 30 additions and 11 deletions

View File

@ -931,7 +931,11 @@ export default function Page() {
? (evt.details.properties as Record<string, unknown>)
: undefined
const file = typeof props?.file === "string" ? props.file : undefined
if (!file || file.startsWith(".git/")) return
if (!file) return
const path = file.replaceAll("\\", "/")
// Worktree watcher events can arrive as absolute .git/worktrees/... paths.
const git = path.startsWith(".git/") || path.includes("/.git/")
if (git) return
refreshVcs()
})
onCleanup(stopVcs)

View File

@ -59,6 +59,13 @@ export namespace FileWatcher {
})
}
function rel(dir: string, file: string) {
const next = path.relative(dir, file).replaceAll("\\", "/")
if (path.isAbsolute(next)) return
if (next === ".." || next.startsWith("../")) return
return next
}
export const hasNativeBinding = () => !!watcher()
export interface Interface {
@ -95,16 +102,24 @@ export namespace FileWatcher {
Effect.promise(() => Promise.allSettled(subs.map((sub) => sub.unsubscribe()))),
)
const cb: ParcelWatcher.SubscribeCallback = Instance.bind((err, evts) => {
if (err) return
for (const evt of evts) {
if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
}
})
const subscribe = (dir: string, ignore: string[], git = false) => {
const cb: ParcelWatcher.SubscribeCallback = Instance.bind((err, evts) => {
if (err) return
for (const evt of evts) {
const file = rel(dir, evt.path)
if (file === undefined) continue
if (git) {
if (file !== "HEAD") continue
} else if (FileIgnore.match(file, { extra: cfgIgnores })) {
continue
}
if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
}
})
const subscribe = (dir: string, ignore: string[]) => {
const pending = w.subscribe(dir, cb, { ignore, backend })
return Effect.gen(function* () {
const sub = yield* Effect.promise(() => pending)
@ -142,7 +157,7 @@ export namespace FileWatcher {
const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter(
(entry) => entry !== "HEAD",
)
yield* subscribe(vcsDir, ignore)
yield* subscribe(vcsDir, ignore, true)
}
}
},