mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 03:32:55 +00:00
fix(server): retain literal symlink targets in Git event identity
Canonical realpaths can erase a symlink target spelling when its parent is also a symlink, such as /var on macOS. Resolve these aliases once during setup so deleted and ignored event paths still match without per-event IO.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { realpath } from "node:fs";
|
||||
import { readlink } from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import pLimit from "p-limit";
|
||||
import { sep } from "node:path";
|
||||
import { dirname, resolve as resolvePath, sep } from "node:path";
|
||||
import { looksLikeDefiniteWindowsPath, normalizePathPreservingCase } from "../utils/path.js";
|
||||
|
||||
const nativeRealpath = promisify(realpath.native);
|
||||
@@ -31,6 +32,22 @@ export class GitObservationPaths {
|
||||
/* Missing roots retain lexical identity. */
|
||||
}
|
||||
}
|
||||
// A link may name /var/... while realpath returns /private/var/... on macOS.
|
||||
// Retain literal targets too, including chains, without following links in callbacks.
|
||||
let current = root;
|
||||
const visited = new Set<string>();
|
||||
for (let remaining = 40; remaining > 0; remaining--) {
|
||||
const alias = normalizePath(current);
|
||||
if (visited.has(alias)) break;
|
||||
visited.add(alias);
|
||||
aliases.add(alias);
|
||||
try {
|
||||
const target = await identityIo(() => readlink(current));
|
||||
current = resolvePath(dirname(current), target);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new GitObservationPaths(root, [...aliases], windows);
|
||||
}
|
||||
|
||||
|
||||
@@ -671,3 +671,27 @@ test("concurrent alias requests establish one working-tree subscription", async
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("Git event identity retains a symlink's literal target before canonicalizing its parent", async () => {
|
||||
const { mkdir, symlink } = await import("node:fs/promises");
|
||||
const { GitObservationPaths } = await import("./git-observation-paths.js");
|
||||
const root = realpathSync(mkdtempSync(path.join(tmpdir(), "git-observation-literal-target-")));
|
||||
const repository = path.join(root, "repository");
|
||||
const parentAlias = path.join(root, "parent-alias");
|
||||
const workspaceAlias = path.join(root, "workspace-alias");
|
||||
const literalTarget = path.join(parentAlias, "repository");
|
||||
const linkType = process.platform === "win32" ? "junction" : "dir";
|
||||
try {
|
||||
await mkdir(repository);
|
||||
await symlink(root, parentAlias, linkType);
|
||||
await symlink(literalTarget, workspaceAlias, linkType);
|
||||
const paths = await GitObservationPaths.create(workspaceAlias);
|
||||
const accept = paths.createEventFilter([path.join(workspaceAlias, "ignored")]);
|
||||
for (const spelling of [workspaceAlias, literalTarget, repository]) {
|
||||
expect(paths.relative(path.join(spelling, "deleted.txt"))).toBe("deleted.txt");
|
||||
expect(accept(path.join(spelling, "ignored", "deleted.txt"))).toBe(false);
|
||||
}
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user