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:
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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", () =>
|
||||
|
||||
@@ -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" } },
|
||||
|
||||
Reference in New Issue
Block a user