fix(core): restore parcel watch for git HEAD

Bun fs.watch misses git's atomic HEAD.lock rename, so checkout never
refreshed the cached branch. Watch the git dir with Parcel like dev,
keeping only HEAD and HEAD.lock.
This commit is contained in:
Aiden Cline
2026-08-10 17:35:50 -05:00
parent 33296e7959
commit e5b576ce3a
4 changed files with 43 additions and 9 deletions
@@ -4,7 +4,6 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Stream } from "effect"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Document } from "@opencode-ai/schema/config"
import path from "path"
import { Config } from "../config"
import { Bus } from "../bus"
import { FSUtil } from "@opencode-ai/util/fs-util"
@@ -36,22 +35,29 @@ const layer = Layer.effect(
.filter((entry): entry is Document => entry.type === "document")
.flatMap((item) => item.info.watcher?.ignore ?? [])
const watch = (dir: string, keep: readonly string[]) =>
Effect.gen(function* () {
const ignore = (yield* fs.readDirectoryEntries(dir).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
(entry) => (keep.includes(entry.name) ? [] : [entry.name]),
)
const updates = yield* watcher.subscribe({ path: dir, type: "directory", ignore })
yield* updates.pipe(Stream.runForEach(publish), Effect.forkScoped)
})
if (location.vcs?.type === "git") {
const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory
const vcs = resolved
? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved)))
: undefined
if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
const updates = yield* watcher.subscribe({ path: path.join(vcs, "HEAD"), type: "file" })
yield* updates.pipe(Stream.runForEach(publish), Effect.forkScoped)
yield* watch(vcs, ["HEAD", "HEAD.lock"])
}
}
if (location.vcs?.type === "hg") {
const store = location.vcs.store
const vcs = yield* fs.realPath(store).pipe(Effect.catch(() => Effect.succeed(store)))
if (!config.includes(".hg") && !config.includes(vcs)) {
const updates = yield* watcher.subscribe({ path: path.join(vcs, "branch"), type: "file" })
yield* updates.pipe(Stream.runForEach(publish), Effect.forkScoped)
yield* watch(vcs, ["branch"])
}
}
}).pipe(
+4 -1
View File
@@ -52,7 +52,10 @@ const layer = Layer.effect(
const store = yield* fs.realPath(vcs.store).pipe(Effect.catch(() => Effect.succeed(vcs.store)))
const isBranchMetadata =
vcs.type === "git"
? (file: string) => path.basename(file) === "HEAD" && FSUtil.contains(store, file)
? (file: string) => {
const name = path.basename(file)
return (name === "HEAD" || name === "HEAD.lock") && FSUtil.contains(store, file)
}
: (file: string) => path.resolve(file) === path.join(store, "branch")
yield* bus.subscribe(FileSystem.Event.Changed).pipe(
Stream.filter((event) => isBranchMetadata(event.data.file)),
+24 -2
View File
@@ -195,7 +195,12 @@ describe("LocationWatcher subscriptions", () => {
Effect.retry(Schedule.spaced("10 millis")),
)
yield* Effect.sleep("10 millis")
expect(subscriptions).toEqual([{ path: path.join(directory, ".git", "HEAD"), type: "file" }])
expect(subscriptions).toHaveLength(1)
const git = subscriptions[0]
if (git?.type !== "directory") throw new Error("expected a directory watch")
expect(git.path).toBe(path.join(directory, ".git"))
expect(git.ignore ?? []).not.toContain("HEAD")
expect(git.ignore ?? []).toContain("objects")
}),
{ vcs: "git", watcher },
)
@@ -218,7 +223,7 @@ describe("LocationWatcher subscriptions", () => {
Effect.retry(Schedule.spaced("10 millis")),
)
yield* Effect.sleep("10 millis")
expect(subscriptions).toEqual([{ path: path.join(directory, ".hg", "branch"), type: "file" }])
expect(subscriptions).toMatchObject([{ path: path.join(directory, ".hg"), type: "directory" }])
}),
{ vcs: "hg", watcher },
)
@@ -351,6 +356,23 @@ describeNative("LocationWatcher", () => {
),
)
it.live("publishes .git/HEAD events from git checkout", () =>
withTmp(
(directory) =>
Effect.gen(function* () {
const head = path.join(directory, ".git", "HEAD")
const branch = `watch-${Math.random().toString(36).slice(2)}`
yield* ready(head)
const event = yield* nextUpdate(
(item) => path.basename(item.file) === "HEAD" || path.basename(item.file) === "HEAD.lock",
Effect.promise(() => $`git checkout -q -b ${branch}`.cwd(directory).quiet()),
)
expect(["HEAD", "HEAD.lock"]).toContain(path.basename(event.file))
}),
{ vcs: "git" },
),
)
const describeSymlink = process.platform !== "win32" ? describe : describe.skip
describeSymlink("symlinked .git", () => {
it.live("publishes .git/HEAD events through a symlinked .git directory", () =>
+4 -1
View File
@@ -112,7 +112,10 @@ describe("Vcs", () => {
yield* bus.publish(FileSystem.Event.Changed, { file: path.join(directory, "HEAD"), event: "change" })
expect(yield* vcs.info()).toEqual({ branch: { current: "main", default: undefined } })
yield* bus.publish(FileSystem.Event.Changed, { file: path.join(directory, ".git", "HEAD"), event: "change" })
yield* bus.publish(FileSystem.Event.Changed, {
file: path.join(directory, ".git", "HEAD.lock"),
event: "change",
})
expect(yield* Fiber.join(updated)).toMatchObject({
_tag: "Some",
value: { location: { directory }, data: { branch: "feature" } },