Procházet zdrojové kódy

fix(watcher): resolve symlinked .git path before subscribing (#27016)

Co-authored-by: Simon Klee <hello@simonklee.dk>
Kagura před 3 měsíci
rodič
revize
1ac3f09468

+ 6 - 3
packages/opencode/src/file/watcher.ts

@@ -2,7 +2,7 @@ import { Cause, Effect, Layer, Context, Schema } from "effect"
 // @ts-ignore
 import { createWrapper } from "@parcel/watcher/wrapper"
 import type ParcelWatcher from "@parcel/watcher"
-import { readdir } from "fs/promises"
+import { readdir, realpath } from "fs/promises"
 import path from "path"
 import { Bus } from "@/bus"
 import { BusEvent } from "@/bus/bus-event"
@@ -131,8 +131,11 @@ export const layer = Layer.effect(
             const result = yield* git.run(["rev-parse", "--git-dir"], {
               cwd: ctx.worktree,
             })
-            const vcsDir = result.exitCode === 0 ? path.resolve(ctx.worktree, result.text().trim()) : undefined
-            if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
+            const resolved = result.exitCode === 0 ? path.resolve(ctx.worktree, result.text().trim()) : undefined
+            const vcsDir = resolved
+              ? yield* Effect.promise(() => realpath(resolved).catch(() => resolved))
+              : undefined
+            if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir) && (!resolved || !cfgIgnores.includes(resolved))) {
               const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter(
                 (entry) => entry !== "HEAD",
               )

+ 53 - 0
packages/opencode/test/file/watcher.test.ts

@@ -260,4 +260,57 @@ describeWatcher("FileWatcher", () => {
       }),
     { git: true },
   )
+
+  // Symlink support varies by platform; skip where unavailable
+  const describeSymlink = process.platform !== "win32" ? describe : describe.skip
+
+  describeSymlink("symlinked .git", () => {
+    it.instance(
+      "publishes .git/HEAD events through a symlinked .git directory",
+      () =>
+        Effect.gen(function* () {
+          const test = yield* TestInstance
+          const fs = yield* AppFileSystem.Service
+          const git = yield* Git.Service
+          const dir = test.directory
+          const actualGit = path.join(dir, "..", "tmp_actual_git_" + Math.random().toString(36).slice(2))
+
+          // Move .git to a sibling directory and replace with a symlink
+          yield* Effect.promise(() => import("fs")).pipe(
+            Effect.flatMap((nodeFs) =>
+              Effect.all([
+                Effect.promise(() => nodeFs.promises.rename(path.join(dir, ".git"), actualGit)),
+                Effect.promise(() => nodeFs.promises.symlink(actualGit, path.join(dir, ".git"))),
+              ]),
+            ),
+          )
+
+          yield* Effect.acquireRelease(
+            Effect.succeed(actualGit),
+            (p) => Effect.promise(() => import("fs").then((f) => f.promises.rm(p, { recursive: true, force: true }).catch(() => undefined))),
+          )
+
+          const head = path.join(dir, ".git", "HEAD")
+          const branch = `watch-${Math.random().toString(36).slice(2)}`
+          yield* git.run(["branch", branch], { cwd: dir })
+
+          yield* withWatcher(
+            dir,
+            nextUpdate(
+              dir,
+              (evt) => evt.file === path.join(actualGit, "HEAD") && evt.event !== "unlink",
+              fs.writeFileString(head, `ref: refs/heads/${branch}\n`),
+            ).pipe(
+              Effect.tap((evt) =>
+                Effect.sync(() => {
+                  expect(evt.file).toBe(path.join(actualGit, "HEAD"))
+                  expect(["add", "change"]).toContain(evt.event)
+                }),
+              ),
+            ),
+          )
+        }),
+      { git: true },
+    )
+  })
 })