fix(core): capture snapshots for a Location opened through a symlink (#61546)

This commit is contained in:
Luke Parker
2026-11-03 19:54:58 -06:00
committed by GitHub
Unverified
parent 0c4dd78fd6
commit 77264eb66e
2 changed files with 59 additions and 2 deletions
+7 -2
View File
@@ -105,10 +105,15 @@ const layer = Layer.effect(
)
const repository = repositoryFiber.pipe(Effect.uninterruptible, Effect.flatMap(Fiber.join))
// The worktree is a real path, so the Location must be one too: a symlinked or junctioned path would fall outside
// it, and a path in another letter case would become a pathspec that matches nothing.
const scope = Effect.fnUntraced(function* (worktree: AbsolutePath) {
if (!FSUtil.contains(worktree, location.directory))
const directory = yield* fs
.realPath(location.directory)
.pipe(Effect.mapError((cause) => failure("capture", cause)))
if (!FSUtil.contains(worktree, directory))
return yield* new Error({ operation: "capture", message: "Location is outside the project" })
return RelativePath.make(path.relative(worktree, location.directory).replaceAll("\\", "/") || ".")
return RelativePath.make(path.relative(worktree, directory).replaceAll("\\", "/") || ".")
})
const enabled = () => location.vcs?.type === "git" && state.get().enabled
+52
View File
@@ -216,6 +216,58 @@ describe("Snapshot", () => {
),
)
testEffect(Layer.empty).live("captures and restores a Location opened through a symlinked path", () =>
withTempDir((tmp) =>
Effect.gen(function* () {
const project = path.join(tmp.path, "project")
const link = path.join(tmp.path, "link")
yield* Effect.promise(async () => {
await fs.mkdir(path.join(project, "scope"), { recursive: true })
await fs.writeFile(path.join(project, "scope", "tracked.txt"), "one\n")
await initGit(project)
// A junction needs no privilege on Windows; other platforms ignore the type.
await fs.symlink(project, link, "junction")
})
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.capture()
yield* Effect.promise(() => fs.writeFile(path.join(link, "scope", "tracked.txt"), "two\n"))
const after = yield* snapshot.capture()
expect(before).toBeDefined()
expect(after).toBeDefined()
if (!before || !after) return
expect(yield* snapshot.files({ from: before, to: after })).toEqual([RelativePath.make("scope/tracked.txt")])
yield* snapshot.restore({ files: new Map([[RelativePath.make("scope/tracked.txt"), before]]) })
expect(yield* read(path.join(project, "scope", "tracked.txt"))).toBe("one\n")
}).pipe(Effect.provide(snapshotLayer(tmp.path, path.join(link, "scope"))))
}),
),
)
;(process.platform === "win32" ? testEffect(Layer.empty).live : testEffect(Layer.empty).live.skip)(
"captures a Location opened with different letter case",
() =>
withTempDir((tmp) =>
Effect.gen(function* () {
const project = path.join(tmp.path, "project")
yield* Effect.promise(async () => {
await fs.mkdir(path.join(project, "scope"), { recursive: true })
await fs.writeFile(path.join(project, "scope", "tracked.txt"), "one\n")
await initGit(project)
})
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.capture()
yield* Effect.promise(() => fs.writeFile(path.join(project, "scope", "tracked.txt"), "two\n"))
const after = yield* snapshot.capture()
expect(before).toBeDefined()
expect(after).toBeDefined()
if (!before || !after) return
expect(yield* snapshot.files({ from: before, to: after })).toEqual([RelativePath.make("scope/tracked.txt")])
}).pipe(Effect.provide(snapshotLayer(tmp.path, path.join(project, "SCOPE"))))
}),
),
)
testEffect(Layer.empty).live("restores many files from several trees and removes paths absent from them", () =>
withTempDir((tmp) =>
Effect.gen(function* () {